From 8715690ce9bf43448d28b8784e1832170cf2791f Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 12 Feb 2014 10:23:40 +0000 Subject: [PATCH] Intial open source comment --- services/web/.gitignore | 69 + services/web/.npmignore | 4 + services/web/BackgroundJobsWorker.coffee | 27 + services/web/Gruntfile.coffee | 197 + services/web/TpdsWorker.coffee | 98 + services/web/app.coffee | 43 + .../Analytics/AnalyticsManager.coffee | 69 + .../AuthenticationController.coffee | 113 + .../AuthenticationManager.coffee | 54 + .../CollaboratorsController.coffee | 49 + .../Features/Compile/ClsiManager.coffee | 97 + .../Features/Compile/CompileController.coffee | 45 + .../Features/Compile/CompileManager.coffee | 82 + .../DocumentUpdaterHandler.coffee | 141 + .../Documents/DocumentController.coffee | 36 + .../ProjectDownloadsController.coffee | 25 + .../Downloads/ProjectZipStreamManager.coffee | 46 + .../Features/Dropbox/DropboxHandler.coffee | 81 + .../Features/Editor/EditorController.coffee | 245 + .../Editor/EditorRealTimeController.coffee | 31 + .../Editor/EditorUpdatesController.coffee | 68 + .../FileStore/FileStoreHandler.coffee | 61 + .../HealthCheck/HealthCheckController.coffee | 42 + .../Project/DocLinesComparitor.coffee | 10 + .../Project/ProjectApiController.coffee | 13 + .../Project/ProjectCreationHandler.coffee | 82 + .../Features/Project/ProjectDeleter.coffee | 19 + .../Project/ProjectDetailsHandler.coffee | 26 + .../Features/Project/ProjectDuplicator.coffee | 53 + .../Project/ProjectEditorHandler.coffee | 63 + .../Project/ProjectEntityHandler.coffee | 355 + .../Features/Project/ProjectGetter.coffee | 61 + .../Features/Project/ProjectLocator.coffee | 141 + .../Project/ProjectOptionsHandler.coffee | 38 + .../Project/ProjectRootDocManager.coffee | 19 + .../Project/ProjectUpdateHandler.coffee | 10 + .../Features/Referal/ReferalAllocator.coffee | 59 + .../Features/Referal/ReferalConnect.coffee | 34 + .../Features/Referal/ReferalController.coffee | 10 + .../Features/Referal/ReferalHandler.coffee | 7 + .../Features/Referal/ReferalMiddleware.coffee | 11 + .../Security/AuthorizationManager.coffee | 38 + .../Features/Security/LoginRateLimiter.coffee | 24 + .../Spelling/SpellingController.coffee | 14 + .../Subscription/LimitationsManager.coffee | 62 + .../Features/Subscription/PlansLocator.coffee | 9 + .../Subscription/RecurlyWrapper.coffee | 181 + .../SubscriptionBackgroundTasks.coffee | 21 + .../SubscriptionController.coffee | 169 + .../SubscriptionFormatters.coffee | 15 + .../SubscriptionGroupController.coffee | 34 + .../SubscriptionGroupHandler.coffee | 45 + .../Subscription/SubscriptionHandler.coffee | 58 + .../Subscription/SubscriptionLocator.coffee | 20 + .../Subscription/SubscriptionRouter.coffee | 36 + .../Subscription/SubscriptionUpdater.coffee | 80 + .../SubscriptionViewModelBuilder.coffee | 63 + .../Subscription/UserFeaturesUpdater.coffee | 16 + .../Features/Tags/TagsController.coffee | 21 + .../coffee/Features/Tags/TagsHandler.coffee | 71 + .../Templates/TemplatesController.coffee | 44 + .../Templates/TemplatesMiddlewear.coffee | 7 + .../Templates/TemplatesPublisher.coffee | 21 + .../ThirdPartyDataStore/TpdsController.coffee | 49 + .../TpdsPollingBackgroundTasks.coffee | 35 + .../TpdsUpdateHandler.coffee | 46 + .../TpdsUpdateSender.coffee | 109 + .../ThirdPartyDataStore/UpdateMerger.coffee | 116 + .../Features/Uploads/ArchiveManager.coffee | 23 + .../Uploads/FileSystemImportManager.coffee | 65 + .../Features/Uploads/FileTypeManager.coffee | 49 + .../Uploads/ProjectUploadController.coffee | 49 + .../Uploads/ProjectUploadManager.coffee | 28 + .../Features/Uploads/UploadsRouter.coffee | 13 + .../Features/User/UserController.coffee | 35 + .../coffee/Features/User/UserCreator.coffee | 19 + .../coffee/Features/User/UserDeleter.coffee | 22 + .../coffee/Features/User/UserGetter.coffee | 15 + .../coffee/Features/User/UserLocator.coffee | 13 + .../User/UserRegistrationHandler.coffee | 30 + .../coffee/Features/User/UserUpdater.coffee | 12 + .../AutomaticSnapshotManager.coffee | 50 + .../Features/Versioning/RedisKeys.coffee | 5 + .../Versioning/VersioningApiController.coffee | 31 + .../Versioning/VersioningApiHandler.coffee | 75 + .../coffee/controllers/AdminController.coffee | 128 + .../coffee/controllers/HomeController.coffee | 53 + .../coffee/controllers/InfoController.coffee | 12 + .../controllers/ProjectController.coffee | 218 + .../coffee/controllers/UserController.coffee | 237 + services/web/app/coffee/errors.coffee | 10 + .../app/coffee/handlers/ProjectHandler.coffee | 164 + .../infrastructure/BackgroundTasks.coffee | 7 + .../infrastructure/CrawlerLogger.coffee | 11 + .../infrastructure/ExpressLocals.coffee | 122 + .../web/app/coffee/infrastructure/Keys.coffee | 5 + .../infrastructure/LoggerSerializers.coffee | 18 + .../app/coffee/infrastructure/Metrics.coffee | 24 + .../app/coffee/infrastructure/Monitor.coffee | 5 + .../coffee/infrastructure/Monitor/HTTP.coffee | 21 + .../infrastructure/Monitor/MongoDB.coffee | 57 + .../infrastructure/RandomLogging.coffee | 6 + .../app/coffee/infrastructure/Server.coffee | 102 + .../infrastructure/SocketIoConfig.coffee | 20 + .../app/coffee/infrastructure/mongojs.coffee | 6 + .../managers/CollaberationManager.coffee | 62 + .../app/coffee/managers/EmailManager.coffee | 45 + .../app/coffee/managers/GuidManager.coffee | 6 + .../coffee/managers/NewsletterManager.coffee | 37 + .../coffee/managers/SecurityManager.coffee | 174 + services/web/app/coffee/models/Doc.coffee | 15 + services/web/app/coffee/models/File.coffee | 14 + services/web/app/coffee/models/Folder.coffee | 20 + services/web/app/coffee/models/Project.coffee | 123 + services/web/app/coffee/models/Quote.coffee | 13 + .../web/app/coffee/models/Subscription.coffee | 33 + services/web/app/coffee/models/User.coffee | 84 + services/web/app/coffee/router.coffee | 359 + .../app/templates/email/emailTemplate.html | 408 + .../email/shared_project_email_template.html | 414 + .../web/app/templates/project_files/main.tex | 31 + .../app/templates/project_files/mainbasic.tex | 14 + .../templates/project_files/references.bib | 8 + .../app/templates/project_files/universe.jpg | Bin 0 -> 28172 bytes services/web/app/views/about/about.jade | 39 + services/web/app/views/about/attribution.jade | 31 + .../app/views/about/planned_maintenance.jade | 16 + services/web/app/views/about/privacy.jade | 26 + services/web/app/views/about/security.jade | 93 + services/web/app/views/about/tos.jade | 34 + services/web/app/views/admin.jade | 133 + services/web/app/views/changelog.jade | 138 + services/web/app/views/general/404.jade | 9 + services/web/app/views/general/closed.jade | 13 + .../web/app/views/general/genericMessage.jade | 9 + .../app/views/general/long-form-features.jade | 45 + .../views/general/partial/registerForm.jade | 13 + services/web/app/views/general/sidebar.jade | 24 + .../web/app/views/general/small-footer.jade | 32 + .../web/app/views/general/social-footer.jade | 30 + services/web/app/views/homepage/comments.jade | 46 + services/web/app/views/homepage/header.jade | 15 + services/web/app/views/homepage/home.jade | 70 + .../web/app/views/homepage/socialMedia.jade | 7 + services/web/app/views/info/advisor.jade | 69 + services/web/app/views/info/dropbox.jade | 94 + services/web/app/views/info/themes.jade | 92 + services/web/app/views/layout.jade | 106 + services/web/app/views/menubar.jade | 48 + services/web/app/views/modals.jade | 96 + services/web/app/views/project/editor.jade | 91 + services/web/app/views/project/flat.jade | 27 + services/web/app/views/project/list.jade | 152 + services/web/app/views/project/new.jade | 19 + .../app/views/project/partials/manage.jade | 77 + .../web/app/views/project/partials/pdf.jade | 38 + services/web/app/views/project/table.jade | 10 + services/web/app/views/referal/bonus.jade | 138 + .../web/app/views/referal/facebookLike.jade | 9 + .../app/views/referal/facebookWallPost.jade | 25 + .../web/app/views/referal/googleplus.jade | 10 + services/web/app/views/referal/tweet.jade | 2 + .../web/app/views/referal/tweetShare.jade | 2 + services/web/app/views/resources.jade | 59 + .../app/views/subscriptions/dashboard.jade | 83 + .../subscriptions/edit-billing-details.jade | 22 + .../app/views/subscriptions/group_admin.jade | 45 + services/web/app/views/subscriptions/new.jade | 19 + .../web/app/views/subscriptions/plans.jade | 157 + .../successful_subscription.jade | 33 + services/web/app/views/templates.jade | 505 + services/web/app/views/templates/dropbox.jade | 15 + services/web/app/views/tests.jade | 14 + services/web/app/views/user/feedback.jade | 14 + services/web/app/views/user/login.jade | 24 + .../web/app/views/user/passwordReset.jade | 17 + services/web/app/views/user/register.jade | 42 + services/web/app/views/user/restricted.jade | 10 + services/web/app/views/user/settings.jade | 176 + services/web/config/.gitignore | 0 .../web/config/settings.development.coffee | 204 + services/web/data/.gitignore | 4 + services/web/data/dumpFolder/.gitignore | 0 services/web/data/logs/.gitignore | 0 services/web/data/pdfs/.gitignore | 0 services/web/data/uploads/.gitignore | 0 services/web/data/zippedProjects/.gitignore | 0 services/web/package.json | 60 + services/web/public/app.build.js | 41 + services/web/public/backbone.js | 1429 + services/web/public/bootstrap/.gitignore | 33 + services/web/public/bootstrap/LICENSE | 13 + services/web/public/bootstrap/Makefile | 59 + services/web/public/bootstrap/README.md | 94 + .../docs/assets/css/bootstrap-responsive.css | 567 + .../bootstrap/docs/assets/css/bootstrap.css | 3396 ++ .../public/bootstrap/docs/assets/css/docs.css | 787 + .../assets/ico/bootstrap-apple-114x114.png | Bin 0 -> 5481 bytes .../docs/assets/ico/bootstrap-apple-57x57.png | Bin 0 -> 2798 bytes .../docs/assets/ico/bootstrap-apple-72x72.png | Bin 0 -> 3817 bytes .../bootstrap/docs/assets/ico/favicon.ico | Bin 0 -> 1150 bytes .../public/bootstrap/docs/assets/img/bird.png | Bin 0 -> 3092 bytes .../assets/img/bootstrap-mdo-sfmoma-01.jpg | Bin 0 -> 130647 bytes .../assets/img/bootstrap-mdo-sfmoma-02.jpg | Bin 0 -> 84505 bytes .../assets/img/bootstrap-mdo-sfmoma-03.jpg | Bin 0 -> 50755 bytes .../bootstrap/docs/assets/img/browsers.png | Bin 0 -> 19776 bytes .../docs/assets/img/example-diagram-01.png | Bin 0 -> 486 bytes .../docs/assets/img/example-diagram-02.png | Bin 0 -> 564 bytes .../docs/assets/img/example-diagram-03.png | Bin 0 -> 369 bytes .../docs/assets/img/example-sites/bartop.png | Bin 0 -> 78189 bytes .../docs/assets/img/example-sites/fleetio.png | Bin 0 -> 41932 bytes .../docs/assets/img/example-sites/jshint.png | Bin 0 -> 7258 bytes .../docs/assets/img/example-sites/kippt.png | Bin 0 -> 52306 bytes .../assets/img/example-sites/railwayjs.png | Bin 0 -> 30550 bytes .../img/example-sites/totalwireframe.png | Bin 0 -> 43364 bytes .../img/examples/bootstrap-example-fluid.jpg | Bin 0 -> 25832 bytes .../img/examples/bootstrap-example-hero.jpg | Bin 0 -> 22280 bytes .../examples/bootstrap-example-starter.jpg | Bin 0 -> 7182 bytes .../bootstrap/docs/assets/img/github-16px.png | Bin 0 -> 398 bytes .../assets/img/glyphicons-halflings-white.png | Bin 0 -> 4352 bytes .../docs/assets/img/glyphicons-halflings.png | Bin 0 -> 4352 bytes .../img/glyphicons/glyphicons_009_magic.png | Bin 0 -> 316 bytes .../img/glyphicons/glyphicons_042_group.png | Bin 0 -> 305 bytes .../img/glyphicons/glyphicons_079_podium.png | Bin 0 -> 213 bytes .../glyphicons/glyphicons_082_roundabout.png | Bin 0 -> 345 bytes .../glyphicons_155_show_thumbnails.png | Bin 0 -> 117 bytes .../img/glyphicons/glyphicons_163_iphone.png | Bin 0 -> 172 bytes .../glyphicons_214_resize_small.png | Bin 0 -> 301 bytes .../glyphicons/glyphicons_266_book_open.png | Bin 0 -> 292 bytes .../docs/assets/img/grid-18px-masked.png | Bin 0 -> 405 bytes .../bootstrap/docs/assets/img/icon-css3.png | Bin 0 -> 370 bytes .../bootstrap/docs/assets/img/icon-github.png | Bin 0 -> 312 bytes .../bootstrap/docs/assets/img/icon-html5.png | Bin 0 -> 452 bytes .../docs/assets/img/icon-twitter.png | Bin 0 -> 264 bytes .../docs/assets/img/less-logo-large.png | Bin 0 -> 13078 bytes .../bootstrap/docs/assets/img/less-small.png | Bin 0 -> 1181 bytes .../assets/img/responsive-illustrations.png | Bin 0 -> 1077 bytes .../public/bootstrap/docs/assets/js/README.md | 106 + .../bootstrap/docs/assets/js/application.js | 180 + .../docs/assets/js/bootstrap-alert.js | 91 + .../docs/assets/js/bootstrap-button.js | 98 + .../docs/assets/js/bootstrap-carousel.js | 154 + .../docs/assets/js/bootstrap-collapse.js | 136 + .../docs/assets/js/bootstrap-dropdown.js | 92 + .../docs/assets/js/bootstrap-modal.js | 210 + .../docs/assets/js/bootstrap-popover.js | 95 + .../docs/assets/js/bootstrap-scrollspy.js | 125 + .../bootstrap/docs/assets/js/bootstrap-tab.js | 130 + .../docs/assets/js/bootstrap-tooltip.js | 270 + .../docs/assets/js/bootstrap-transition.js | 51 + .../docs/assets/js/bootstrap-typeahead.js | 271 + .../js/google-code-prettify/prettify.css | 37 + .../js/google-code-prettify/prettify.js | 28 + .../public/bootstrap/docs/assets/js/jquery.js | 9252 ++++ .../web/public/bootstrap/docs/base-css.html | 1628 + .../web/public/bootstrap/docs/build/index.js | 33 + .../docs/build/node_modules/.bin/hulk | 93 + .../build/node_modules/hogan.js/.git_ignore | 1 + .../build/node_modules/hogan.js/.gitmodules | 3 + .../docs/build/node_modules/hogan.js/LICENSE | 177 + .../docs/build/node_modules/hogan.js/Makefile | 62 + .../build/node_modules/hogan.js/README.md | 93 + .../docs/build/node_modules/hogan.js/bin/hulk | 93 + .../node_modules/hogan.js/lib/compiler.js | 348 + .../build/node_modules/hogan.js/lib/hogan.js | 20 + .../node_modules/hogan.js/lib/template.js | 233 + .../build/node_modules/hogan.js/package.json | 20 + .../node_modules/hogan.js/test/html/list.html | 8 + .../node_modules/hogan.js/test/index.html | 13 + .../build/node_modules/hogan.js/test/index.js | 848 + .../node_modules/hogan.js/test/mustache.js | 90 + .../build/node_modules/hogan.js/test/spec.js | 77 + .../node_modules/hogan.js/test/spec/Changes | 31 + .../node_modules/hogan.js/test/spec/README.md | 65 + .../node_modules/hogan.js/test/spec/Rakefile | 27 + .../hogan.js/test/spec/TESTING.md | 46 + .../hogan.js/test/spec/specs/comments.json | 1 + .../hogan.js/test/spec/specs/comments.yml | 103 + .../hogan.js/test/spec/specs/delimiters.json | 1 + .../hogan.js/test/spec/specs/delimiters.yml | 158 + .../test/spec/specs/interpolation.json | 1 + .../test/spec/specs/interpolation.yml | 230 + .../hogan.js/test/spec/specs/inverted.json | 1 + .../hogan.js/test/spec/specs/inverted.yml | 193 + .../hogan.js/test/spec/specs/partials.json | 1 + .../hogan.js/test/spec/specs/partials.yml | 109 + .../hogan.js/test/spec/specs/sections.json | 1 + .../hogan.js/test/spec/specs/sections.yml | 256 + .../hogan.js/test/spec/specs/~lambdas.json | 1 + .../hogan.js/test/spec/specs/~lambdas.yml | 149 + .../hogan.js/test/templates/list.mustache | 8 + .../node_modules/hogan.js/tools/release.js | 74 + .../hogan.js/tools/web_templates.js | 32 + .../node_modules/hogan.js/web/1.0.0/hogan.js | 500 + .../hogan.js/web/1.0.0/hogan.min.js | 14 + .../hogan.js/web/builds/1.0.0/hogan.js | 500 + .../hogan.js/web/builds/1.0.0/hogan.min.js | 14 + .../hogan.js/web/builds/1.0.3/hogan.js | 545 + .../hogan.js/web/builds/1.0.3/hogan.min.js | 5 + .../web/builds/1.0.5/hogan-1.0.5.amd.js | 576 + .../web/builds/1.0.5/hogan-1.0.5.common.js | 576 + .../hogan.js/web/builds/1.0.5/hogan-1.0.5.js | 572 + .../web/builds/1.0.5/hogan-1.0.5.min.amd.js | 5 + .../builds/1.0.5/hogan-1.0.5.min.common.js | 5 + .../web/builds/1.0.5/hogan-1.0.5.min.js | 5 + .../builds/1.0.5/hogan-1.0.5.min.mustache.js | 5 + .../web/builds/1.0.5/hogan-1.0.5.mustache.js | 619 + .../web/builds/1.0.5/template-1.0.5.js | 233 + .../web/builds/1.0.5/template-1.0.5.min.js | 5 + .../node_modules/hogan.js/web/favicon.ico | Bin 0 -> 1150 bytes .../node_modules/hogan.js/web/images/logo.png | Bin 0 -> 3389 bytes .../hogan.js/web/images/noise.png | Bin 0 -> 10378 bytes .../hogan.js/web/images/small-hogan-icon.png | Bin 0 -> 472 bytes .../hogan.js/web/images/stripes.png | Bin 0 -> 115 bytes .../hogan.js/web/index.html.mustache | 139 + .../hogan.js/web/stylesheets/layout.css | 206 + .../hogan.js/web/stylesheets/skeleton.css | 236 + .../hogan.js/wrappers/amd.js.mustache | 21 + .../hogan.js/wrappers/common.js.mustache | 21 + .../hogan.js/wrappers/js.mustache | 17 + .../hogan.js/wrappers/mustache.js.mustache | 64 + .../public/bootstrap/docs/build/package.json | 6 + .../web/public/bootstrap/docs/components.html | 1528 + .../web/public/bootstrap/docs/download.html | 362 + .../web/public/bootstrap/docs/examples.html | 145 + .../public/bootstrap/docs/examples/fluid.html | 151 + .../public/bootstrap/docs/examples/hero.html | 108 + .../docs/examples/starter-template.html | 78 + services/web/public/bootstrap/docs/index.html | 248 + .../web/public/bootstrap/docs/javascript.html | 1476 + services/web/public/bootstrap/docs/less.html | 795 + .../public/bootstrap/docs/scaffolding.html | 442 + .../bootstrap/docs/templates/layout.mustache | 132 + .../docs/templates/pages/base-css.mustache | 1514 + .../docs/templates/pages/components.mustache | 1414 + .../docs/templates/pages/download.mustache | 248 + .../docs/templates/pages/examples.mustache | 31 + .../docs/templates/pages/index.mustache | 135 + .../docs/templates/pages/javascript.mustache | 1363 + .../docs/templates/pages/less.mustache | 681 + .../docs/templates/pages/scaffolding.mustache | 328 + .../docs/templates/pages/upgrading.mustache | 193 + .../web/public/bootstrap/docs/upgrading.html | 307 + .../img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../bootstrap/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes services/web/public/bootstrap/js/README.md | 106 + .../public/bootstrap/js/bootstrap-alert.js | 91 + .../public/bootstrap/js/bootstrap-button.js | 98 + .../public/bootstrap/js/bootstrap-carousel.js | 154 + .../public/bootstrap/js/bootstrap-collapse.js | 136 + .../public/bootstrap/js/bootstrap-dropdown.js | 92 + .../public/bootstrap/js/bootstrap-modal.js | 210 + .../public/bootstrap/js/bootstrap-popover.js | 95 + .../bootstrap/js/bootstrap-scrollspy.js | 125 + .../web/public/bootstrap/js/bootstrap-tab.js | 130 + .../public/bootstrap/js/bootstrap-tooltip.js | 270 + .../bootstrap/js/bootstrap-transition.js | 51 + .../bootstrap/js/bootstrap-typeahead.js | 271 + .../web/public/bootstrap/js/tests/index.html | 49 + .../js/tests/unit/bootstrap-alert.js | 41 + .../js/tests/unit/bootstrap-button.js | 54 + .../js/tests/unit/bootstrap-collapse.js | 25 + .../js/tests/unit/bootstrap-dropdown.js | 53 + .../js/tests/unit/bootstrap-modal.js | 85 + .../js/tests/unit/bootstrap-popover.js | 93 + .../js/tests/unit/bootstrap-scrollspy.js | 31 + .../bootstrap/js/tests/unit/bootstrap-tab.js | 45 + .../js/tests/unit/bootstrap-tooltip.js | 62 + .../js/tests/unit/bootstrap-transition.js | 13 + .../js/tests/unit/bootstrap-typeahead.js | 128 + .../bootstrap/js/tests/vendor/jquery.js | 9252 ++++ .../bootstrap/js/tests/vendor/qunit.css | 232 + .../public/bootstrap/js/tests/vendor/qunit.js | 1510 + .../web/public/bootstrap/less/accordion.less | 28 + .../web/public/bootstrap/less/alerts.less | 70 + .../web/public/bootstrap/less/bootstrap.less | 62 + .../public/bootstrap/less/breadcrumbs.less | 22 + .../public/bootstrap/less/button-groups.less | 147 + .../web/public/bootstrap/less/buttons.less | 167 + .../web/public/bootstrap/less/carousel.less | 121 + services/web/public/bootstrap/less/close.less | 18 + services/web/public/bootstrap/less/code.less | 56 + .../bootstrap/less/component-animations.less | 18 + .../web/public/bootstrap/less/dropdowns.less | 130 + services/web/public/bootstrap/less/forms.less | 492 + services/web/public/bootstrap/less/grid.less | 8 + .../web/public/bootstrap/less/hero-unit.less | 20 + .../public/bootstrap/less/labels-badges.less | 84 + .../web/public/bootstrap/less/labels.less | 32 + .../web/public/bootstrap/less/layouts.less | 17 + .../web/public/bootstrap/less/mixins.less | 577 + .../web/public/bootstrap/less/modals.less | 72 + .../web/public/bootstrap/less/navbar.less | 291 + services/web/public/bootstrap/less/navs.less | 344 + services/web/public/bootstrap/less/pager.less | 30 + .../web/public/bootstrap/less/pagination.less | 55 + .../web/public/bootstrap/less/popovers.less | 49 + .../public/bootstrap/less/progress-bars.less | 95 + services/web/public/bootstrap/less/reset.less | 126 + .../web/public/bootstrap/less/responsive.less | 326 + .../public/bootstrap/less/scaffolding.less | 29 + .../web/public/bootstrap/less/sprites.less | 158 + .../web/public/bootstrap/less/tables.less | 150 + .../web/public/bootstrap/less/thumbnails.less | 35 + .../web/public/bootstrap/less/tooltip.less | 35 + services/web/public/bootstrap/less/type.less | 218 + .../web/public/bootstrap/less/utilities.less | 23 + .../web/public/bootstrap/less/variables.less | 106 + services/web/public/bootstrap/less/wells.less | 17 + .../coffee/SubscriptionGroupsManager.coffee | 74 + services/web/public/coffee/ab_testing.coffee | 26 + .../coffee/account/AccountManager.coffee | 59 + services/web/public/coffee/admin.coffee | 33 + .../coffee/analytics/AnalyticsManager.coffee | 16 + .../auto-complete/AutoCompleteManager.coffee | 152 + .../coffee/auto-complete/MenuView.coffee | 57 + .../auto-complete/SuggestionManager.coffee | 104 + .../coffee/auto-complete/commands.coffee | 64 + .../coffee/cursors/CursorManager.coffee | 99 + .../coffee/editor/AceUpdateManager.coffee | 40 + .../web/public/coffee/editor/Document.coffee | 159 + .../web/public/coffee/editor/Editor.coffee | 355 + .../public/coffee/editor/ShareJSHeader.coffee | 3 + .../public/coffee/editor/ShareJsDoc.coffee | 116 + .../coffee/editor/sharejs/client/ace.coffee | 128 + .../coffee/editor/sharejs/client/ace.js | 119 + .../coffee/editor/sharejs/client/cm.coffee | 94 + .../editor/sharejs/client/connection.coffee | 167 + .../coffee/editor/sharejs/client/doc.coffee | 327 + .../coffee/editor/sharejs/client/doc.js | 332 + .../coffee/editor/sharejs/client/index.coffee | 73 + .../editor/sharejs/client/microevent.coffee | 46 + .../editor/sharejs/client/microevent.js | 85 + .../editor/sharejs/client/textarea.coffee | 69 + .../editor/sharejs/client/web-prelude.coffee | 13 + .../public/coffee/editor/sharejs/index.coffee | 5 + .../sharejs/server/browserchannel.coffee | 333 + .../editor/sharejs/server/db/couchdb.coffee | 149 + .../editor/sharejs/server/db/index.coffee | 28 + .../coffee/editor/sharejs/server/db/pg.coffee | 198 + .../editor/sharejs/server/db/redis.coffee | 140 + .../coffee/editor/sharejs/server/index.coffee | 57 + .../coffee/editor/sharejs/server/model.coffee | 603 + .../coffee/editor/sharejs/server/rest.coffee | 148 + .../editor/sharejs/server/socketio.coffee | 336 + .../editor/sharejs/server/syncqueue.coffee | 42 + .../editor/sharejs/server/useragent.coffee | 146 + .../coffee/editor/sharejs/types/README.md | 48 + .../coffee/editor/sharejs/types/count.coffee | 22 + .../editor/sharejs/types/helpers.coffee | 65 + .../coffee/editor/sharejs/types/index.coffee | 15 + .../editor/sharejs/types/json-api.coffee | 180 + .../coffee/editor/sharejs/types/json.coffee | 441 + .../coffee/editor/sharejs/types/simple.coffee | 38 + .../editor/sharejs/types/text-api.coffee | 32 + .../coffee/editor/sharejs/types/text-api.js | 58 + .../sharejs/types/text-composable-api.coffee | 43 + .../sharejs/types/text-composable.coffee | 261 + .../editor/sharejs/types/text-tp2-api.coffee | 89 + .../editor/sharejs/types/text-tp2.coffee | 322 + .../coffee/editor/sharejs/types/text.coffee | 209 + .../coffee/editor/sharejs/types/text.js | 239 + .../editor/sharejs/types/web-prelude.coffee | 11 + .../web/public/coffee/event_tracking.coffee | 15 + .../public/coffee/file-tree/DocView.coffee | 9 + .../public/coffee/file-tree/EntityView.coffee | 113 + .../coffee/file-tree/FileTreeManager.coffee | 274 + .../coffee/file-tree/FileTreeView.coffee | 22 + .../public/coffee/file-tree/FileView.coffee | 8 + .../public/coffee/file-tree/FolderView.coffee | 121 + .../coffee/file-tree/RootFolderView.coffee | 58 + .../public/coffee/file-view/FileView.coffee | 30 + .../coffee/file-view/FileViewManager.coffee | 17 + services/web/public/coffee/forms.coffee | 197 + services/web/public/coffee/gui.coffee | 16 + .../web/public/coffee/help/HelpManager.coffee | 13 + .../web/public/coffee/history/FileDiff.coffee | 14 + .../public/coffee/history/FileDiffView.coffee | 64 + .../coffee/history/HistoryManager.coffee | 64 + .../public/coffee/history/HistoryView.coffee | 39 + .../web/public/coffee/history/Version.coffee | 10 + .../coffee/history/VersionDetailView.coffee | 28 + .../public/coffee/history/VersionList.coffee | 48 + .../coffee/history/VersionListView.coffee | 106 + .../web/public/coffee/history/util.coffee | 28 + services/web/public/coffee/home.coffee | 8 + services/web/public/coffee/ide.coffee | 229 + .../coffee/ide/ConnectionManager.coffee | 78 + .../coffee/ide/FileUploadManager.coffee | 63 + .../public/coffee/ide/LayoutManager.coffee | 76 + .../public/coffee/ide/MainAreaManager.coffee | 69 + .../public/coffee/ide/SideBarManager.coffee | 38 + .../web/public/coffee/ide/TabManager.coffee | 92 + .../coffee/keys/BackspaceHighjack.coffee | 9 + .../public/coffee/keys/HotkeysManager.coffee | 30 + services/web/public/coffee/list.coffee | 220 + services/web/public/coffee/main.coffee | 9 + .../coffee/messages/MessageManager.coffee | 25 + services/web/public/coffee/models/Doc.coffee | 11 + services/web/public/coffee/models/File.coffee | 25 + .../web/public/coffee/models/Folder.coffee | 36 + .../coffee/models/FolderChildren.coffee | 29 + .../web/public/coffee/models/Project.coffee | 81 + .../coffee/models/ProjectMemberList.coffee | 14 + services/web/public/coffee/models/User.coffee | 21 + .../web/public/coffee/pdf/CompiledView.coffee | 216 + .../public/coffee/pdf/NativePdfView.coffee | 22 + .../web/public/coffee/pdf/PDFjsView.coffee | 123 + .../web/public/coffee/pdf/PdfManager.coffee | 190 + .../ProjectMembersManager.coffee | 302 + .../public/coffee/search/SearchManager.coffee | 18 + .../settings/DropboxSettingsManager.coffee | 52 + .../coffee/settings/SettingsManager.coffee | 201 + services/web/public/coffee/slides.coffee | 19 + .../spelling/HighlightedWordManager.coffee | 146 + .../coffee/spelling/SpellingManager.coffee | 159 + .../coffee/spelling/SpellingMenuView.coffee | 82 + services/web/public/coffee/tags.coffee | 112 + .../coffee/tests/unit/UndoManagerTests.coffee | 455 + .../auto-complete/SuggestionManager.coffee | 68 + .../tests/unit/editor/DocumentTests.coffee | 233 + .../tests/unit/editor/ShareJsDocTests.coffee | 251 + .../public/coffee/tests/unit/helpers.coffee | 10 + .../coffee/tests/unit/history/FileDiff.coffee | 140 + .../tests/unit/history/HistoryManager.coffee | 158 + .../tests/unit/history/HistoryView.coffee | 47 + .../tests/unit/history/VersionListView.coffee | 71 + .../web/public/coffee/tests/unit/modal.coffee | 56 + .../coffee/tests/unit/project-members.coffee | 275 + .../public/coffee/tests/unit/project.coffee | 164 + .../web/public/coffee/tests/unit/run.coffee | 21 + .../HighlightedWordManagerTests.coffee | 295 + .../unit/spelling/SpellingManagerTests.coffee | 218 + .../web/public/coffee/tests/unit/user.coffee | 24 + .../web/public/coffee/tour/IdeTour.coffee | 92 + .../web/public/coffee/undo/UndoManager.coffee | 358 + .../public/coffee/utils/ContextMenu.coffee | 56 + .../web/public/coffee/utils/Effects.coffee | 22 + services/web/public/coffee/utils/Modal.coffee | 66 + services/web/public/favicon.ico | Bin 0 -> 1150 bytes services/web/public/font/PTSans-webfont.eot | Bin 0 -> 26928 bytes services/web/public/font/PTSans-webfont.svg | 153 + services/web/public/font/PTSans-webfont.ttf | Bin 0 -> 26744 bytes services/web/public/font/PTSans-webfont.woff | Bin 0 -> 17592 bytes services/web/public/font/cmunrb.otf | Bin 0 -> 226856 bytes services/web/public/font/cmunrm.otf | Bin 0 -> 330492 bytes services/web/public/font/cmuntt.otf | Bin 0 -> 301664 bytes .../web/public/font/fontawesome-webfont.eot | Bin 0 -> 38708 bytes .../web/public/font/fontawesome-webfont.svg | 255 + .../web/public/font/fontawesome-webfont.ttf | Bin 0 -> 68476 bytes .../web/public/font/fontawesome-webfont.woff | Bin 0 -> 41752 bytes services/web/public/humans.txt | 12 + .../public/img/about/coffeescript_logo.png | Bin 0 -> 5511 bytes .../web/public/img/about/henry_oswald.jpg | Bin 0 -> 13314 bytes services/web/public/img/about/james_allen.jpg | Bin 0 -> 9753 bytes services/web/public/img/about/nodejs_logo.jpg | Bin 0 -> 8275 bytes services/web/public/img/about/redis_logo.png | Bin 0 -> 22967 bytes .../web/public/img/about/scribtex_logo.jpeg | Bin 0 -> 7712 bytes services/web/public/img/add.png | Bin 0 -> 170 bytes services/web/public/img/aqua.jpg | Bin 0 -> 378536 bytes services/web/public/img/arrow-up.png | Bin 0 -> 486 bytes services/web/public/img/arrow.png | Bin 0 -> 4033 bytes services/web/public/img/arrow1.png | Bin 0 -> 1896 bytes services/web/public/img/backward.png | Bin 0 -> 355 bytes services/web/public/img/book.png | Bin 0 -> 593 bytes services/web/public/img/clock.png | Bin 0 -> 3607 bytes services/web/public/img/create-folder.png | Bin 0 -> 259 bytes services/web/public/img/created.png | Bin 0 -> 733 bytes services/web/public/img/crests/cambridge.png | Bin 0 -> 25972 bytes services/web/public/img/crests/durham.png | Bin 0 -> 4995 bytes services/web/public/img/crests/harvard.gif | Bin 0 -> 3314 bytes services/web/public/img/crests/icl.png | Bin 0 -> 3854 bytes services/web/public/img/crests/liverpool.jpg | Bin 0 -> 13722 bytes services/web/public/img/crests/mit.gif | Bin 0 -> 593 bytes services/web/public/img/crests/nasa.png | Bin 0 -> 14703 bytes services/web/public/img/crests/oxford.gif | Bin 0 -> 8502 bytes services/web/public/img/crests/stanford.png | Bin 0 -> 8739 bytes services/web/public/img/crests/tokyo.png | Bin 0 -> 24557 bytes services/web/public/img/crests/toronto.gif | Bin 0 -> 13214 bytes services/web/public/img/crests/yale.png | Bin 0 -> 21581 bytes services/web/public/img/deleted.png | Bin 0 -> 715 bytes services/web/public/img/doc.png | Bin 0 -> 3163 bytes services/web/public/img/documentation.png | Bin 0 -> 3230 bytes services/web/public/img/down-arrow.png | Bin 0 -> 232 bytes .../img/dropbox/document_updated_modal.png | Bin 0 -> 22840 bytes .../web/public/img/dropbox/dropbox_banner.png | Bin 0 -> 224666 bytes .../img/dropbox/dropbox_banner_tall.png | Bin 0 -> 77493 bytes .../web/public/img/dropbox/dropbox_logo.png | Bin 0 -> 144022 bytes .../img/dropbox/dropbox_progress_bar.png | Bin 0 -> 7380 bytes .../web/public/img/dropbox/history_diff.png | Bin 0 -> 51132 bytes .../img/dropbox/share_dropbox_folder.png | Bin 0 -> 35961 bytes services/web/public/img/faileupload.png | Bin 0 -> 22410 bytes services/web/public/img/favicon.ico | Bin 0 -> 1150 bytes services/web/public/img/favicon.png | Bin 0 -> 577 bytes services/web/public/img/file.png | Bin 0 -> 757 bytes services/web/public/img/fit-to-height.png | Bin 0 -> 1611 bytes services/web/public/img/fit-to-width.png | Bin 0 -> 1692 bytes services/web/public/img/flatview.png | Bin 0 -> 208 bytes services/web/public/img/folder-large.png | Bin 0 -> 10114 bytes services/web/public/img/folder-open.png | Bin 0 -> 625 bytes services/web/public/img/folder.png | Bin 0 -> 3291 bytes services/web/public/img/forward.png | Bin 0 -> 328 bytes services/web/public/img/galaxy.jpg | Bin 0 -> 168045 bytes .../public/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../web/public/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes services/web/public/img/icons/16/code.png | Bin 0 -> 428 bytes .../web/public/img/icons/16/collaborators.png | Bin 0 -> 343 bytes services/web/public/img/icons/16/history.png | Bin 0 -> 486 bytes services/web/public/img/icons/16/projects.png | Bin 0 -> 363 bytes services/web/public/img/icons/16/settings.png | Bin 0 -> 415 bytes .../web/public/img/icons/16/subscription.png | Bin 0 -> 363 bytes services/web/public/img/icons/24/code.png | Bin 0 -> 610 bytes .../web/public/img/icons/24/collaborators.png | Bin 0 -> 462 bytes services/web/public/img/icons/24/history.png | Bin 0 -> 721 bytes services/web/public/img/icons/24/settings.png | Bin 0 -> 601 bytes services/web/public/img/keyboard.png | Bin 0 -> 570 bytes services/web/public/img/list/45.png | Bin 0 -> 155 bytes services/web/public/img/list/48.png | Bin 0 -> 303 bytes services/web/public/img/list/50.png | Bin 0 -> 218 bytes services/web/public/img/list/56.png | Bin 0 -> 326 bytes services/web/public/img/loading.gif | Bin 0 -> 1688 bytes services/web/public/img/log.png | Bin 0 -> 664 bytes services/web/public/img/logo/banner-plain.png | Bin 0 -> 15584 bytes services/web/public/img/logo/banner.png | Bin 0 -> 16380 bytes services/web/public/img/logo/facebook-32.png | Bin 0 -> 288 bytes services/web/public/img/logo/icon_128.png | Bin 0 -> 5509 bytes services/web/public/img/logo/link-32.png | Bin 0 -> 2251 bytes services/web/public/img/logo/lion-128.png | Bin 0 -> 10601 bytes services/web/public/img/logo/lion-32.png | Bin 0 -> 3618 bytes services/web/public/img/logo/lion-64.png | Bin 0 -> 5532 bytes services/web/public/img/logo/logo.gif | Bin 0 -> 4233 bytes services/web/public/img/logo/logosmall.png | Bin 0 -> 8542 bytes services/web/public/img/logo/mail-32.png | Bin 0 -> 1426 bytes services/web/public/img/logo/main.acorn | Bin 0 -> 24576 bytes services/web/public/img/logo/twitter-32.png | Bin 0 -> 914 bytes services/web/public/img/moved.png | Bin 0 -> 494 bytes services/web/public/img/nide.png | Bin 0 -> 14723 bytes services/web/public/img/noise.png | Bin 0 -> 4461 bytes services/web/public/img/pdf.png | Bin 0 -> 668 bytes services/web/public/img/project.png | Bin 0 -> 756 bytes services/web/public/img/project1.png | Bin 0 -> 3449 bytes services/web/public/img/remove.png | Bin 0 -> 149 bytes services/web/public/img/rename.png | Bin 0 -> 1818 bytes services/web/public/img/resizer.png | Bin 0 -> 189 bytes services/web/public/img/right-arrow.png | Bin 0 -> 236 bytes services/web/public/img/sad.png | Bin 0 -> 814 bytes services/web/public/img/save.png | Bin 0 -> 1439 bytes services/web/public/img/screen_CS3.png | Bin 0 -> 54111 bytes services/web/public/img/screenshot.png | Bin 0 -> 424355 bytes .../web/public/img/screenshots/editorCode.png | Bin 0 -> 111532 bytes .../img/screenshots/editorCollaborating.png | Bin 0 -> 139307 bytes .../web/public/img/screenshots/editorLogs.png | Bin 0 -> 85839 bytes .../web/public/img/screenshots/editorPDF.png | Bin 0 -> 98334 bytes .../public/img/screenshots/editorThemes.png | Bin 0 -> 114595 bytes .../img/screenshots/origionals/editorCode.jpg | Bin 0 -> 73976 bytes .../img/screenshots/origionals/editorCode.png | Bin 0 -> 246685 bytes .../img/screenshots/origionals/editorLogs.jpg | Bin 0 -> 54841 bytes .../img/screenshots/origionals/editorPdf.jpg | Bin 0 -> 67149 bytes .../screenshots/precompressed/editorCode.png | Bin 0 -> 104590 bytes .../screenshots/precompressed/editorLogs.png | Bin 0 -> 95233 bytes .../screenshots/precompressed/editorPDF.png | Bin 0 -> 86553 bytes .../precompressed/editorThemes.png | Bin 0 -> 51509 bytes services/web/public/img/search.png | Bin 0 -> 3446 bytes services/web/public/img/settings.png | Bin 0 -> 2079 bytes .../web/public/img/share/facebook_button.png | Bin 0 -> 3218 bytes services/web/public/img/share/mail.png | Bin 0 -> 852 bytes .../web/public/img/share/twitter_button.png | Bin 0 -> 2900 bytes services/web/public/img/sort-asc.gif | Bin 0 -> 54 bytes services/web/public/img/sort-bg.gif | Bin 0 -> 64 bytes services/web/public/img/sort-desc.gif | Bin 0 -> 54 bytes .../web/public/img/spellcheck-underline.png | Bin 0 -> 199 bytes services/web/public/img/spin.gif | Bin 0 -> 29866 bytes services/web/public/img/spin1.gif | Bin 0 -> 3208 bytes services/web/public/img/splitview.png | Bin 0 -> 219 bytes .../web/public/img/textures/concrete_wall.png | Bin 0 -> 177749 bytes services/web/public/img/textures/polaroid.png | Bin 0 -> 5210 bytes .../web/public/img/themes/origonal/Chrome.jpg | Bin 0 -> 86797 bytes .../public/img/themes/origonal/Monokai.jpg | Bin 0 -> 75805 bytes .../web/public/img/themes/origonal/clouds.jpg | Bin 0 -> 76744 bytes .../img/themes/origonal/clouds_midnight.jpg | Bin 0 -> 67963 bytes .../web/public/img/themes/origonal/cobalt.jpg | Bin 0 -> 85475 bytes .../img/themes/origonal/crimson_editor.jpg | Bin 0 -> 83142 bytes .../web/public/img/themes/origonal/dawn.jpg | Bin 0 -> 79661 bytes .../img/themes/origonal/dreamweaver.jpg | Bin 0 -> 86599 bytes .../public/img/themes/origonal/eclipse.jpg | Bin 0 -> 83374 bytes .../img/themes/origonal/ide_fingers.jpg | Bin 0 -> 76632 bytes .../public/img/themes/origonal/kr_theme.jpg | Bin 0 -> 81006 bytes .../public/img/themes/origonal/merbivore.jpg | Bin 0 -> 81616 bytes .../img/themes/origonal/merbivore_soft.jpg | Bin 0 -> 78895 bytes .../img/themes/origonal/mono_industrial.jpg | Bin 0 -> 77769 bytes .../img/themes/origonal/pastles_on_dark.jpg | Bin 0 -> 65839 bytes .../img/themes/origonal/solarized_dark.jpg | Bin 0 -> 68985 bytes .../img/themes/origonal/solarized_light.jpg | Bin 0 -> 73067 bytes .../public/img/themes/origonal/tomorrow.jpg | Bin 0 -> 74825 bytes .../img/themes/origonal/tomorrow_night.jpg | Bin 0 -> 75554 bytes .../themes/origonal/tomorrow_night_blue.jpg | Bin 0 -> 85161 bytes .../themes/origonal/tomorrow_night_bright.jpg | Bin 0 -> 83483 bytes .../origonal/tomorrow_night_eightys.jpg | Bin 0 -> 75031 bytes .../public/img/themes/origonal/twilight.jpg | Bin 0 -> 79823 bytes .../img/themes/origonal/vibrant_ink.jpg | Bin 0 -> 85879 bytes services/web/public/img/txture.png | Bin 0 -> 149 bytes services/web/public/img/upload-file.png | Bin 0 -> 1366 bytes .../web/public/img/upwards-sweeping-arrow.png | Bin 0 -> 986 bytes services/web/public/img/user.png | Bin 0 -> 525 bytes services/web/public/img/users/3.png | Bin 0 -> 345 bytes services/web/public/img/users/numbers.png | Bin 0 -> 1422 bytes services/web/public/img/users/numbers/1.png | Bin 0 -> 975 bytes services/web/public/img/users/numbers/2.png | Bin 0 -> 308 bytes services/web/public/img/users/user1.png | Bin 0 -> 641 bytes services/web/public/img/users/user2.png | Bin 0 -> 848 bytes services/web/public/img/washi.png | Bin 0 -> 5913 bytes services/web/public/img/zoom-in.png | Bin 0 -> 2246 bytes services/web/public/img/zoom-out.png | Bin 0 -> 2168 bytes services/web/public/js/ace/ace.js | 117 + services/web/public/js/ace/anchor.js | 242 + services/web/public/js/ace/anchor_test.js | 188 + services/web/public/js/ace/autocomplete.js | 351 + .../web/public/js/ace/autocomplete/popup.js | 332 + .../js/ace/autocomplete/text_completer.js | 78 + .../web/public/js/ace/autocomplete/util.js | 74 + .../web/public/js/ace/background_tokenizer.js | 255 + .../js/ace/background_tokenizer_test.js | 85 + .../public/js/ace/commands/command_manager.js | 112 + .../js/ace/commands/command_manager_test.js | 199 + .../js/ace/commands/default_commands.js | 542 + .../commands/incremental_search_commands.js | 180 + .../js/ace/commands/multi_select_commands.js | 97 + .../public/js/ace/commands/occur_commands.js | 110 + services/web/public/js/ace/config.js | 295 + services/web/public/js/ace/config_test.js | 128 + .../css/codefolding-fold-button-states.png | Bin 0 -> 759 bytes services/web/public/js/ace/css/editor.css | 453 + .../web/public/js/ace/css/expand-marker.png | Bin 0 -> 290 bytes services/web/public/js/ace/document.js | 642 + services/web/public/js/ace/document_test.js | 306 + services/web/public/js/ace/edit_session.js | 2541 + .../js/ace/edit_session/bracket_match.js | 219 + .../web/public/js/ace/edit_session/fold.js | 140 + .../public/js/ace/edit_session/fold_line.js | 268 + .../web/public/js/ace/edit_session/folding.js | 846 + .../web/public/js/ace/edit_session_test.js | 1075 + services/web/public/js/ace/editor.js | 2444 + .../js/ace/editor_change_document_test.js | 188 + .../editor_highlight_selected_word_test.js | 223 + .../public/js/ace/editor_navigation_test.js | 164 + .../public/js/ace/editor_text_edit_test.js | 557 + services/web/public/js/ace/ext/chromevox.js | 980 + .../js/ace/ext/elastic_tabstops_lite.js | 319 + services/web/public/js/ace/ext/emmet.js | 415 + .../web/public/js/ace/ext/keybinding_menu.js | 86 + .../web/public/js/ace/ext/language_tools.js | 129 + .../ext/menu_tools/add_editor_menu_options.js | 103 + .../ace/ext/menu_tools/element_generator.js | 148 + .../ext/menu_tools/generate_settings_menu.js | 258 + .../get_editor_keyboard_shortcuts.js | 100 + .../ace/ext/menu_tools/get_set_functions.js | 141 + .../js/ace/ext/menu_tools/overlay_page.js | 116 + .../js/ace/ext/menu_tools/settings_menu.css | 48 + services/web/public/js/ace/ext/modelist.js | 175 + services/web/public/js/ace/ext/old_ie.js | 108 + services/web/public/js/ace/ext/old_ie_test.js | 77 + services/web/public/js/ace/ext/searchbox.css | 158 + services/web/public/js/ace/ext/searchbox.js | 286 + .../web/public/js/ace/ext/settings_menu.js | 76 + services/web/public/js/ace/ext/spellcheck.js | 69 + services/web/public/js/ace/ext/split.js | 40 + services/web/public/js/ace/ext/static.css | 32 + .../web/public/js/ace/ext/static_highlight.js | 186 + .../js/ace/ext/static_highlight_test.js | 96 + services/web/public/js/ace/ext/statusbar.js | 49 + services/web/public/js/ace/ext/textarea.js | 547 + services/web/public/js/ace/ext/themelist.js | 101 + services/web/public/js/ace/ext/whitespace.js | 212 + .../web/public/js/ace/incremental_search.js | 259 + .../public/js/ace/incremental_search_test.js | 208 + services/web/public/js/ace/keyboard/emacs.js | 599 + .../web/public/js/ace/keyboard/emacs_test.js | 73 + .../public/js/ace/keyboard/hash_handler.js | 198 + .../web/public/js/ace/keyboard/keybinding.js | 136 + .../public/js/ace/keyboard/keybinding_test.js | 69 + .../public/js/ace/keyboard/state_handler.js | 249 + .../web/public/js/ace/keyboard/textinput.js | 505 + services/web/public/js/ace/keyboard/vim.js | 195 + .../public/js/ace/keyboard/vim/commands.js | 613 + .../js/ace/keyboard/vim/maps/aliases.js | 94 + .../js/ace/keyboard/vim/maps/motions.js | 664 + .../js/ace/keyboard/vim/maps/operators.js | 195 + .../public/js/ace/keyboard/vim/maps/util.js | 134 + .../public/js/ace/keyboard/vim/registers.js | 42 + services/web/public/js/ace/layer/cursor.js | 217 + services/web/public/js/ace/layer/gutter.js | 272 + services/web/public/js/ace/layer/marker.js | 218 + services/web/public/js/ace/layer/text.js | 667 + services/web/public/js/ace/layer/text_test.js | 126 + services/web/public/js/ace/lib/dom.js | 283 + services/web/public/js/ace/lib/es5-shim.js | 1062 + services/web/public/js/ace/lib/event.js | 355 + .../web/public/js/ace/lib/event_emitter.js | 155 + .../public/js/ace/lib/event_emitter_test.js | 65 + .../web/public/js/ace/lib/fixoldbrowsers.js | 19 + services/web/public/js/ace/lib/keys.js | 138 + services/web/public/js/ace/lib/lang.js | 218 + services/web/public/js/ace/lib/net.js | 41 + services/web/public/js/ace/lib/oop.js | 57 + services/web/public/js/ace/lib/regexp.js | 113 + services/web/public/js/ace/lib/useragent.js | 103 + services/web/public/js/ace/line_widgets.js | 293 + .../web/public/js/ace/mode/_test/Readme.md | 9 + .../js/ace/mode/_test/highlight_rules_test.js | 159 + .../web/public/js/ace/mode/_test/package.json | 8 + .../js/ace/mode/_test/text_asciidoc.txt | 111 + .../public/js/ace/mode/_test/text_coffee.txt | 56 + .../public/js/ace/mode/_test/text_curly.txt | 9 + .../public/js/ace/mode/_test/text_html.txt | 8 + .../js/ace/mode/_test/text_javascript.txt | 86 + .../js/ace/mode/_test/text_livescript.txt | 1 + .../public/js/ace/mode/_test/text_lucene.txt | 16 + .../js/ace/mode/_test/text_markdown.txt | 22 + .../public/js/ace/mode/_test/text_ruby.txt | 34 + .../web/public/js/ace/mode/_test/text_xml.txt | 7 + .../public/js/ace/mode/_test/tokens_abap.json | 189 + .../ace/mode/_test/tokens_actionscript.json | 263 + .../js/ace/mode/_test/tokens_asciidoc.json | 422 + .../ace/mode/_test/tokens_assembly_x86.json | 114 + .../js/ace/mode/_test/tokens_autohotkey.json | 261 + .../js/ace/mode/_test/tokens_batchfile.json | 70 + .../js/ace/mode/_test/tokens_c9search.json | 131 + .../js/ace/mode/_test/tokens_c_cpp.json | 185 + .../js/ace/mode/_test/tokens_clojure.json | 162 + .../js/ace/mode/_test/tokens_coffee.json | 528 + .../js/ace/mode/_test/tokens_coldfusion.json | 26 + .../js/ace/mode/_test/tokens_csharp.json | 31 + .../public/js/ace/mode/_test/tokens_css.json | 148 + .../js/ace/mode/_test/tokens_curly.json | 56 + .../public/js/ace/mode/_test/tokens_dart.json | 368 + .../public/js/ace/mode/_test/tokens_diff.json | 398 + .../public/js/ace/mode/_test/tokens_dot.json | 2254 + .../js/ace/mode/_test/tokens_erlang.json | 166 + .../js/ace/mode/_test/tokens_forth.json | 219 + .../public/js/ace/mode/_test/tokens_ftl.json | 341 + .../public/js/ace/mode/_test/tokens_glsl.json | 127 + .../js/ace/mode/_test/tokens_golang.json | 256 + .../js/ace/mode/_test/tokens_groovy.json | 410 + .../public/js/ace/mode/_test/tokens_haml.json | 174 + .../js/ace/mode/_test/tokens_haskell.json | 156 + .../public/js/ace/mode/_test/tokens_haxe.json | 143 + .../public/js/ace/mode/_test/tokens_html.json | 51 + .../js/ace/mode/_test/tokens_html_ruby.json | 247 + .../public/js/ace/mode/_test/tokens_jade.json | 188 + .../public/js/ace/mode/_test/tokens_java.json | 95 + .../js/ace/mode/_test/tokens_javascript.json | 592 + .../public/js/ace/mode/_test/tokens_json.json | 412 + .../public/js/ace/mode/_test/tokens_jsp.json | 435 + .../public/js/ace/mode/_test/tokens_jsx.json | 51 + .../js/ace/mode/_test/tokens_julia.json | 105 + .../js/ace/mode/_test/tokens_latex.json | 127 + .../public/js/ace/mode/_test/tokens_less.json | 204 + .../js/ace/mode/_test/tokens_liquid.json | 551 + .../public/js/ace/mode/_test/tokens_lisp.json | 248 + .../js/ace/mode/_test/tokens_livescript.json | 6 + .../js/ace/mode/_test/tokens_logiql.json | 190 + .../public/js/ace/mode/_test/tokens_lsl.json | 495 + .../public/js/ace/mode/_test/tokens_lua.json | 348 + .../js/ace/mode/_test/tokens_luapage.json | 633 + .../js/ace/mode/_test/tokens_lucene.json | 92 + .../js/ace/mode/_test/tokens_markdown.json | 114 + .../js/ace/mode/_test/tokens_mushcode.json | 790 + .../js/ace/mode/_test/tokens_objectivec.json | 792 + .../js/ace/mode/_test/tokens_ocaml.json | 200 + .../js/ace/mode/_test/tokens_pascal.json | 297 + .../public/js/ace/mode/_test/tokens_perl.json | 227 + .../js/ace/mode/_test/tokens_pgsql.json | 735 + .../public/js/ace/mode/_test/tokens_php.json | 134 + .../js/ace/mode/_test/tokens_powershell.json | 184 + .../js/ace/mode/_test/tokens_prolog.json | 265 + .../js/ace/mode/_test/tokens_properties.json | 68 + .../js/ace/mode/_test/tokens_python.json | 152 + .../public/js/ace/mode/_test/tokens_r.json | 235 + .../public/js/ace/mode/_test/tokens_rdoc.json | 441 + .../js/ace/mode/_test/tokens_rhtml.json | 106 + .../public/js/ace/mode/_test/tokens_ruby.json | 232 + .../public/js/ace/mode/_test/tokens_rust.json | 136 + .../public/js/ace/mode/_test/tokens_sass.json | 229 + .../public/js/ace/mode/_test/tokens_scad.json | 194 + .../js/ace/mode/_test/tokens_scala.json | 542 + .../js/ace/mode/_test/tokens_scheme.json | 216 + .../public/js/ace/mode/_test/tokens_scss.json | 123 + .../public/js/ace/mode/_test/tokens_sh.json | 334 + .../js/ace/mode/_test/tokens_snippets.json | 159 + .../public/js/ace/mode/_test/tokens_sql.json | 54 + .../js/ace/mode/_test/tokens_stylus.json | 271 + .../public/js/ace/mode/_test/tokens_svg.json | 684 + .../public/js/ace/mode/_test/tokens_tcl.json | 385 + .../public/js/ace/mode/_test/tokens_tex.json | 130 + .../public/js/ace/mode/_test/tokens_text.json | 29 + .../js/ace/mode/_test/tokens_textile.json | 113 + .../public/js/ace/mode/_test/tokens_toml.json | 131 + .../public/js/ace/mode/_test/tokens_twig.json | 288 + .../js/ace/mode/_test/tokens_typescript.json | 559 + .../js/ace/mode/_test/tokens_vbscript.json | 249 + .../js/ace/mode/_test/tokens_velocity.json | 281 + .../public/js/ace/mode/_test/tokens_xml.json | 43 + .../js/ace/mode/_test/tokens_xquery.json | 44 + .../public/js/ace/mode/_test/tokens_yaml.json | 150 + services/web/public/js/ace/mode/abap.js | 77 + .../js/ace/mode/abap_highlight_rules.js | 134 + .../web/public/js/ace/mode/actionscript.js | 62 + .../ace/mode/actionscript_highlight_rules.js | 141 + services/web/public/js/ace/mode/ada.js | 54 + .../public/js/ace/mode/ada_highlight_rules.js | 93 + .../web/public/js/ace/mode/apache_conf.js | 62 + .../ace/mode/apache_conf_highlight_rules.js | 231 + services/web/public/js/ace/mode/asciidoc.js | 64 + .../js/ace/mode/asciidoc_highlight_rules.js | 234 + .../web/public/js/ace/mode/assembly_x86.js | 56 + .../ace/mode/assembly_x86_highlight_rules.js | 114 + services/web/public/js/ace/mode/autohotkey.js | 62 + .../js/ace/mode/autohotkey_highlight_rules.js | 107 + services/web/public/js/ace/mode/batchfile.js | 61 + .../js/ace/mode/batchfile_highlight_rules.js | 97 + services/web/public/js/ace/mode/behaviour.js | 90 + .../web/public/js/ace/mode/behaviour/css.js | 108 + .../public/js/ace/mode/behaviour/cstyle.js | 365 + .../web/public/js/ace/mode/behaviour/html.js | 88 + .../web/public/js/ace/mode/behaviour/xml.js | 103 + .../public/js/ace/mode/behaviour/xquery.js | 92 + services/web/public/js/ace/mode/c9search.js | 67 + .../js/ace/mode/c9search_highlight_rules.js | 173 + services/web/public/js/ace/mode/c_cpp.js | 101 + .../js/ace/mode/c_cpp_highlight_rules.js | 183 + services/web/public/js/ace/mode/clojure.js | 86 + .../js/ace/mode/clojure_highlight_rules.js | 200 + services/web/public/js/ace/mode/cobol.js | 53 + .../js/ace/mode/cobol_highlight_rules.js | 100 + services/web/public/js/ace/mode/coffee.js | 114 + .../js/ace/mode/coffee/coffee-script.js | 62 + .../web/public/js/ace/mode/coffee/helpers.js | 271 + .../web/public/js/ace/mode/coffee/lexer.js | 929 + .../web/public/js/ace/mode/coffee/nodes.js | 3080 ++ .../web/public/js/ace/mode/coffee/parser.js | 724 + .../public/js/ace/mode/coffee/parser_test.js | 88 + .../web/public/js/ace/mode/coffee/rewriter.js | 513 + .../web/public/js/ace/mode/coffee/scope.js | 174 + .../js/ace/mode/coffee_highlight_rules.js | 233 + .../web/public/js/ace/mode/coffee_worker.js | 74 + services/web/public/js/ace/mode/coldfusion.js | 62 + .../js/ace/mode/coldfusion_highlight_rules.js | 49 + .../web/public/js/ace/mode/coldfusion_test.js | 67 + services/web/public/js/ace/mode/csharp.js | 61 + .../js/ace/mode/csharp_highlight_rules.js | 96 + services/web/public/js/ace/mode/css.js | 100 + .../web/public/js/ace/mode/css/csslint.js | 9206 ++++ .../public/js/ace/mode/css_highlight_rules.js | 179 + services/web/public/js/ace/mode/css_test.js | 78 + services/web/public/js/ace/mode/css_worker.js | 95 + .../web/public/js/ace/mode/css_worker_test.js | 68 + services/web/public/js/ace/mode/curly.js | 63 + .../js/ace/mode/curly_highlight_rules.js | 66 + services/web/public/js/ace/mode/d.js | 56 + .../public/js/ace/mode/d_highlight_rules.js | 318 + services/web/public/js/ace/mode/dart.js | 62 + .../js/ace/mode/dart_highlight_rules.js | 182 + services/web/public/js/ace/mode/diff.js | 52 + .../js/ace/mode/diff_highlight_rules.js | 108 + services/web/public/js/ace/mode/django.js | 116 + .../ace/mode/doc_comment_highlight_rules.js | 73 + services/web/public/js/ace/mode/dot.js | 55 + .../public/js/ace/mode/dot_highlight_rules.js | 126 + services/web/public/js/ace/mode/ejs.js | 109 + services/web/public/js/ace/mode/erlang.js | 57 + .../js/ace/mode/erlang_highlight_rules.js | 876 + .../public/js/ace/mode/folding/asciidoc.js | 142 + .../public/js/ace/mode/folding/c9search.js | 77 + .../web/public/js/ace/mode/folding/coffee.js | 120 + .../public/js/ace/mode/folding/coffee_test.js | 101 + .../web/public/js/ace/mode/folding/csharp.js | 138 + .../web/public/js/ace/mode/folding/cstyle.js | 124 + .../public/js/ace/mode/folding/cstyle_test.js | 109 + .../web/public/js/ace/mode/folding/diff.js | 69 + .../public/js/ace/mode/folding/fold_mode.js | 120 + .../web/public/js/ace/mode/folding/html.js | 79 + .../public/js/ace/mode/folding/html_test.js | 162 + .../web/public/js/ace/mode/folding/ini.js | 80 + .../web/public/js/ace/mode/folding/latex.js | 162 + .../web/public/js/ace/mode/folding/lua.js | 163 + .../public/js/ace/mode/folding/markdown.js | 125 + .../web/public/js/ace/mode/folding/mixed.js | 83 + .../public/js/ace/mode/folding/pythonic.js | 58 + .../js/ace/mode/folding/pythonic_test.js | 98 + .../public/js/ace/mode/folding/velocity.js | 120 + .../web/public/js/ace/mode/folding/xml.js | 254 + .../public/js/ace/mode/folding/xml_test.js | 110 + services/web/public/js/ace/mode/forth.js | 62 + .../js/ace/mode/forth_highlight_rules.js | 164 + services/web/public/js/ace/mode/ftl.js | 49 + .../public/js/ace/mode/ftl_highlight_rules.js | 195 + services/web/public/js/ace/mode/glsl.js | 53 + .../js/ace/mode/glsl_highlight_rules.js | 81 + services/web/public/js/ace/mode/golang.js | 55 + .../js/ace/mode/golang_highlight_rules.js | 111 + services/web/public/js/ace/mode/groovy.js | 24 + .../js/ace/mode/groovy_highlight_rules.js | 173 + services/web/public/js/ace/mode/haml.js | 61 + .../js/ace/mode/haml_highlight_rules.js | 132 + services/web/public/js/ace/mode/handlebars.js | 29 + .../js/ace/mode/handlebars_highlight_rules.js | 72 + services/web/public/js/ace/mode/haskell.js | 62 + .../js/ace/mode/haskell_highlight_rules.js | 246 + services/web/public/js/ace/mode/haxe.js | 56 + .../js/ace/mode/haxe_highlight_rules.js | 98 + services/web/public/js/ace/mode/html.js | 77 + .../public/js/ace/mode/html_completions.js | 313 + .../js/ace/mode/html_highlight_rules.js | 123 + services/web/public/js/ace/mode/html_ruby.js | 59 + .../js/ace/mode/html_ruby_highlight_rules.js | 84 + services/web/public/js/ace/mode/html_test.js | 67 + services/web/public/js/ace/mode/ini.js | 53 + .../public/js/ace/mode/ini_highlight_rules.js | 112 + services/web/public/js/ace/mode/jack.js | 79 + .../js/ace/mode/jack_highlight_rules.js | 142 + services/web/public/js/ace/mode/jade.js | 57 + .../js/ace/mode/jade_highlight_rules.js | 341 + services/web/public/js/ace/mode/java.js | 24 + .../js/ace/mode/java_highlight_rules.js | 131 + services/web/public/js/ace/mode/javascript.js | 116 + .../public/js/ace/mode/javascript/jshint.js | 9075 ++++ .../js/ace/mode/javascript_highlight_rules.js | 362 + .../web/public/js/ace/mode/javascript_test.js | 213 + .../public/js/ace/mode/javascript_worker.js | 187 + .../js/ace/mode/javascript_worker_test.js | 106 + .../js/ace/mode/js_regex_highlight_rules.js | 94 + services/web/public/js/ace/mode/json.js | 93 + .../web/public/js/ace/mode/json/json_parse.js | 346 + .../js/ace/mode/json_highlight_rules.js | 100 + .../web/public/js/ace/mode/json_worker.js | 67 + .../public/js/ace/mode/json_worker_test.js | 101 + services/web/public/js/ace/mode/jsoniq.js | 106 + services/web/public/js/ace/mode/jsp.js | 55 + .../public/js/ace/mode/jsp_highlight_rules.js | 91 + services/web/public/js/ace/mode/jsx.js | 56 + .../public/js/ace/mode/jsx_highlight_rules.js | 120 + services/web/public/js/ace/mode/julia.js | 62 + .../js/ace/mode/julia_highlight_rules.js | 170 + services/web/public/js/ace/mode/latex.js | 24 + .../js/ace/mode/latex_highlight_rules.js | 38 + services/web/public/js/ace/mode/less.js | 84 + .../js/ace/mode/less_highlight_rules.js | 271 + services/web/public/js/ace/mode/liquid.js | 82 + .../js/ace/mode/liquid_highlight_rules.js | 131 + services/web/public/js/ace/mode/lisp.js | 56 + .../js/ace/mode/lisp_highlight_rules.js | 124 + services/web/public/js/ace/mode/livescript.js | 248 + services/web/public/js/ace/mode/logiql.js | 139 + .../js/ace/mode/logiql_highlight_rules.js | 119 + .../web/public/js/ace/mode/logiql_test.js | 99 + services/web/public/js/ace/mode/lsl.js | 92 + .../public/js/ace/mode/lsl_highlight_rules.js | 363 + services/web/public/js/ace/mode/lua.js | 168 + .../web/public/js/ace/mode/lua/luaparse.js | 1989 + .../public/js/ace/mode/lua_highlight_rules.js | 193 + services/web/public/js/ace/mode/lua_worker.js | 71 + services/web/public/js/ace/mode/luapage.js | 21 + .../js/ace/mode/luapage_highlight_rules.js | 49 + services/web/public/js/ace/mode/lucene.js | 16 + .../js/ace/mode/lucene_highlight_rules.js | 49 + services/web/public/js/ace/mode/makefile.js | 63 + .../js/ace/mode/makefile_highlight_rules.js | 75 + services/web/public/js/ace/mode/markdown.js | 76 + .../js/ace/mode/markdown_highlight_rules.js | 224 + .../js/ace/mode/matching_brace_outdent.js | 69 + .../js/ace/mode/matching_parens_outdent.js | 74 + services/web/public/js/ace/mode/matlab.js | 55 + .../js/ace/mode/matlab_highlight_rules.js | 204 + services/web/public/js/ace/mode/mushcode.js | 116 + .../public/js/ace/mode/mushcode_high_rules.js | 569 + services/web/public/js/ace/mode/mysql.js | 51 + .../js/ace/mode/mysql_highlight_rules.js | 122 + services/web/public/js/ace/mode/nix.js | 62 + .../public/js/ace/mode/nix_highlight_rules.js | 119 + services/web/public/js/ace/mode/objectivec.js | 61 + .../js/ace/mode/objectivec_highlight_rules.js | 331 + services/web/public/js/ace/mode/ocaml.js | 97 + .../js/ace/mode/ocaml_highlight_rules.js | 337 + services/web/public/js/ace/mode/pascal.js | 67 + .../js/ace/mode/pascal_highlight_rules.js | 127 + services/web/public/js/ace/mode/perl.js | 90 + .../js/ace/mode/perl_highlight_rules.js | 165 + services/web/public/js/ace/mode/pgsql.js | 59 + .../js/ace/mode/pgsql_highlight_rules.js | 570 + services/web/public/js/ace/mode/php.js | 135 + services/web/public/js/ace/mode/php/php.js | 5003 ++ .../public/js/ace/mode/php_highlight_rules.js | 1088 + services/web/public/js/ace/mode/php_worker.js | 76 + services/web/public/js/ace/mode/plain_text.js | 55 + .../web/public/js/ace/mode/plain_text_test.js | 56 + services/web/public/js/ace/mode/powershell.js | 61 + .../js/ace/mode/powershell_highlight_rules.js | 145 + services/web/public/js/ace/mode/prolog.js | 62 + .../js/ace/mode/prolog_highlight_rules.js | 238 + services/web/public/js/ace/mode/properties.js | 45 + .../js/ace/mode/properties_highlight_rules.js | 86 + services/web/public/js/ace/mode/protobuf.js | 66 + .../js/ace/mode/protobuf_highlight_rules.js | 66 + services/web/public/js/ace/mode/python.js | 113 + .../js/ace/mode/python_highlight_rules.js | 191 + .../web/public/js/ace/mode/python_test.js | 79 + services/web/public/js/ace/mode/r.js | 154 + .../public/js/ace/mode/r_highlight_rules.js | 188 + services/web/public/js/ace/mode/rdoc.js | 61 + .../js/ace/mode/rdoc_highlight_rules.js | 99 + services/web/public/js/ace/mode/rhtml.js | 86 + .../js/ace/mode/rhtml_highlight_rules.js | 46 + services/web/public/js/ace/mode/ruby.js | 91 + .../js/ace/mode/ruby_highlight_rules.js | 249 + services/web/public/js/ace/mode/ruby_test.js | 78 + services/web/public/js/ace/mode/rust.js | 62 + .../js/ace/mode/rust_highlight_rules.js | 129 + services/web/public/js/ace/mode/sass.js | 52 + .../js/ace/mode/sass_highlight_rules.js | 79 + services/web/public/js/ace/mode/scad.js | 99 + .../js/ace/mode/scad_highlight_rules.js | 142 + services/web/public/js/ace/mode/scala.js | 25 + .../js/ace/mode/scala_highlight_rules.js | 160 + services/web/public/js/ace/mode/scheme.js | 56 + .../js/ace/mode/scheme_highlight_rules.js | 123 + services/web/public/js/ace/mode/scss.js | 84 + .../js/ace/mode/scss_highlight_rules.js | 296 + services/web/public/js/ace/mode/sh.js | 114 + .../public/js/ace/mode/sh_highlight_rules.js | 144 + services/web/public/js/ace/mode/sjs.js | 59 + .../public/js/ace/mode/sjs_highlight_rules.js | 233 + services/web/public/js/ace/mode/snippets.js | 112 + .../web/public/js/ace/mode/soy_template.js | 60 + .../ace/mode/soy_template_highlight_rules.js | 356 + services/web/public/js/ace/mode/space.js | 21 + .../js/ace/mode/space_highlight_rules.js | 56 + services/web/public/js/ace/mode/sql.js | 53 + .../public/js/ace/mode/sql_highlight_rules.js | 94 + services/web/public/js/ace/mode/stylus.js | 59 + .../js/ace/mode/stylus_highlight_rules.js | 165 + services/web/public/js/ace/mode/svg.js | 69 + .../public/js/ace/mode/svg_highlight_rules.js | 49 + services/web/public/js/ace/mode/tcl.js | 84 + .../public/js/ace/mode/tcl_highlight_rules.js | 172 + services/web/public/js/ace/mode/tex.js | 68 + .../public/js/ace/mode/tex_highlight_rules.js | 107 + services/web/public/js/ace/mode/text.js | 384 + .../js/ace/mode/text_highlight_rules.js | 234 + services/web/public/js/ace/mode/text_test.js | 64 + services/web/public/js/ace/mode/textile.js | 66 + .../js/ace/mode/textile_highlight_rules.js | 93 + services/web/public/js/ace/mode/toml.js | 56 + .../js/ace/mode/toml_highlight_rules.js | 103 + services/web/public/js/ace/mode/twig.js | 92 + .../js/ace/mode/twig_highlight_rules.js | 166 + services/web/public/js/ace/mode/typescript.js | 62 + .../js/ace/mode/typescript_highlight_rules.js | 98 + services/web/public/js/ace/mode/vbscript.js | 60 + .../js/ace/mode/vbscript_highlight_rules.js | 246 + services/web/public/js/ace/mode/velocity.js | 59 + .../js/ace/mode/velocity_highlight_rules.js | 177 + services/web/public/js/ace/mode/verilog.js | 54 + .../js/ace/mode/verilog_highlight_rules.js | 101 + services/web/public/js/ace/mode/vhdl.js | 52 + .../js/ace/mode/vhdl_highlight_rules.js | 115 + services/web/public/js/ace/mode/xml.js | 56 + .../public/js/ace/mode/xml_highlight_rules.js | 216 + services/web/public/js/ace/mode/xml_test.js | 75 + services/web/public/js/ace/mode/xml_util.js | 100 + services/web/public/js/ace/mode/xquery.js | 139 + .../ace/mode/xquery/JSONParseTreeHandler.js | 178 + .../public/js/ace/mode/xquery/JSONiqLexer.js | 302 + .../js/ace/mode/xquery/JSONiqTokenizer.ebnf | 544 + .../js/ace/mode/xquery/JSONiqTokenizer.js | 4205 ++ .../web/public/js/ace/mode/xquery/Readme.md | 6 + .../public/js/ace/mode/xquery/XQueryLexer.js | 303 + .../js/ace/mode/xquery/XQueryParser.ebnf | 1180 + .../public/js/ace/mode/xquery/XQueryParser.js | 33487 +++++++++++++ .../js/ace/mode/xquery/XQueryTokenizer.ebnf | 543 + .../js/ace/mode/xquery/XQueryTokenizer.js | 4205 ++ .../xquery/visitors/SemanticHighlighter.js | 76 + .../web/public/js/ace/mode/xquery_worker.js | 81 + services/web/public/js/ace/mode/yaml.js | 78 + .../js/ace/mode/yaml_highlight_rules.js | 116 + services/web/public/js/ace/model/editor.js | 62 + .../js/ace/mouse/default_gutter_handler.js | 161 + .../public/js/ace/mouse/default_handlers.js | 275 + .../public/js/ace/mouse/dragdrop_handler.js | 417 + .../web/public/js/ace/mouse/fold_handler.js | 93 + .../web/public/js/ace/mouse/mouse_event.js | 129 + .../web/public/js/ace/mouse/mouse_handler.js | 161 + .../js/ace/mouse/multi_select_handler.js | 160 + services/web/public/js/ace/multi_select.js | 956 + .../web/public/js/ace/multi_select_test.js | 205 + services/web/public/js/ace/occur.js | 190 + services/web/public/js/ace/occur_test.js | 154 + services/web/public/js/ace/placeholder.js | 269 + .../web/public/js/ace/placeholder_test.js | 156 + services/web/public/js/ace/range.js | 549 + services/web/public/js/ace/range_list.js | 240 + services/web/public/js/ace/range_list_test.js | 182 + services/web/public/js/ace/range_test.js | 191 + services/web/public/js/ace/renderloop.js | 75 + services/web/public/js/ace/requirejs/text.js | 51 + services/web/public/js/ace/scrollbar.js | 204 + services/web/public/js/ace/search.js | 389 + .../web/public/js/ace/search_highlight.js | 82 + services/web/public/js/ace/search_test.js | 461 + services/web/public/js/ace/selection.js | 920 + services/web/public/js/ace/selection_test.js | 480 + services/web/public/js/ace/snippets.js | 844 + .../web/public/js/ace/snippets/_.snippets | 240 + services/web/public/js/ace/snippets/abap.js | 7 + .../web/public/js/ace/snippets/abap.snippets | 0 .../public/js/ace/snippets/actionscript.js | 7 + .../js/ace/snippets/actionscript.snippets | 157 + services/web/public/js/ace/snippets/ada.js | 7 + .../web/public/js/ace/snippets/ada.snippets | 0 .../public/js/ace/snippets/apache.snippets | 35 + .../web/public/js/ace/snippets/apache_conf.js | 7 + .../js/ace/snippets/apache_conf.snippets | 0 .../web/public/js/ace/snippets/asciidoc.js | 7 + .../public/js/ace/snippets/asciidoc.snippets | 0 .../public/js/ace/snippets/assembly_x86.js | 7 + .../js/ace/snippets/assembly_x86.snippets | 0 .../web/public/js/ace/snippets/autohotkey.js | 7 + .../js/ace/snippets/autohotkey.snippets | 0 .../public/js/ace/snippets/autoit.snippets | 66 + .../web/public/js/ace/snippets/batchfile.js | 7 + .../public/js/ace/snippets/batchfile.snippets | 0 .../web/public/js/ace/snippets/c.snippets | 235 + .../web/public/js/ace/snippets/c9search.js | 7 + .../public/js/ace/snippets/c9search.snippets | 0 services/web/public/js/ace/snippets/c_cpp.js | 7 + .../web/public/js/ace/snippets/c_cpp.snippets | 131 + .../web/public/js/ace/snippets/chef.snippets | 204 + .../web/public/js/ace/snippets/clojure.js | 7 + .../public/js/ace/snippets/clojure.snippets | 90 + .../web/public/js/ace/snippets/cmake.snippets | 58 + services/web/public/js/ace/snippets/cobol.js | 7 + .../web/public/js/ace/snippets/cobol.snippets | 0 services/web/public/js/ace/snippets/coffee.js | 7 + .../public/js/ace/snippets/coffee.snippets | 95 + .../web/public/js/ace/snippets/coldfusion.js | 7 + .../js/ace/snippets/coldfusion.snippets | 0 .../web/public/js/ace/snippets/cs.snippets | 374 + services/web/public/js/ace/snippets/csharp.js | 7 + .../public/js/ace/snippets/csharp.snippets | 0 services/web/public/js/ace/snippets/css.js | 7 + .../web/public/js/ace/snippets/css.snippets | 967 + services/web/public/js/ace/snippets/curly.js | 7 + .../web/public/js/ace/snippets/curly.snippets | 0 services/web/public/js/ace/snippets/d.js | 7 + .../web/public/js/ace/snippets/d.snippets | 0 services/web/public/js/ace/snippets/dart.js | 7 + .../web/public/js/ace/snippets/dart.snippets | 83 + services/web/public/js/ace/snippets/diff.js | 7 + .../web/public/js/ace/snippets/diff.snippets | 11 + services/web/public/js/ace/snippets/django.js | 7 + .../public/js/ace/snippets/django.snippets | 108 + services/web/public/js/ace/snippets/dot.js | 7 + .../web/public/js/ace/snippets/dot.snippets | 0 services/web/public/js/ace/snippets/ejs.js | 7 + .../web/public/js/ace/snippets/ejs.snippets | 0 services/web/public/js/ace/snippets/erlang.js | 7 + .../public/js/ace/snippets/erlang.snippets | 160 + .../web/public/js/ace/snippets/eruby.snippets | 113 + .../public/js/ace/snippets/falcon.snippets | 71 + services/web/public/js/ace/snippets/forth.js | 7 + .../web/public/js/ace/snippets/forth.snippets | 0 services/web/public/js/ace/snippets/ftl.js | 7 + .../web/public/js/ace/snippets/ftl.snippets | 0 services/web/public/js/ace/snippets/glsl.js | 7 + .../web/public/js/ace/snippets/glsl.snippets | 0 .../web/public/js/ace/snippets/go.snippets | 201 + services/web/public/js/ace/snippets/golang.js | 7 + .../public/js/ace/snippets/golang.snippets | 0 services/web/public/js/ace/snippets/groovy.js | 7 + .../public/js/ace/snippets/groovy.snippets | 0 services/web/public/js/ace/snippets/haml.js | 7 + .../web/public/js/ace/snippets/haml.snippets | 20 + .../web/public/js/ace/snippets/handlebars.js | 7 + .../js/ace/snippets/handlebars.snippets | 0 .../web/public/js/ace/snippets/haskell.js | 7 + .../public/js/ace/snippets/haskell.snippets | 82 + services/web/public/js/ace/snippets/haxe.js | 7 + .../web/public/js/ace/snippets/haxe.snippets | 0 services/web/public/js/ace/snippets/html.js | 7 + .../web/public/js/ace/snippets/html.snippets | 828 + .../web/public/js/ace/snippets/html_ruby.js | 7 + .../public/js/ace/snippets/html_ruby.snippets | 0 .../js/ace/snippets/htmldjango.snippets | 136 + .../js/ace/snippets/htmltornado.snippets | 55 + services/web/public/js/ace/snippets/ini.js | 7 + .../web/public/js/ace/snippets/ini.snippets | 0 services/web/public/js/ace/snippets/jade.js | 7 + .../web/public/js/ace/snippets/jade.snippets | 0 services/web/public/js/ace/snippets/java.js | 7 + .../web/public/js/ace/snippets/java.snippets | 240 + .../ace/snippets/javascript-jquery.snippets | 589 + .../web/public/js/ace/snippets/javascript.js | 7 + .../js/ace/snippets/javascript.snippets | 195 + services/web/public/js/ace/snippets/json.js | 7 + .../web/public/js/ace/snippets/json.snippets | 0 services/web/public/js/ace/snippets/jsoniq.js | 7 + .../public/js/ace/snippets/jsoniq.snippets | 0 services/web/public/js/ace/snippets/jsp.js | 7 + .../web/public/js/ace/snippets/jsp.snippets | 99 + services/web/public/js/ace/snippets/jsx.js | 7 + .../web/public/js/ace/snippets/jsx.snippets | 0 services/web/public/js/ace/snippets/julia.js | 7 + .../web/public/js/ace/snippets/julia.snippets | 0 services/web/public/js/ace/snippets/latex.js | 7 + .../web/public/js/ace/snippets/latex.snippets | 0 .../public/js/ace/snippets/ledger.snippets | 5 + services/web/public/js/ace/snippets/less.js | 7 + .../web/public/js/ace/snippets/less.snippets | 0 services/web/public/js/ace/snippets/liquid.js | 7 + .../public/js/ace/snippets/liquid.snippets | 0 services/web/public/js/ace/snippets/lisp.js | 7 + .../web/public/js/ace/snippets/lisp.snippets | 0 .../web/public/js/ace/snippets/livescript.js | 7 + .../js/ace/snippets/livescript.snippets | 0 services/web/public/js/ace/snippets/logiql.js | 7 + .../public/js/ace/snippets/logiql.snippets | 0 services/web/public/js/ace/snippets/lsl.js | 7 + .../web/public/js/ace/snippets/lsl.snippets | 887 + services/web/public/js/ace/snippets/lua.js | 7 + .../web/public/js/ace/snippets/lua.snippets | 21 + .../web/public/js/ace/snippets/luapage.js | 7 + .../public/js/ace/snippets/luapage.snippets | 0 services/web/public/js/ace/snippets/lucene.js | 7 + .../public/js/ace/snippets/lucene.snippets | 0 .../web/public/js/ace/snippets/makefile.js | 7 + .../public/js/ace/snippets/makefile.snippets | 4 + .../web/public/js/ace/snippets/mako.snippets | 54 + .../web/public/js/ace/snippets/markdown.js | 7 + .../public/js/ace/snippets/markdown.snippets | 88 + services/web/public/js/ace/snippets/matlab.js | 7 + .../public/js/ace/snippets/matlab.snippets | 0 .../web/public/js/ace/snippets/mushcode.js | 7 + .../public/js/ace/snippets/mushcode.snippets | 0 .../js/ace/snippets/mushcode_high_rules.js | 7 + .../ace/snippets/mushcode_high_rules.snippets | 0 services/web/public/js/ace/snippets/mysql.js | 7 + .../web/public/js/ace/snippets/mysql.snippets | 0 services/web/public/js/ace/snippets/nix.js | 7 + .../web/public/js/ace/snippets/nix.snippets | 0 .../web/public/js/ace/snippets/objc.snippets | 247 + .../web/public/js/ace/snippets/objectivec.js | 7 + .../js/ace/snippets/objectivec.snippets | 0 services/web/public/js/ace/snippets/ocaml.js | 7 + .../web/public/js/ace/snippets/ocaml.snippets | 0 services/web/public/js/ace/snippets/pascal.js | 7 + .../public/js/ace/snippets/pascal.snippets | 0 services/web/public/js/ace/snippets/perl.js | 7 + .../web/public/js/ace/snippets/perl.snippets | 347 + services/web/public/js/ace/snippets/pgsql.js | 7 + .../web/public/js/ace/snippets/pgsql.snippets | 0 services/web/public/js/ace/snippets/php.js | 7 + .../web/public/js/ace/snippets/php.snippets | 377 + .../web/public/js/ace/snippets/powershell.js | 7 + .../js/ace/snippets/powershell.snippets | 0 services/web/public/js/ace/snippets/prolog.js | 7 + .../public/js/ace/snippets/prolog.snippets | 0 .../web/public/js/ace/snippets/properties.js | 7 + .../js/ace/snippets/properties.snippets | 0 .../web/public/js/ace/snippets/protobuf.js | 7 + services/web/public/js/ace/snippets/python.js | 7 + .../public/js/ace/snippets/python.snippets | 158 + services/web/public/js/ace/snippets/r.js | 7 + .../web/public/js/ace/snippets/r.snippets | 121 + services/web/public/js/ace/snippets/rdoc.js | 7 + .../web/public/js/ace/snippets/rdoc.snippets | 0 services/web/public/js/ace/snippets/rhtml.js | 7 + .../web/public/js/ace/snippets/rhtml.snippets | 0 .../web/public/js/ace/snippets/rst.snippets | 22 + services/web/public/js/ace/snippets/ruby.js | 7 + .../web/public/js/ace/snippets/ruby.snippets | 928 + services/web/public/js/ace/snippets/rust.js | 7 + .../web/public/js/ace/snippets/rust.snippets | 0 services/web/public/js/ace/snippets/sass.js | 7 + .../web/public/js/ace/snippets/sass.snippets | 0 services/web/public/js/ace/snippets/scad.js | 7 + .../web/public/js/ace/snippets/scad.snippets | 0 services/web/public/js/ace/snippets/scala.js | 7 + .../web/public/js/ace/snippets/scala.snippets | 0 services/web/public/js/ace/snippets/scheme.js | 7 + .../public/js/ace/snippets/scheme.snippets | 0 services/web/public/js/ace/snippets/scss.js | 7 + .../web/public/js/ace/snippets/scss.snippets | 0 services/web/public/js/ace/snippets/sh.js | 7 + .../web/public/js/ace/snippets/sh.snippets | 83 + .../web/public/js/ace/snippets/snippets.js | 7 + .../public/js/ace/snippets/snippets.snippets | 9 + .../public/js/ace/snippets/soy_template.js | 7 + .../js/ace/snippets/soy_template.snippets | 0 services/web/public/js/ace/snippets/sql.js | 7 + .../web/public/js/ace/snippets/sql.snippets | 26 + services/web/public/js/ace/snippets/stylus.js | 7 + .../public/js/ace/snippets/stylus.snippets | 0 services/web/public/js/ace/snippets/svg.js | 7 + .../web/public/js/ace/snippets/svg.snippets | 0 services/web/public/js/ace/snippets/tcl.js | 7 + .../web/public/js/ace/snippets/tcl.snippets | 92 + services/web/public/js/ace/snippets/tex.js | 7 + .../web/public/js/ace/snippets/tex.snippets | 191 + services/web/public/js/ace/snippets/text.js | 7 + .../web/public/js/ace/snippets/text.snippets | 0 .../web/public/js/ace/snippets/textile.js | 7 + .../public/js/ace/snippets/textile.snippets | 30 + .../public/js/ace/snippets/tmsnippet.snippets | 0 services/web/public/js/ace/snippets/toml.js | 7 + .../web/public/js/ace/snippets/toml.snippets | 0 services/web/public/js/ace/snippets/twig.js | 7 + .../web/public/js/ace/snippets/twig.snippets | 0 .../web/public/js/ace/snippets/typescript.js | 7 + .../js/ace/snippets/typescript.snippets | 0 .../web/public/js/ace/snippets/vbscript.js | 7 + .../public/js/ace/snippets/vbscript.snippets | 0 .../web/public/js/ace/snippets/velocity.js | 7 + .../public/js/ace/snippets/velocity.snippets | 28 + .../web/public/js/ace/snippets/verilog.js | 7 + .../public/js/ace/snippets/verilog.snippets | 0 services/web/public/js/ace/snippets/vhdl.js | 7 + .../web/public/js/ace/snippets/vhdl.snippets | 0 services/web/public/js/ace/snippets/xml.js | 7 + .../web/public/js/ace/snippets/xml.snippets | 0 services/web/public/js/ace/snippets/xquery.js | 7 + .../public/js/ace/snippets/xquery.snippets | 0 .../web/public/js/ace/snippets/xslt.snippets | 97 + services/web/public/js/ace/snippets/yaml.js | 7 + .../web/public/js/ace/snippets/yaml.snippets | 0 services/web/public/js/ace/snippets_test.js | 131 + services/web/public/js/ace/split.js | 373 + services/web/public/js/ace/test/all.js | 35 + .../web/public/js/ace/test/all_browser.js | 138 + services/web/public/js/ace/test/assertions.js | 56 + .../web/public/js/ace/test/asyncjs/assert.js | 313 + .../web/public/js/ace/test/asyncjs/async.js | 529 + .../web/public/js/ace/test/asyncjs/index.js | 13 + .../web/public/js/ace/test/asyncjs/test.js | 195 + .../web/public/js/ace/test/asyncjs/utils.js | 65 + services/web/public/js/ace/test/benchmark.js | 78 + services/web/public/js/ace/test/mockdom.js | 10 + .../web/public/js/ace/test/mockrenderer.js | 201 + services/web/public/js/ace/test/tests.html | 46 + services/web/public/js/ace/theme/ambiance.css | 217 + services/web/public/js/ace/theme/ambiance.js | 33 + services/web/public/js/ace/theme/chaos.css | 154 + services/web/public/js/ace/theme/chaos.js | 33 + services/web/public/js/ace/theme/chrome.css | 153 + services/web/public/js/ace/theme/chrome.js | 39 + services/web/public/js/ace/theme/clouds.css | 112 + services/web/public/js/ace/theme/clouds.js | 39 + .../public/js/ace/theme/clouds_midnight.css | 113 + .../public/js/ace/theme/clouds_midnight.js | 39 + services/web/public/js/ace/theme/cobalt.css | 134 + services/web/public/js/ace/theme/cobalt.js | 39 + .../public/js/ace/theme/crimson_editor.css | 143 + .../web/public/js/ace/theme/crimson_editor.js | 39 + services/web/public/js/ace/theme/dawn.css | 127 + services/web/public/js/ace/theme/dawn.js | 39 + .../web/public/js/ace/theme/dreamweaver.css | 171 + .../web/public/js/ace/theme/dreamweaver.js | 38 + services/web/public/js/ace/theme/eclipse.css | 108 + services/web/public/js/ace/theme/eclipse.js | 41 + services/web/public/js/ace/theme/github.css | 119 + services/web/public/js/ace/theme/github.js | 39 + .../web/public/js/ace/theme/idle_fingers.css | 113 + .../web/public/js/ace/theme/idle_fingers.js | 39 + .../web/public/js/ace/theme/katzenmilch.css | 140 + .../web/public/js/ace/theme/katzenmilch.js | 39 + services/web/public/js/ace/theme/kr_theme.css | 124 + services/web/public/js/ace/theme/kr_theme.js | 39 + services/web/public/js/ace/theme/kuroir.css | 68 + services/web/public/js/ace/theme/kuroir.js | 39 + .../web/public/js/ace/theme/merbivore.css | 110 + services/web/public/js/ace/theme/merbivore.js | 39 + .../public/js/ace/theme/merbivore_soft.css | 111 + .../web/public/js/ace/theme/merbivore_soft.js | 39 + .../public/js/ace/theme/mono_industrial.css | 126 + .../public/js/ace/theme/mono_industrial.js | 39 + services/web/public/js/ace/theme/monokai.css | 122 + services/web/public/js/ace/theme/monokai.js | 39 + .../public/js/ace/theme/pastel_on_dark.css | 129 + .../web/public/js/ace/theme/pastel_on_dark.js | 39 + .../public/js/ace/theme/solarized_dark.css | 101 + .../web/public/js/ace/theme/solarized_dark.js | 39 + .../public/js/ace/theme/solarized_light.css | 106 + .../public/js/ace/theme/solarized_light.js | 39 + services/web/public/js/ace/theme/terminal.css | 132 + services/web/public/js/ace/theme/terminal.js | 39 + services/web/public/js/ace/theme/textmate.css | 154 + services/web/public/js/ace/theme/textmate.js | 40 + services/web/public/js/ace/theme/tomorrow.css | 125 + services/web/public/js/ace/theme/tomorrow.js | 39 + .../public/js/ace/theme/tomorrow_night.css | 125 + .../web/public/js/ace/theme/tomorrow_night.js | 39 + .../js/ace/theme/tomorrow_night_blue.css | 122 + .../js/ace/theme/tomorrow_night_blue.js | 39 + .../js/ace/theme/tomorrow_night_bright.css | 140 + .../js/ace/theme/tomorrow_night_bright.js | 39 + .../js/ace/theme/tomorrow_night_eighties.css | 125 + .../js/ace/theme/tomorrow_night_eighties.js | 39 + services/web/public/js/ace/theme/twilight.css | 128 + services/web/public/js/ace/theme/twilight.js | 39 + .../web/public/js/ace/theme/vibrant_ink.css | 110 + .../web/public/js/ace/theme/vibrant_ink.js | 39 + services/web/public/js/ace/theme/xcode.css | 104 + services/web/public/js/ace/theme/xcode.js | 39 + services/web/public/js/ace/token_iterator.js | 150 + .../web/public/js/ace/token_iterator_test.js | 212 + services/web/public/js/ace/tokenizer.js | 337 + services/web/public/js/ace/tokenizer_dev.js | 183 + services/web/public/js/ace/tokenizer_test.js | 69 + services/web/public/js/ace/undomanager.js | 167 + services/web/public/js/ace/unicode.js | 107 + .../web/public/js/ace/virtual_renderer.js | 1695 + .../public/js/ace/virtual_renderer_test.js | 86 + services/web/public/js/ace/worker/mirror.js | 49 + services/web/public/js/ace/worker/worker.js | 182 + .../web/public/js/ace/worker/worker_client.js | 219 + .../web/public/js/ace/worker/worker_test.js | 125 + services/web/public/js/codeprettifyer.js | 7 + services/web/public/js/documentUpdater.js | 181 + services/web/public/js/libs/backbone.js | 1430 + services/web/public/js/libs/bootstrap.js | 1723 + .../js/libs/bootstrap/bootstrap2full.js | 1947 + services/web/public/js/libs/chai.js | 3464 ++ services/web/public/js/libs/codemirror.css | 263 + services/web/public/js/libs/compatibility.js | 483 + services/web/public/js/libs/fileuploader.js | 1248 + services/web/public/js/libs/fineuploader.js | 3761 ++ .../js/libs/google-code-prettify/latex.js | 49 + .../js/libs/google-code-prettify/prettify.js | 31 + services/web/public/js/libs/intro.js | 795 + services/web/public/js/libs/jquery-layout.js | 9174 ++++ services/web/public/js/libs/jquery.color.js | 10 + .../web/public/js/libs/jquery.dataTables.js | 12099 +++++ services/web/public/js/libs/jquery.js | 2 + .../web/public/js/libs/jquery.slides.min.js | 7 + services/web/public/js/libs/jquery.storage.js | 85 + .../web/public/js/libs/jquery.tablesorter.js | 1341 + .../web/public/js/libs/jquery.validate.js | 1230 + .../web/public/js/libs/latex-log-parser.js | 275 + services/web/public/js/libs/linkify.js | 215 + services/web/public/js/libs/mocha.js | 4477 ++ services/web/public/js/libs/moment.js | 1662 + services/web/public/js/libs/mustache.js | 6 + services/web/public/js/libs/orchard.js | 12155 +++++ services/web/public/js/libs/pdf.js | 7640 +++ services/web/public/js/libs/pdf.worker.js | 40015 ++++++++++++++++ .../.AnnotationsLayerBuilder.js.swp | Bin 0 -> 12288 bytes .../js/libs/pdfListView/.PdfListView.js.swp | Bin 0 -> 45056 bytes .../js/libs/pdfListView/AnnotationsLayer.css | 14 + .../pdfListView/AnnotationsLayerBuilder.js | 64 + .../public/js/libs/pdfListView/PdfListView.js | 819 + .../public/js/libs/pdfListView/TextLayer.css | 28 + .../js/libs/pdfListView/TextLayerBuilder.js | 190 + services/web/public/js/libs/recurly.min.js | 1 + services/web/public/js/libs/require.js | 36 + services/web/public/js/libs/sinon.js | 4081 ++ services/web/public/js/libs/tagmanager.js | 548 + services/web/public/js/libs/typeahead.js | 1142 + services/web/public/js/libs/underscore.js | 1068 + .../public/js/models/revisionHistoryModel.js | 37 + .../web/public/js/revisionHistoryModel.js | 17 + services/web/public/js/search/searchbox.js | 262 + services/web/public/js/text.js | 373 + services/web/public/r.js | 26058 ++++++++++ services/web/public/recurly/images/check.png | Bin 0 -> 190 bytes .../public/recurly/images/coupon_check.png | Bin 0 -> 213 bytes .../public/recurly/images/coupon_checking.gif | Bin 0 -> 847 bytes .../public/recurly/images/coupon_invalid.png | Bin 0 -> 225 bytes .../public/recurly/images/coupon_valid.png | Bin 0 -> 225 bytes .../images/credit_cards/american_express.png | Bin 0 -> 2630 bytes .../images/credit_cards/diners_club.png | Bin 0 -> 2387 bytes .../recurly/images/credit_cards/discover.png | Bin 0 -> 2193 bytes .../recurly/images/credit_cards/jcb.png | Bin 0 -> 2304 bytes .../recurly/images/credit_cards/laser.png | Bin 0 -> 2385 bytes .../recurly/images/credit_cards/maestro.png | Bin 0 -> 2647 bytes .../images/credit_cards/mastercard.png | Bin 0 -> 2586 bytes .../recurly/images/credit_cards/visa.png | Bin 0 -> 2310 bytes services/web/public/recurly/images/dash.png | Bin 0 -> 84 bytes .../web/public/recurly/images/due_now.png | Bin 0 -> 1403 bytes services/web/public/recurly/images/error.png | Bin 0 -> 332 bytes .../web/public/recurly/images/loading.gif | Bin 0 -> 1849 bytes .../web/public/recurly/images/paypal_logo.png | Bin 0 -> 5888 bytes .../web/public/recurly/images/submitting.gif | Bin 0 -> 3208 bytes .../web/public/recurly/images/uncheck.png | Bin 0 -> 186 bytes services/web/public/recurly/recurly.css | 787 + services/web/public/recurly/recurly.styl | 772 + services/web/public/robots.txt | 5 + services/web/public/sharelatex-security.pub | 15 + .../public/stylesheets/bootstrap-select.css | 266 + .../web/public/stylesheets/codemirror.css | 263 + .../web/public/stylesheets/fileuploader.css | 31 + .../web/public/stylesheets/less/blog.less | 77 + .../web/public/stylesheets/less/bonus.less | 216 + .../web/public/stylesheets/less/core.less | 73 + .../web/public/stylesheets/less/editor.less | 1011 + .../web/public/stylesheets/less/elements.less | 136 + services/web/public/stylesheets/less/faq.less | 5 + .../public/stylesheets/less/fileuploader.less | 159 + .../web/public/stylesheets/less/footer.less | 41 + .../web/public/stylesheets/less/home.less | 114 + .../web/public/stylesheets/less/intro.less | 216 + .../web/public/stylesheets/less/list.less | 239 + .../web/public/stylesheets/less/navbar.less | 15 + .../web/public/stylesheets/less/orchard.less | 236 + .../web/public/stylesheets/less/plans.less | 619 + .../web/public/stylesheets/less/prettify.less | 30 + .../public/stylesheets/less/revisions.less | 190 + .../web/public/stylesheets/less/style.less | 82 + .../stylesheets/less/subscriptions.less | 80 + .../public/stylesheets/less/tagmanager.less | 101 + services/web/public/stylesheets/loading.gif | Bin 0 -> 1688 bytes .../web/public/stylesheets/mainStyle.less | 62 + services/web/public/stylesheets/mocha.css | 182 + .../web/public/stylesheets/orbit-1.2.3.css | 201 + services/web/public/stylesheets/prices.less | 7 + .../web/public/stylesheets/variables.less | 99 + .../AuthenticationControllerTests.coffee | 313 + .../AuthenticationManagerTests.coffee | 143 + .../CollaboratorsControllerTests.coffee | 148 + .../coffee/Compile/ClsiManagerTests.coffee | 169 + .../Compile/CompileControllerTests.coffee | 109 + .../coffee/Compile/CompileManagerTests.coffee | 231 + .../DocumentUpdaterHandlerTests.coffee | 270 + .../GetNumberOfDocsInMemoryTests.coffee | 43 + .../Documents/DocumentControllerTests.coffee | 89 + .../ProjectDownloadsControllerTests.coffee | 75 + .../ProjectZipStreamManagerTests.coffee | 140 + .../coffee/Dropbox/DropboxHandlerTests.coffee | 83 + .../Editor/EditorControllerTests.coffee | 612 + .../EditorRealTimeControllerTests.coffee | 89 + .../EditorUpdatesControllerTests.coffee | 175 + .../FileStore/FileStoreHandlerTests.coffee | 144 + .../Project/DocLinesComparitorTests.coffee | 71 + .../Project/ProjectApiControllerTests.coffee | 40 + .../ProjectCreationHandlerTests.coffee | 168 + .../coffee/Project/ProjectDeleterTests.coffee | 41 + .../Project/ProjectDetailsHandlerTests.coffee | 59 + .../Project/ProjectDuplicatorTests.coffee | 130 + .../Project/ProjectEditorHandlerTests.coffee | 168 + .../Project/ProjectEntityHandlerTests.coffee | 684 + .../coffee/Project/ProjectGetterTests.coffee | 77 + .../coffee/Project/ProjectLocatorTests.coffee | 243 + .../Project/ProjectOptionsHandlerTests.coffee | 61 + .../Project/ProjectRootDocManagerTests.coffee | 80 + .../Project/ProjectUpdateHandlerTests.coffee | 24 + .../Referal/ReferalAllocatorTests.coffee | 219 + .../coffee/Referal/ReferalConnectTests.coffee | 101 + .../Referal/ReferalControllerTests.coffee | 13 + .../coffee/Referal/ReferalHandlerTests.coffee | 35 + .../Security/AuthorizationManagerTests.coffee | 96 + .../coffee/Security/LoginRateLimiter.coffee | 68 + .../LimitationsManagerTests.coffee | 243 + .../Subscription/RecurlyWrapperTests.coffee | 310 + .../SubscriptionBackgroundTasksTests.coffee | 46 + .../SubscriptionControllerTests.coffee | 299 + .../SubscriptionGroupControllerTests.coffee | 61 + .../SubscriptionGroupHandlerTests.coffee | 90 + .../SubscriptionHandlerTests.coffee | 195 + .../SubscriptionLocatorTests.coffee | 60 + .../SubscriptionUpdaterTests.coffee | 164 + .../UserFeaturesUpdaterTests.coffee | 34 + .../coffee/Tags/TagsControllerTests.coffee | 50 + .../coffee/Tags/TagsHandlerTests.coffee | 102 + .../Templates/TemplatesControllerTests.coffee | 135 + .../Templates/TemplatesPublisherTests.coffee | 42 + .../TpdsControllerTests.coffee | 72 + .../TpdsPollingBackgroundTasksTests.coffee | 61 + .../TpdsUpdateHandlerTests.coffee | 96 + .../TpdsUpdateSenderTests.coffee | 104 + .../UpdateMergerTests.coffee | 178 + .../coffee/Uploads/ArchiveManagerTests.coffee | 59 + .../FileSystemImportManagerTests.coffee | 173 + .../Uploads/FileTypeManagerTests.coffee | 105 + .../ProjectUploadControllerTests.coffee | 173 + .../Uploads/ProjectUploadManagerTests.coffee | 94 + .../coffee/User/UserControllerTests.coffee | 105 + .../coffee/User/UserCreatorTests.coffee | 57 + .../coffee/User/UserDeleterTests.coffee | 47 + .../coffee/User/UserLocatorTests.coffee | 37 + .../User/UserRegistrationHandlerTests.coffee | 67 + .../AutomaticSnapshotManagerTests.coffee | 140 + .../VersioningApiControllerTests.coffee | 89 + .../VersioningApiHandlerTests.coffee | 168 + .../coffee/helpers/MockClient.coffee | 17 + .../coffee/helpers/MockRequest.coffee | 10 + .../coffee/helpers/MockResponse.coffee | 51 + .../web/test/smoke/coffee/SmokeTests.coffee | 55 + 1694 files changed, 426896 insertions(+) create mode 100644 services/web/.gitignore create mode 100644 services/web/.npmignore create mode 100644 services/web/BackgroundJobsWorker.coffee create mode 100644 services/web/Gruntfile.coffee create mode 100644 services/web/TpdsWorker.coffee create mode 100644 services/web/app.coffee create mode 100644 services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee create mode 100644 services/web/app/coffee/Features/Authentication/AuthenticationController.coffee create mode 100644 services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee create mode 100644 services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee create mode 100644 services/web/app/coffee/Features/Compile/ClsiManager.coffee create mode 100644 services/web/app/coffee/Features/Compile/CompileController.coffee create mode 100644 services/web/app/coffee/Features/Compile/CompileManager.coffee create mode 100644 services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee create mode 100644 services/web/app/coffee/Features/Documents/DocumentController.coffee create mode 100644 services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee create mode 100644 services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee create mode 100644 services/web/app/coffee/Features/Dropbox/DropboxHandler.coffee create mode 100644 services/web/app/coffee/Features/Editor/EditorController.coffee create mode 100644 services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee create mode 100644 services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee create mode 100644 services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee create mode 100644 services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee create mode 100644 services/web/app/coffee/Features/Project/DocLinesComparitor.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectApiController.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectDeleter.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectDuplicator.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectGetter.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectLocator.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee create mode 100644 services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee create mode 100644 services/web/app/coffee/Features/Referal/ReferalAllocator.coffee create mode 100644 services/web/app/coffee/Features/Referal/ReferalConnect.coffee create mode 100644 services/web/app/coffee/Features/Referal/ReferalController.coffee create mode 100644 services/web/app/coffee/Features/Referal/ReferalHandler.coffee create mode 100644 services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee create mode 100644 services/web/app/coffee/Features/Security/AuthorizationManager.coffee create mode 100644 services/web/app/coffee/Features/Security/LoginRateLimiter.coffee create mode 100644 services/web/app/coffee/Features/Spelling/SpellingController.coffee create mode 100644 services/web/app/coffee/Features/Subscription/LimitationsManager.coffee create mode 100644 services/web/app/coffee/Features/Subscription/PlansLocator.coffee create mode 100644 services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionBackgroundTasks.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionController.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee create mode 100644 services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee create mode 100644 services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee create mode 100644 services/web/app/coffee/Features/Tags/TagsController.coffee create mode 100644 services/web/app/coffee/Features/Tags/TagsHandler.coffee create mode 100644 services/web/app/coffee/Features/Templates/TemplatesController.coffee create mode 100644 services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee create mode 100644 services/web/app/coffee/Features/Templates/TemplatesPublisher.coffee create mode 100644 services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee create mode 100644 services/web/app/coffee/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks.coffee create mode 100644 services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee create mode 100644 services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee create mode 100644 services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee create mode 100644 services/web/app/coffee/Features/Uploads/ArchiveManager.coffee create mode 100644 services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee create mode 100644 services/web/app/coffee/Features/Uploads/FileTypeManager.coffee create mode 100644 services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee create mode 100644 services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee create mode 100644 services/web/app/coffee/Features/Uploads/UploadsRouter.coffee create mode 100644 services/web/app/coffee/Features/User/UserController.coffee create mode 100644 services/web/app/coffee/Features/User/UserCreator.coffee create mode 100644 services/web/app/coffee/Features/User/UserDeleter.coffee create mode 100644 services/web/app/coffee/Features/User/UserGetter.coffee create mode 100644 services/web/app/coffee/Features/User/UserLocator.coffee create mode 100644 services/web/app/coffee/Features/User/UserRegistrationHandler.coffee create mode 100644 services/web/app/coffee/Features/User/UserUpdater.coffee create mode 100644 services/web/app/coffee/Features/Versioning/AutomaticSnapshotManager.coffee create mode 100644 services/web/app/coffee/Features/Versioning/RedisKeys.coffee create mode 100644 services/web/app/coffee/Features/Versioning/VersioningApiController.coffee create mode 100644 services/web/app/coffee/Features/Versioning/VersioningApiHandler.coffee create mode 100755 services/web/app/coffee/controllers/AdminController.coffee create mode 100755 services/web/app/coffee/controllers/HomeController.coffee create mode 100755 services/web/app/coffee/controllers/InfoController.coffee create mode 100755 services/web/app/coffee/controllers/ProjectController.coffee create mode 100644 services/web/app/coffee/controllers/UserController.coffee create mode 100644 services/web/app/coffee/errors.coffee create mode 100755 services/web/app/coffee/handlers/ProjectHandler.coffee create mode 100644 services/web/app/coffee/infrastructure/BackgroundTasks.coffee create mode 100644 services/web/app/coffee/infrastructure/CrawlerLogger.coffee create mode 100644 services/web/app/coffee/infrastructure/ExpressLocals.coffee create mode 100644 services/web/app/coffee/infrastructure/Keys.coffee create mode 100644 services/web/app/coffee/infrastructure/LoggerSerializers.coffee create mode 100644 services/web/app/coffee/infrastructure/Metrics.coffee create mode 100644 services/web/app/coffee/infrastructure/Monitor.coffee create mode 100644 services/web/app/coffee/infrastructure/Monitor/HTTP.coffee create mode 100644 services/web/app/coffee/infrastructure/Monitor/MongoDB.coffee create mode 100644 services/web/app/coffee/infrastructure/RandomLogging.coffee create mode 100644 services/web/app/coffee/infrastructure/Server.coffee create mode 100644 services/web/app/coffee/infrastructure/SocketIoConfig.coffee create mode 100644 services/web/app/coffee/infrastructure/mongojs.coffee create mode 100644 services/web/app/coffee/managers/CollaberationManager.coffee create mode 100644 services/web/app/coffee/managers/EmailManager.coffee create mode 100644 services/web/app/coffee/managers/GuidManager.coffee create mode 100644 services/web/app/coffee/managers/NewsletterManager.coffee create mode 100644 services/web/app/coffee/managers/SecurityManager.coffee create mode 100644 services/web/app/coffee/models/Doc.coffee create mode 100644 services/web/app/coffee/models/File.coffee create mode 100644 services/web/app/coffee/models/Folder.coffee create mode 100644 services/web/app/coffee/models/Project.coffee create mode 100644 services/web/app/coffee/models/Quote.coffee create mode 100644 services/web/app/coffee/models/Subscription.coffee create mode 100644 services/web/app/coffee/models/User.coffee create mode 100644 services/web/app/coffee/router.coffee create mode 100755 services/web/app/templates/email/emailTemplate.html create mode 100755 services/web/app/templates/email/shared_project_email_template.html create mode 100644 services/web/app/templates/project_files/main.tex create mode 100644 services/web/app/templates/project_files/mainbasic.tex create mode 100644 services/web/app/templates/project_files/references.bib create mode 100644 services/web/app/templates/project_files/universe.jpg create mode 100644 services/web/app/views/about/about.jade create mode 100644 services/web/app/views/about/attribution.jade create mode 100644 services/web/app/views/about/planned_maintenance.jade create mode 100644 services/web/app/views/about/privacy.jade create mode 100644 services/web/app/views/about/security.jade create mode 100644 services/web/app/views/about/tos.jade create mode 100644 services/web/app/views/admin.jade create mode 100644 services/web/app/views/changelog.jade create mode 100644 services/web/app/views/general/404.jade create mode 100644 services/web/app/views/general/closed.jade create mode 100644 services/web/app/views/general/genericMessage.jade create mode 100644 services/web/app/views/general/long-form-features.jade create mode 100644 services/web/app/views/general/partial/registerForm.jade create mode 100644 services/web/app/views/general/sidebar.jade create mode 100644 services/web/app/views/general/small-footer.jade create mode 100644 services/web/app/views/general/social-footer.jade create mode 100644 services/web/app/views/homepage/comments.jade create mode 100644 services/web/app/views/homepage/header.jade create mode 100644 services/web/app/views/homepage/home.jade create mode 100644 services/web/app/views/homepage/socialMedia.jade create mode 100644 services/web/app/views/info/advisor.jade create mode 100644 services/web/app/views/info/dropbox.jade create mode 100644 services/web/app/views/info/themes.jade create mode 100644 services/web/app/views/layout.jade create mode 100644 services/web/app/views/menubar.jade create mode 100644 services/web/app/views/modals.jade create mode 100644 services/web/app/views/project/editor.jade create mode 100644 services/web/app/views/project/flat.jade create mode 100644 services/web/app/views/project/list.jade create mode 100644 services/web/app/views/project/new.jade create mode 100644 services/web/app/views/project/partials/manage.jade create mode 100644 services/web/app/views/project/partials/pdf.jade create mode 100644 services/web/app/views/project/table.jade create mode 100644 services/web/app/views/referal/bonus.jade create mode 100644 services/web/app/views/referal/facebookLike.jade create mode 100644 services/web/app/views/referal/facebookWallPost.jade create mode 100644 services/web/app/views/referal/googleplus.jade create mode 100644 services/web/app/views/referal/tweet.jade create mode 100644 services/web/app/views/referal/tweetShare.jade create mode 100644 services/web/app/views/resources.jade create mode 100644 services/web/app/views/subscriptions/dashboard.jade create mode 100644 services/web/app/views/subscriptions/edit-billing-details.jade create mode 100644 services/web/app/views/subscriptions/group_admin.jade create mode 100644 services/web/app/views/subscriptions/new.jade create mode 100644 services/web/app/views/subscriptions/plans.jade create mode 100644 services/web/app/views/subscriptions/successful_subscription.jade create mode 100644 services/web/app/views/templates.jade create mode 100644 services/web/app/views/templates/dropbox.jade create mode 100644 services/web/app/views/tests.jade create mode 100644 services/web/app/views/user/feedback.jade create mode 100644 services/web/app/views/user/login.jade create mode 100644 services/web/app/views/user/passwordReset.jade create mode 100644 services/web/app/views/user/register.jade create mode 100644 services/web/app/views/user/restricted.jade create mode 100644 services/web/app/views/user/settings.jade create mode 100644 services/web/config/.gitignore create mode 100644 services/web/config/settings.development.coffee create mode 100644 services/web/data/.gitignore create mode 100644 services/web/data/dumpFolder/.gitignore create mode 100644 services/web/data/logs/.gitignore create mode 100644 services/web/data/pdfs/.gitignore create mode 100644 services/web/data/uploads/.gitignore create mode 100644 services/web/data/zippedProjects/.gitignore create mode 100644 services/web/package.json create mode 100644 services/web/public/app.build.js create mode 100644 services/web/public/backbone.js create mode 100644 services/web/public/bootstrap/.gitignore create mode 100644 services/web/public/bootstrap/LICENSE create mode 100644 services/web/public/bootstrap/Makefile create mode 100644 services/web/public/bootstrap/README.md create mode 100644 services/web/public/bootstrap/docs/assets/css/bootstrap-responsive.css create mode 100644 services/web/public/bootstrap/docs/assets/css/bootstrap.css create mode 100644 services/web/public/bootstrap/docs/assets/css/docs.css create mode 100644 services/web/public/bootstrap/docs/assets/ico/bootstrap-apple-114x114.png create mode 100644 services/web/public/bootstrap/docs/assets/ico/bootstrap-apple-57x57.png create mode 100644 services/web/public/bootstrap/docs/assets/ico/bootstrap-apple-72x72.png create mode 100644 services/web/public/bootstrap/docs/assets/ico/favicon.ico create mode 100644 services/web/public/bootstrap/docs/assets/img/bird.png create mode 100644 services/web/public/bootstrap/docs/assets/img/bootstrap-mdo-sfmoma-01.jpg create mode 100644 services/web/public/bootstrap/docs/assets/img/bootstrap-mdo-sfmoma-02.jpg create mode 100644 services/web/public/bootstrap/docs/assets/img/bootstrap-mdo-sfmoma-03.jpg create mode 100644 services/web/public/bootstrap/docs/assets/img/browsers.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-diagram-01.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-diagram-02.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-diagram-03.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-sites/bartop.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-sites/fleetio.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-sites/jshint.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-sites/kippt.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-sites/railwayjs.png create mode 100644 services/web/public/bootstrap/docs/assets/img/example-sites/totalwireframe.png create mode 100644 services/web/public/bootstrap/docs/assets/img/examples/bootstrap-example-fluid.jpg create mode 100644 services/web/public/bootstrap/docs/assets/img/examples/bootstrap-example-hero.jpg create mode 100644 services/web/public/bootstrap/docs/assets/img/examples/bootstrap-example-starter.jpg create mode 100644 services/web/public/bootstrap/docs/assets/img/github-16px.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons-halflings-white.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons-halflings.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_009_magic.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_042_group.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_079_podium.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_082_roundabout.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_155_show_thumbnails.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_163_iphone.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_214_resize_small.png create mode 100644 services/web/public/bootstrap/docs/assets/img/glyphicons/glyphicons_266_book_open.png create mode 100644 services/web/public/bootstrap/docs/assets/img/grid-18px-masked.png create mode 100644 services/web/public/bootstrap/docs/assets/img/icon-css3.png create mode 100644 services/web/public/bootstrap/docs/assets/img/icon-github.png create mode 100644 services/web/public/bootstrap/docs/assets/img/icon-html5.png create mode 100644 services/web/public/bootstrap/docs/assets/img/icon-twitter.png create mode 100644 services/web/public/bootstrap/docs/assets/img/less-logo-large.png create mode 100644 services/web/public/bootstrap/docs/assets/img/less-small.png create mode 100644 services/web/public/bootstrap/docs/assets/img/responsive-illustrations.png create mode 100644 services/web/public/bootstrap/docs/assets/js/README.md create mode 100644 services/web/public/bootstrap/docs/assets/js/application.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-alert.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-button.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-carousel.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-collapse.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-dropdown.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-modal.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-popover.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-scrollspy.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-tab.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-tooltip.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-transition.js create mode 100644 services/web/public/bootstrap/docs/assets/js/bootstrap-typeahead.js create mode 100644 services/web/public/bootstrap/docs/assets/js/google-code-prettify/prettify.css create mode 100644 services/web/public/bootstrap/docs/assets/js/google-code-prettify/prettify.js create mode 100644 services/web/public/bootstrap/docs/assets/js/jquery.js create mode 100644 services/web/public/bootstrap/docs/base-css.html create mode 100644 services/web/public/bootstrap/docs/build/index.js create mode 100755 services/web/public/bootstrap/docs/build/node_modules/.bin/hulk create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/.git_ignore create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/.gitmodules create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/LICENSE create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/Makefile create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/README.md create mode 100755 services/web/public/bootstrap/docs/build/node_modules/hogan.js/bin/hulk create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/lib/compiler.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/lib/hogan.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/lib/template.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/package.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/html/list.html create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/index.html create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/index.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/mustache.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/Changes create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/README.md create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/Rakefile create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/TESTING.md create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/comments.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/comments.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/delimiters.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/delimiters.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/interpolation.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/interpolation.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/inverted.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/inverted.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/partials.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/partials.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/sections.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/sections.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/~lambdas.json create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/spec/specs/~lambdas.yml create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/test/templates/list.mustache create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/tools/release.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/tools/web_templates.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/1.0.0/hogan.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/1.0.0/hogan.min.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.0/hogan.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.0/hogan.min.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.3/hogan.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.3/hogan.min.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.amd.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.common.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.min.amd.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.min.common.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.min.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.min.mustache.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/hogan-1.0.5.mustache.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/template-1.0.5.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/builds/1.0.5/template-1.0.5.min.js create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/favicon.ico create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/images/logo.png create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/images/noise.png create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/images/small-hogan-icon.png create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/images/stripes.png create mode 100755 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/index.html.mustache create mode 100755 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/stylesheets/layout.css create mode 100755 services/web/public/bootstrap/docs/build/node_modules/hogan.js/web/stylesheets/skeleton.css create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/wrappers/amd.js.mustache create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/wrappers/common.js.mustache create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/wrappers/js.mustache create mode 100644 services/web/public/bootstrap/docs/build/node_modules/hogan.js/wrappers/mustache.js.mustache create mode 100644 services/web/public/bootstrap/docs/build/package.json create mode 100644 services/web/public/bootstrap/docs/components.html create mode 100644 services/web/public/bootstrap/docs/download.html create mode 100644 services/web/public/bootstrap/docs/examples.html create mode 100644 services/web/public/bootstrap/docs/examples/fluid.html create mode 100644 services/web/public/bootstrap/docs/examples/hero.html create mode 100644 services/web/public/bootstrap/docs/examples/starter-template.html create mode 100644 services/web/public/bootstrap/docs/index.html create mode 100644 services/web/public/bootstrap/docs/javascript.html create mode 100644 services/web/public/bootstrap/docs/less.html create mode 100644 services/web/public/bootstrap/docs/scaffolding.html create mode 100644 services/web/public/bootstrap/docs/templates/layout.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/base-css.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/components.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/download.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/examples.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/index.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/javascript.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/less.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/scaffolding.mustache create mode 100644 services/web/public/bootstrap/docs/templates/pages/upgrading.mustache create mode 100644 services/web/public/bootstrap/docs/upgrading.html create mode 100644 services/web/public/bootstrap/img/glyphicons-halflings-white.png create mode 100644 services/web/public/bootstrap/img/glyphicons-halflings.png create mode 100644 services/web/public/bootstrap/js/README.md create mode 100644 services/web/public/bootstrap/js/bootstrap-alert.js create mode 100644 services/web/public/bootstrap/js/bootstrap-button.js create mode 100644 services/web/public/bootstrap/js/bootstrap-carousel.js create mode 100644 services/web/public/bootstrap/js/bootstrap-collapse.js create mode 100644 services/web/public/bootstrap/js/bootstrap-dropdown.js create mode 100644 services/web/public/bootstrap/js/bootstrap-modal.js create mode 100644 services/web/public/bootstrap/js/bootstrap-popover.js create mode 100644 services/web/public/bootstrap/js/bootstrap-scrollspy.js create mode 100644 services/web/public/bootstrap/js/bootstrap-tab.js create mode 100644 services/web/public/bootstrap/js/bootstrap-tooltip.js create mode 100644 services/web/public/bootstrap/js/bootstrap-transition.js create mode 100644 services/web/public/bootstrap/js/bootstrap-typeahead.js create mode 100644 services/web/public/bootstrap/js/tests/index.html create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-alert.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-button.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-collapse.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-dropdown.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-modal.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-popover.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-scrollspy.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-tab.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-tooltip.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-transition.js create mode 100644 services/web/public/bootstrap/js/tests/unit/bootstrap-typeahead.js create mode 100644 services/web/public/bootstrap/js/tests/vendor/jquery.js create mode 100644 services/web/public/bootstrap/js/tests/vendor/qunit.css create mode 100644 services/web/public/bootstrap/js/tests/vendor/qunit.js create mode 100644 services/web/public/bootstrap/less/accordion.less create mode 100644 services/web/public/bootstrap/less/alerts.less create mode 100644 services/web/public/bootstrap/less/bootstrap.less create mode 100644 services/web/public/bootstrap/less/breadcrumbs.less create mode 100644 services/web/public/bootstrap/less/button-groups.less create mode 100644 services/web/public/bootstrap/less/buttons.less create mode 100644 services/web/public/bootstrap/less/carousel.less create mode 100644 services/web/public/bootstrap/less/close.less create mode 100644 services/web/public/bootstrap/less/code.less create mode 100644 services/web/public/bootstrap/less/component-animations.less create mode 100644 services/web/public/bootstrap/less/dropdowns.less create mode 100644 services/web/public/bootstrap/less/forms.less create mode 100644 services/web/public/bootstrap/less/grid.less create mode 100644 services/web/public/bootstrap/less/hero-unit.less create mode 100755 services/web/public/bootstrap/less/labels-badges.less create mode 100644 services/web/public/bootstrap/less/labels.less create mode 100644 services/web/public/bootstrap/less/layouts.less create mode 100644 services/web/public/bootstrap/less/mixins.less create mode 100644 services/web/public/bootstrap/less/modals.less create mode 100644 services/web/public/bootstrap/less/navbar.less create mode 100644 services/web/public/bootstrap/less/navs.less create mode 100644 services/web/public/bootstrap/less/pager.less create mode 100644 services/web/public/bootstrap/less/pagination.less create mode 100644 services/web/public/bootstrap/less/popovers.less create mode 100644 services/web/public/bootstrap/less/progress-bars.less create mode 100644 services/web/public/bootstrap/less/reset.less create mode 100644 services/web/public/bootstrap/less/responsive.less create mode 100644 services/web/public/bootstrap/less/scaffolding.less create mode 100644 services/web/public/bootstrap/less/sprites.less create mode 100644 services/web/public/bootstrap/less/tables.less create mode 100644 services/web/public/bootstrap/less/thumbnails.less create mode 100644 services/web/public/bootstrap/less/tooltip.less create mode 100644 services/web/public/bootstrap/less/type.less create mode 100644 services/web/public/bootstrap/less/utilities.less create mode 100644 services/web/public/bootstrap/less/variables.less create mode 100644 services/web/public/bootstrap/less/wells.less create mode 100644 services/web/public/coffee/SubscriptionGroupsManager.coffee create mode 100644 services/web/public/coffee/ab_testing.coffee create mode 100644 services/web/public/coffee/account/AccountManager.coffee create mode 100644 services/web/public/coffee/admin.coffee create mode 100644 services/web/public/coffee/analytics/AnalyticsManager.coffee create mode 100644 services/web/public/coffee/auto-complete/AutoCompleteManager.coffee create mode 100644 services/web/public/coffee/auto-complete/MenuView.coffee create mode 100644 services/web/public/coffee/auto-complete/SuggestionManager.coffee create mode 100644 services/web/public/coffee/auto-complete/commands.coffee create mode 100644 services/web/public/coffee/cursors/CursorManager.coffee create mode 100644 services/web/public/coffee/editor/AceUpdateManager.coffee create mode 100644 services/web/public/coffee/editor/Document.coffee create mode 100644 services/web/public/coffee/editor/Editor.coffee create mode 100644 services/web/public/coffee/editor/ShareJSHeader.coffee create mode 100644 services/web/public/coffee/editor/ShareJsDoc.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/ace.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/ace.js create mode 100644 services/web/public/coffee/editor/sharejs/client/cm.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/connection.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/doc.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/doc.js create mode 100644 services/web/public/coffee/editor/sharejs/client/index.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/microevent.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/microevent.js create mode 100644 services/web/public/coffee/editor/sharejs/client/textarea.coffee create mode 100644 services/web/public/coffee/editor/sharejs/client/web-prelude.coffee create mode 100644 services/web/public/coffee/editor/sharejs/index.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/browserchannel.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/db/couchdb.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/db/index.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/db/pg.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/db/redis.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/index.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/model.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/rest.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/socketio.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/syncqueue.coffee create mode 100644 services/web/public/coffee/editor/sharejs/server/useragent.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/README.md create mode 100644 services/web/public/coffee/editor/sharejs/types/count.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/helpers.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/index.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/json-api.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/json.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/simple.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text-api.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text-api.js create mode 100644 services/web/public/coffee/editor/sharejs/types/text-composable-api.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text-composable.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text-tp2-api.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text-tp2.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text.coffee create mode 100644 services/web/public/coffee/editor/sharejs/types/text.js create mode 100644 services/web/public/coffee/editor/sharejs/types/web-prelude.coffee create mode 100644 services/web/public/coffee/event_tracking.coffee create mode 100644 services/web/public/coffee/file-tree/DocView.coffee create mode 100644 services/web/public/coffee/file-tree/EntityView.coffee create mode 100644 services/web/public/coffee/file-tree/FileTreeManager.coffee create mode 100644 services/web/public/coffee/file-tree/FileTreeView.coffee create mode 100644 services/web/public/coffee/file-tree/FileView.coffee create mode 100644 services/web/public/coffee/file-tree/FolderView.coffee create mode 100644 services/web/public/coffee/file-tree/RootFolderView.coffee create mode 100644 services/web/public/coffee/file-view/FileView.coffee create mode 100644 services/web/public/coffee/file-view/FileViewManager.coffee create mode 100644 services/web/public/coffee/forms.coffee create mode 100644 services/web/public/coffee/gui.coffee create mode 100644 services/web/public/coffee/help/HelpManager.coffee create mode 100644 services/web/public/coffee/history/FileDiff.coffee create mode 100644 services/web/public/coffee/history/FileDiffView.coffee create mode 100644 services/web/public/coffee/history/HistoryManager.coffee create mode 100644 services/web/public/coffee/history/HistoryView.coffee create mode 100644 services/web/public/coffee/history/Version.coffee create mode 100644 services/web/public/coffee/history/VersionDetailView.coffee create mode 100644 services/web/public/coffee/history/VersionList.coffee create mode 100644 services/web/public/coffee/history/VersionListView.coffee create mode 100644 services/web/public/coffee/history/util.coffee create mode 100644 services/web/public/coffee/home.coffee create mode 100644 services/web/public/coffee/ide.coffee create mode 100644 services/web/public/coffee/ide/ConnectionManager.coffee create mode 100644 services/web/public/coffee/ide/FileUploadManager.coffee create mode 100644 services/web/public/coffee/ide/LayoutManager.coffee create mode 100644 services/web/public/coffee/ide/MainAreaManager.coffee create mode 100644 services/web/public/coffee/ide/SideBarManager.coffee create mode 100644 services/web/public/coffee/ide/TabManager.coffee create mode 100644 services/web/public/coffee/keys/BackspaceHighjack.coffee create mode 100644 services/web/public/coffee/keys/HotkeysManager.coffee create mode 100644 services/web/public/coffee/list.coffee create mode 100644 services/web/public/coffee/main.coffee create mode 100644 services/web/public/coffee/messages/MessageManager.coffee create mode 100644 services/web/public/coffee/models/Doc.coffee create mode 100644 services/web/public/coffee/models/File.coffee create mode 100644 services/web/public/coffee/models/Folder.coffee create mode 100644 services/web/public/coffee/models/FolderChildren.coffee create mode 100644 services/web/public/coffee/models/Project.coffee create mode 100644 services/web/public/coffee/models/ProjectMemberList.coffee create mode 100644 services/web/public/coffee/models/User.coffee create mode 100644 services/web/public/coffee/pdf/CompiledView.coffee create mode 100644 services/web/public/coffee/pdf/NativePdfView.coffee create mode 100644 services/web/public/coffee/pdf/PDFjsView.coffee create mode 100644 services/web/public/coffee/pdf/PdfManager.coffee create mode 100644 services/web/public/coffee/project-members/ProjectMembersManager.coffee create mode 100644 services/web/public/coffee/search/SearchManager.coffee create mode 100644 services/web/public/coffee/settings/DropboxSettingsManager.coffee create mode 100644 services/web/public/coffee/settings/SettingsManager.coffee create mode 100644 services/web/public/coffee/slides.coffee create mode 100644 services/web/public/coffee/spelling/HighlightedWordManager.coffee create mode 100644 services/web/public/coffee/spelling/SpellingManager.coffee create mode 100644 services/web/public/coffee/spelling/SpellingMenuView.coffee create mode 100644 services/web/public/coffee/tags.coffee create mode 100644 services/web/public/coffee/tests/unit/UndoManagerTests.coffee create mode 100644 services/web/public/coffee/tests/unit/auto-complete/SuggestionManager.coffee create mode 100644 services/web/public/coffee/tests/unit/editor/DocumentTests.coffee create mode 100644 services/web/public/coffee/tests/unit/editor/ShareJsDocTests.coffee create mode 100644 services/web/public/coffee/tests/unit/helpers.coffee create mode 100644 services/web/public/coffee/tests/unit/history/FileDiff.coffee create mode 100644 services/web/public/coffee/tests/unit/history/HistoryManager.coffee create mode 100644 services/web/public/coffee/tests/unit/history/HistoryView.coffee create mode 100644 services/web/public/coffee/tests/unit/history/VersionListView.coffee create mode 100644 services/web/public/coffee/tests/unit/modal.coffee create mode 100644 services/web/public/coffee/tests/unit/project-members.coffee create mode 100644 services/web/public/coffee/tests/unit/project.coffee create mode 100644 services/web/public/coffee/tests/unit/run.coffee create mode 100644 services/web/public/coffee/tests/unit/spelling/HighlightedWordManagerTests.coffee create mode 100644 services/web/public/coffee/tests/unit/spelling/SpellingManagerTests.coffee create mode 100644 services/web/public/coffee/tests/unit/user.coffee create mode 100644 services/web/public/coffee/tour/IdeTour.coffee create mode 100644 services/web/public/coffee/undo/UndoManager.coffee create mode 100644 services/web/public/coffee/utils/ContextMenu.coffee create mode 100644 services/web/public/coffee/utils/Effects.coffee create mode 100644 services/web/public/coffee/utils/Modal.coffee create mode 100644 services/web/public/favicon.ico create mode 100755 services/web/public/font/PTSans-webfont.eot create mode 100755 services/web/public/font/PTSans-webfont.svg create mode 100755 services/web/public/font/PTSans-webfont.ttf create mode 100755 services/web/public/font/PTSans-webfont.woff create mode 100644 services/web/public/font/cmunrb.otf create mode 100644 services/web/public/font/cmunrm.otf create mode 100644 services/web/public/font/cmuntt.otf create mode 100755 services/web/public/font/fontawesome-webfont.eot create mode 100755 services/web/public/font/fontawesome-webfont.svg create mode 100755 services/web/public/font/fontawesome-webfont.ttf create mode 100755 services/web/public/font/fontawesome-webfont.woff create mode 100644 services/web/public/humans.txt create mode 100644 services/web/public/img/about/coffeescript_logo.png create mode 100644 services/web/public/img/about/henry_oswald.jpg create mode 100644 services/web/public/img/about/james_allen.jpg create mode 100644 services/web/public/img/about/nodejs_logo.jpg create mode 100644 services/web/public/img/about/redis_logo.png create mode 100644 services/web/public/img/about/scribtex_logo.jpeg create mode 100644 services/web/public/img/add.png create mode 100644 services/web/public/img/aqua.jpg create mode 100755 services/web/public/img/arrow-up.png create mode 100644 services/web/public/img/arrow.png create mode 100644 services/web/public/img/arrow1.png create mode 100644 services/web/public/img/backward.png create mode 100644 services/web/public/img/book.png create mode 100644 services/web/public/img/clock.png create mode 100644 services/web/public/img/create-folder.png create mode 100755 services/web/public/img/created.png create mode 100644 services/web/public/img/crests/cambridge.png create mode 100644 services/web/public/img/crests/durham.png create mode 100644 services/web/public/img/crests/harvard.gif create mode 100644 services/web/public/img/crests/icl.png create mode 100644 services/web/public/img/crests/liverpool.jpg create mode 100644 services/web/public/img/crests/mit.gif create mode 100644 services/web/public/img/crests/nasa.png create mode 100644 services/web/public/img/crests/oxford.gif create mode 100644 services/web/public/img/crests/stanford.png create mode 100644 services/web/public/img/crests/tokyo.png create mode 100644 services/web/public/img/crests/toronto.gif create mode 100644 services/web/public/img/crests/yale.png create mode 100755 services/web/public/img/deleted.png create mode 100644 services/web/public/img/doc.png create mode 100644 services/web/public/img/documentation.png create mode 100644 services/web/public/img/down-arrow.png create mode 100644 services/web/public/img/dropbox/document_updated_modal.png create mode 100644 services/web/public/img/dropbox/dropbox_banner.png create mode 100644 services/web/public/img/dropbox/dropbox_banner_tall.png create mode 100644 services/web/public/img/dropbox/dropbox_logo.png create mode 100644 services/web/public/img/dropbox/dropbox_progress_bar.png create mode 100644 services/web/public/img/dropbox/history_diff.png create mode 100644 services/web/public/img/dropbox/share_dropbox_folder.png create mode 100644 services/web/public/img/faileupload.png create mode 100644 services/web/public/img/favicon.ico create mode 100644 services/web/public/img/favicon.png create mode 100644 services/web/public/img/file.png create mode 100644 services/web/public/img/fit-to-height.png create mode 100644 services/web/public/img/fit-to-width.png create mode 100644 services/web/public/img/flatview.png create mode 100644 services/web/public/img/folder-large.png create mode 100644 services/web/public/img/folder-open.png create mode 100644 services/web/public/img/folder.png create mode 100644 services/web/public/img/forward.png create mode 100644 services/web/public/img/galaxy.jpg create mode 100755 services/web/public/img/glyphicons-halflings-white.png create mode 100755 services/web/public/img/glyphicons-halflings.png create mode 100644 services/web/public/img/icons/16/code.png create mode 100644 services/web/public/img/icons/16/collaborators.png create mode 100644 services/web/public/img/icons/16/history.png create mode 100644 services/web/public/img/icons/16/projects.png create mode 100644 services/web/public/img/icons/16/settings.png create mode 100644 services/web/public/img/icons/16/subscription.png create mode 100644 services/web/public/img/icons/24/code.png create mode 100644 services/web/public/img/icons/24/collaborators.png create mode 100644 services/web/public/img/icons/24/history.png create mode 100644 services/web/public/img/icons/24/settings.png create mode 100755 services/web/public/img/keyboard.png create mode 100644 services/web/public/img/list/45.png create mode 100644 services/web/public/img/list/48.png create mode 100644 services/web/public/img/list/50.png create mode 100644 services/web/public/img/list/56.png create mode 100644 services/web/public/img/loading.gif create mode 100644 services/web/public/img/log.png create mode 100644 services/web/public/img/logo/banner-plain.png create mode 100644 services/web/public/img/logo/banner.png create mode 100644 services/web/public/img/logo/facebook-32.png create mode 100644 services/web/public/img/logo/icon_128.png create mode 100644 services/web/public/img/logo/link-32.png create mode 100644 services/web/public/img/logo/lion-128.png create mode 100644 services/web/public/img/logo/lion-32.png create mode 100644 services/web/public/img/logo/lion-64.png create mode 100644 services/web/public/img/logo/logo.gif create mode 100644 services/web/public/img/logo/logosmall.png create mode 100644 services/web/public/img/logo/mail-32.png create mode 100644 services/web/public/img/logo/main.acorn create mode 100644 services/web/public/img/logo/twitter-32.png create mode 100644 services/web/public/img/moved.png create mode 100644 services/web/public/img/nide.png create mode 100644 services/web/public/img/noise.png create mode 100644 services/web/public/img/pdf.png create mode 100644 services/web/public/img/project.png create mode 100644 services/web/public/img/project1.png create mode 100644 services/web/public/img/remove.png create mode 100644 services/web/public/img/rename.png create mode 100644 services/web/public/img/resizer.png create mode 100644 services/web/public/img/right-arrow.png create mode 100644 services/web/public/img/sad.png create mode 100644 services/web/public/img/save.png create mode 100644 services/web/public/img/screen_CS3.png create mode 100644 services/web/public/img/screenshot.png create mode 100644 services/web/public/img/screenshots/editorCode.png create mode 100644 services/web/public/img/screenshots/editorCollaborating.png create mode 100644 services/web/public/img/screenshots/editorLogs.png create mode 100644 services/web/public/img/screenshots/editorPDF.png create mode 100644 services/web/public/img/screenshots/editorThemes.png create mode 100644 services/web/public/img/screenshots/origionals/editorCode.jpg create mode 100644 services/web/public/img/screenshots/origionals/editorCode.png create mode 100644 services/web/public/img/screenshots/origionals/editorLogs.jpg create mode 100644 services/web/public/img/screenshots/origionals/editorPdf.jpg create mode 100644 services/web/public/img/screenshots/precompressed/editorCode.png create mode 100644 services/web/public/img/screenshots/precompressed/editorLogs.png create mode 100644 services/web/public/img/screenshots/precompressed/editorPDF.png create mode 100644 services/web/public/img/screenshots/precompressed/editorThemes.png create mode 100644 services/web/public/img/search.png create mode 100644 services/web/public/img/settings.png create mode 100644 services/web/public/img/share/facebook_button.png create mode 100644 services/web/public/img/share/mail.png create mode 100644 services/web/public/img/share/twitter_button.png create mode 100644 services/web/public/img/sort-asc.gif create mode 100644 services/web/public/img/sort-bg.gif create mode 100644 services/web/public/img/sort-desc.gif create mode 100644 services/web/public/img/spellcheck-underline.png create mode 100644 services/web/public/img/spin.gif create mode 100644 services/web/public/img/spin1.gif create mode 100644 services/web/public/img/splitview.png create mode 100644 services/web/public/img/textures/concrete_wall.png create mode 100644 services/web/public/img/textures/polaroid.png create mode 100644 services/web/public/img/themes/origonal/Chrome.jpg create mode 100644 services/web/public/img/themes/origonal/Monokai.jpg create mode 100644 services/web/public/img/themes/origonal/clouds.jpg create mode 100644 services/web/public/img/themes/origonal/clouds_midnight.jpg create mode 100644 services/web/public/img/themes/origonal/cobalt.jpg create mode 100644 services/web/public/img/themes/origonal/crimson_editor.jpg create mode 100644 services/web/public/img/themes/origonal/dawn.jpg create mode 100644 services/web/public/img/themes/origonal/dreamweaver.jpg create mode 100644 services/web/public/img/themes/origonal/eclipse.jpg create mode 100644 services/web/public/img/themes/origonal/ide_fingers.jpg create mode 100644 services/web/public/img/themes/origonal/kr_theme.jpg create mode 100644 services/web/public/img/themes/origonal/merbivore.jpg create mode 100644 services/web/public/img/themes/origonal/merbivore_soft.jpg create mode 100644 services/web/public/img/themes/origonal/mono_industrial.jpg create mode 100644 services/web/public/img/themes/origonal/pastles_on_dark.jpg create mode 100644 services/web/public/img/themes/origonal/solarized_dark.jpg create mode 100644 services/web/public/img/themes/origonal/solarized_light.jpg create mode 100644 services/web/public/img/themes/origonal/tomorrow.jpg create mode 100644 services/web/public/img/themes/origonal/tomorrow_night.jpg create mode 100644 services/web/public/img/themes/origonal/tomorrow_night_blue.jpg create mode 100644 services/web/public/img/themes/origonal/tomorrow_night_bright.jpg create mode 100644 services/web/public/img/themes/origonal/tomorrow_night_eightys.jpg create mode 100644 services/web/public/img/themes/origonal/twilight.jpg create mode 100644 services/web/public/img/themes/origonal/vibrant_ink.jpg create mode 100644 services/web/public/img/txture.png create mode 100644 services/web/public/img/upload-file.png create mode 100644 services/web/public/img/upwards-sweeping-arrow.png create mode 100644 services/web/public/img/user.png create mode 100644 services/web/public/img/users/3.png create mode 100644 services/web/public/img/users/numbers.png create mode 100644 services/web/public/img/users/numbers/1.png create mode 100644 services/web/public/img/users/numbers/2.png create mode 100644 services/web/public/img/users/user1.png create mode 100644 services/web/public/img/users/user2.png create mode 100644 services/web/public/img/washi.png create mode 100644 services/web/public/img/zoom-in.png create mode 100644 services/web/public/img/zoom-out.png create mode 100755 services/web/public/js/ace/ace.js create mode 100755 services/web/public/js/ace/anchor.js create mode 100755 services/web/public/js/ace/anchor_test.js create mode 100755 services/web/public/js/ace/autocomplete.js create mode 100755 services/web/public/js/ace/autocomplete/popup.js create mode 100755 services/web/public/js/ace/autocomplete/text_completer.js create mode 100755 services/web/public/js/ace/autocomplete/util.js create mode 100755 services/web/public/js/ace/background_tokenizer.js create mode 100755 services/web/public/js/ace/background_tokenizer_test.js create mode 100755 services/web/public/js/ace/commands/command_manager.js create mode 100755 services/web/public/js/ace/commands/command_manager_test.js create mode 100755 services/web/public/js/ace/commands/default_commands.js create mode 100755 services/web/public/js/ace/commands/incremental_search_commands.js create mode 100755 services/web/public/js/ace/commands/multi_select_commands.js create mode 100755 services/web/public/js/ace/commands/occur_commands.js create mode 100755 services/web/public/js/ace/config.js create mode 100755 services/web/public/js/ace/config_test.js create mode 100755 services/web/public/js/ace/css/codefolding-fold-button-states.png create mode 100755 services/web/public/js/ace/css/editor.css create mode 100755 services/web/public/js/ace/css/expand-marker.png create mode 100755 services/web/public/js/ace/document.js create mode 100755 services/web/public/js/ace/document_test.js create mode 100755 services/web/public/js/ace/edit_session.js create mode 100755 services/web/public/js/ace/edit_session/bracket_match.js create mode 100755 services/web/public/js/ace/edit_session/fold.js create mode 100755 services/web/public/js/ace/edit_session/fold_line.js create mode 100755 services/web/public/js/ace/edit_session/folding.js create mode 100755 services/web/public/js/ace/edit_session_test.js create mode 100755 services/web/public/js/ace/editor.js create mode 100755 services/web/public/js/ace/editor_change_document_test.js create mode 100755 services/web/public/js/ace/editor_highlight_selected_word_test.js create mode 100755 services/web/public/js/ace/editor_navigation_test.js create mode 100755 services/web/public/js/ace/editor_text_edit_test.js create mode 100755 services/web/public/js/ace/ext/chromevox.js create mode 100755 services/web/public/js/ace/ext/elastic_tabstops_lite.js create mode 100755 services/web/public/js/ace/ext/emmet.js create mode 100755 services/web/public/js/ace/ext/keybinding_menu.js create mode 100755 services/web/public/js/ace/ext/language_tools.js create mode 100755 services/web/public/js/ace/ext/menu_tools/add_editor_menu_options.js create mode 100755 services/web/public/js/ace/ext/menu_tools/element_generator.js create mode 100755 services/web/public/js/ace/ext/menu_tools/generate_settings_menu.js create mode 100755 services/web/public/js/ace/ext/menu_tools/get_editor_keyboard_shortcuts.js create mode 100755 services/web/public/js/ace/ext/menu_tools/get_set_functions.js create mode 100755 services/web/public/js/ace/ext/menu_tools/overlay_page.js create mode 100755 services/web/public/js/ace/ext/menu_tools/settings_menu.css create mode 100755 services/web/public/js/ace/ext/modelist.js create mode 100755 services/web/public/js/ace/ext/old_ie.js create mode 100755 services/web/public/js/ace/ext/old_ie_test.js create mode 100755 services/web/public/js/ace/ext/searchbox.css create mode 100755 services/web/public/js/ace/ext/searchbox.js create mode 100755 services/web/public/js/ace/ext/settings_menu.js create mode 100755 services/web/public/js/ace/ext/spellcheck.js create mode 100755 services/web/public/js/ace/ext/split.js create mode 100755 services/web/public/js/ace/ext/static.css create mode 100755 services/web/public/js/ace/ext/static_highlight.js create mode 100755 services/web/public/js/ace/ext/static_highlight_test.js create mode 100755 services/web/public/js/ace/ext/statusbar.js create mode 100755 services/web/public/js/ace/ext/textarea.js create mode 100755 services/web/public/js/ace/ext/themelist.js create mode 100755 services/web/public/js/ace/ext/whitespace.js create mode 100755 services/web/public/js/ace/incremental_search.js create mode 100755 services/web/public/js/ace/incremental_search_test.js create mode 100755 services/web/public/js/ace/keyboard/emacs.js create mode 100755 services/web/public/js/ace/keyboard/emacs_test.js create mode 100755 services/web/public/js/ace/keyboard/hash_handler.js create mode 100755 services/web/public/js/ace/keyboard/keybinding.js create mode 100755 services/web/public/js/ace/keyboard/keybinding_test.js create mode 100755 services/web/public/js/ace/keyboard/state_handler.js create mode 100755 services/web/public/js/ace/keyboard/textinput.js create mode 100755 services/web/public/js/ace/keyboard/vim.js create mode 100755 services/web/public/js/ace/keyboard/vim/commands.js create mode 100755 services/web/public/js/ace/keyboard/vim/maps/aliases.js create mode 100755 services/web/public/js/ace/keyboard/vim/maps/motions.js create mode 100755 services/web/public/js/ace/keyboard/vim/maps/operators.js create mode 100755 services/web/public/js/ace/keyboard/vim/maps/util.js create mode 100755 services/web/public/js/ace/keyboard/vim/registers.js create mode 100755 services/web/public/js/ace/layer/cursor.js create mode 100755 services/web/public/js/ace/layer/gutter.js create mode 100755 services/web/public/js/ace/layer/marker.js create mode 100755 services/web/public/js/ace/layer/text.js create mode 100755 services/web/public/js/ace/layer/text_test.js create mode 100755 services/web/public/js/ace/lib/dom.js create mode 100755 services/web/public/js/ace/lib/es5-shim.js create mode 100755 services/web/public/js/ace/lib/event.js create mode 100755 services/web/public/js/ace/lib/event_emitter.js create mode 100755 services/web/public/js/ace/lib/event_emitter_test.js create mode 100755 services/web/public/js/ace/lib/fixoldbrowsers.js create mode 100755 services/web/public/js/ace/lib/keys.js create mode 100755 services/web/public/js/ace/lib/lang.js create mode 100755 services/web/public/js/ace/lib/net.js create mode 100755 services/web/public/js/ace/lib/oop.js create mode 100755 services/web/public/js/ace/lib/regexp.js create mode 100755 services/web/public/js/ace/lib/useragent.js create mode 100755 services/web/public/js/ace/line_widgets.js create mode 100755 services/web/public/js/ace/mode/_test/Readme.md create mode 100755 services/web/public/js/ace/mode/_test/highlight_rules_test.js create mode 100755 services/web/public/js/ace/mode/_test/package.json create mode 100755 services/web/public/js/ace/mode/_test/text_asciidoc.txt create mode 100755 services/web/public/js/ace/mode/_test/text_coffee.txt create mode 100755 services/web/public/js/ace/mode/_test/text_curly.txt create mode 100755 services/web/public/js/ace/mode/_test/text_html.txt create mode 100755 services/web/public/js/ace/mode/_test/text_javascript.txt create mode 100755 services/web/public/js/ace/mode/_test/text_livescript.txt create mode 100755 services/web/public/js/ace/mode/_test/text_lucene.txt create mode 100755 services/web/public/js/ace/mode/_test/text_markdown.txt create mode 100755 services/web/public/js/ace/mode/_test/text_ruby.txt create mode 100755 services/web/public/js/ace/mode/_test/text_xml.txt create mode 100755 services/web/public/js/ace/mode/_test/tokens_abap.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_actionscript.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_asciidoc.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_assembly_x86.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_autohotkey.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_batchfile.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_c9search.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_c_cpp.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_clojure.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_coffee.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_coldfusion.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_csharp.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_css.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_curly.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_dart.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_diff.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_dot.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_erlang.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_forth.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_ftl.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_glsl.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_golang.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_groovy.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_haml.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_haskell.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_haxe.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_html.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_html_ruby.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_jade.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_java.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_javascript.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_json.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_jsp.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_jsx.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_julia.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_latex.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_less.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_liquid.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_lisp.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_livescript.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_logiql.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_lsl.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_lua.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_luapage.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_lucene.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_markdown.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_mushcode.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_objectivec.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_ocaml.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_pascal.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_perl.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_pgsql.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_php.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_powershell.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_prolog.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_properties.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_python.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_r.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_rdoc.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_rhtml.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_ruby.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_rust.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_sass.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_scad.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_scala.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_scheme.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_scss.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_sh.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_snippets.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_sql.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_stylus.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_svg.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_tcl.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_tex.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_text.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_textile.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_toml.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_twig.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_typescript.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_vbscript.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_velocity.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_xml.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_xquery.json create mode 100755 services/web/public/js/ace/mode/_test/tokens_yaml.json create mode 100755 services/web/public/js/ace/mode/abap.js create mode 100755 services/web/public/js/ace/mode/abap_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/actionscript.js create mode 100755 services/web/public/js/ace/mode/actionscript_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/ada.js create mode 100755 services/web/public/js/ace/mode/ada_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/apache_conf.js create mode 100755 services/web/public/js/ace/mode/apache_conf_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/asciidoc.js create mode 100755 services/web/public/js/ace/mode/asciidoc_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/assembly_x86.js create mode 100755 services/web/public/js/ace/mode/assembly_x86_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/autohotkey.js create mode 100755 services/web/public/js/ace/mode/autohotkey_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/batchfile.js create mode 100755 services/web/public/js/ace/mode/batchfile_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/behaviour.js create mode 100755 services/web/public/js/ace/mode/behaviour/css.js create mode 100755 services/web/public/js/ace/mode/behaviour/cstyle.js create mode 100755 services/web/public/js/ace/mode/behaviour/html.js create mode 100755 services/web/public/js/ace/mode/behaviour/xml.js create mode 100755 services/web/public/js/ace/mode/behaviour/xquery.js create mode 100755 services/web/public/js/ace/mode/c9search.js create mode 100755 services/web/public/js/ace/mode/c9search_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/c_cpp.js create mode 100755 services/web/public/js/ace/mode/c_cpp_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/clojure.js create mode 100755 services/web/public/js/ace/mode/clojure_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/cobol.js create mode 100755 services/web/public/js/ace/mode/cobol_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/coffee.js create mode 100755 services/web/public/js/ace/mode/coffee/coffee-script.js create mode 100755 services/web/public/js/ace/mode/coffee/helpers.js create mode 100755 services/web/public/js/ace/mode/coffee/lexer.js create mode 100755 services/web/public/js/ace/mode/coffee/nodes.js create mode 100755 services/web/public/js/ace/mode/coffee/parser.js create mode 100755 services/web/public/js/ace/mode/coffee/parser_test.js create mode 100755 services/web/public/js/ace/mode/coffee/rewriter.js create mode 100755 services/web/public/js/ace/mode/coffee/scope.js create mode 100755 services/web/public/js/ace/mode/coffee_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/coffee_worker.js create mode 100755 services/web/public/js/ace/mode/coldfusion.js create mode 100755 services/web/public/js/ace/mode/coldfusion_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/coldfusion_test.js create mode 100755 services/web/public/js/ace/mode/csharp.js create mode 100755 services/web/public/js/ace/mode/csharp_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/css.js create mode 100755 services/web/public/js/ace/mode/css/csslint.js create mode 100755 services/web/public/js/ace/mode/css_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/css_test.js create mode 100755 services/web/public/js/ace/mode/css_worker.js create mode 100755 services/web/public/js/ace/mode/css_worker_test.js create mode 100755 services/web/public/js/ace/mode/curly.js create mode 100755 services/web/public/js/ace/mode/curly_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/d.js create mode 100755 services/web/public/js/ace/mode/d_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/dart.js create mode 100755 services/web/public/js/ace/mode/dart_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/diff.js create mode 100755 services/web/public/js/ace/mode/diff_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/django.js create mode 100755 services/web/public/js/ace/mode/doc_comment_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/dot.js create mode 100755 services/web/public/js/ace/mode/dot_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/ejs.js create mode 100755 services/web/public/js/ace/mode/erlang.js create mode 100755 services/web/public/js/ace/mode/erlang_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/folding/asciidoc.js create mode 100755 services/web/public/js/ace/mode/folding/c9search.js create mode 100755 services/web/public/js/ace/mode/folding/coffee.js create mode 100755 services/web/public/js/ace/mode/folding/coffee_test.js create mode 100755 services/web/public/js/ace/mode/folding/csharp.js create mode 100755 services/web/public/js/ace/mode/folding/cstyle.js create mode 100755 services/web/public/js/ace/mode/folding/cstyle_test.js create mode 100755 services/web/public/js/ace/mode/folding/diff.js create mode 100755 services/web/public/js/ace/mode/folding/fold_mode.js create mode 100755 services/web/public/js/ace/mode/folding/html.js create mode 100755 services/web/public/js/ace/mode/folding/html_test.js create mode 100755 services/web/public/js/ace/mode/folding/ini.js create mode 100755 services/web/public/js/ace/mode/folding/latex.js create mode 100755 services/web/public/js/ace/mode/folding/lua.js create mode 100755 services/web/public/js/ace/mode/folding/markdown.js create mode 100755 services/web/public/js/ace/mode/folding/mixed.js create mode 100755 services/web/public/js/ace/mode/folding/pythonic.js create mode 100755 services/web/public/js/ace/mode/folding/pythonic_test.js create mode 100755 services/web/public/js/ace/mode/folding/velocity.js create mode 100755 services/web/public/js/ace/mode/folding/xml.js create mode 100755 services/web/public/js/ace/mode/folding/xml_test.js create mode 100755 services/web/public/js/ace/mode/forth.js create mode 100755 services/web/public/js/ace/mode/forth_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/ftl.js create mode 100755 services/web/public/js/ace/mode/ftl_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/glsl.js create mode 100755 services/web/public/js/ace/mode/glsl_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/golang.js create mode 100755 services/web/public/js/ace/mode/golang_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/groovy.js create mode 100755 services/web/public/js/ace/mode/groovy_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/haml.js create mode 100755 services/web/public/js/ace/mode/haml_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/handlebars.js create mode 100755 services/web/public/js/ace/mode/handlebars_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/haskell.js create mode 100755 services/web/public/js/ace/mode/haskell_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/haxe.js create mode 100755 services/web/public/js/ace/mode/haxe_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/html.js create mode 100755 services/web/public/js/ace/mode/html_completions.js create mode 100755 services/web/public/js/ace/mode/html_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/html_ruby.js create mode 100755 services/web/public/js/ace/mode/html_ruby_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/html_test.js create mode 100755 services/web/public/js/ace/mode/ini.js create mode 100755 services/web/public/js/ace/mode/ini_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/jack.js create mode 100755 services/web/public/js/ace/mode/jack_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/jade.js create mode 100755 services/web/public/js/ace/mode/jade_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/java.js create mode 100755 services/web/public/js/ace/mode/java_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/javascript.js create mode 100755 services/web/public/js/ace/mode/javascript/jshint.js create mode 100755 services/web/public/js/ace/mode/javascript_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/javascript_test.js create mode 100755 services/web/public/js/ace/mode/javascript_worker.js create mode 100755 services/web/public/js/ace/mode/javascript_worker_test.js create mode 100755 services/web/public/js/ace/mode/js_regex_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/json.js create mode 100755 services/web/public/js/ace/mode/json/json_parse.js create mode 100755 services/web/public/js/ace/mode/json_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/json_worker.js create mode 100755 services/web/public/js/ace/mode/json_worker_test.js create mode 100755 services/web/public/js/ace/mode/jsoniq.js create mode 100755 services/web/public/js/ace/mode/jsp.js create mode 100755 services/web/public/js/ace/mode/jsp_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/jsx.js create mode 100755 services/web/public/js/ace/mode/jsx_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/julia.js create mode 100755 services/web/public/js/ace/mode/julia_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/latex.js create mode 100755 services/web/public/js/ace/mode/latex_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/less.js create mode 100755 services/web/public/js/ace/mode/less_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/liquid.js create mode 100755 services/web/public/js/ace/mode/liquid_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/lisp.js create mode 100755 services/web/public/js/ace/mode/lisp_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/livescript.js create mode 100755 services/web/public/js/ace/mode/logiql.js create mode 100755 services/web/public/js/ace/mode/logiql_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/logiql_test.js create mode 100755 services/web/public/js/ace/mode/lsl.js create mode 100755 services/web/public/js/ace/mode/lsl_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/lua.js create mode 100755 services/web/public/js/ace/mode/lua/luaparse.js create mode 100755 services/web/public/js/ace/mode/lua_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/lua_worker.js create mode 100755 services/web/public/js/ace/mode/luapage.js create mode 100755 services/web/public/js/ace/mode/luapage_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/lucene.js create mode 100755 services/web/public/js/ace/mode/lucene_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/makefile.js create mode 100755 services/web/public/js/ace/mode/makefile_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/markdown.js create mode 100755 services/web/public/js/ace/mode/markdown_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/matching_brace_outdent.js create mode 100755 services/web/public/js/ace/mode/matching_parens_outdent.js create mode 100755 services/web/public/js/ace/mode/matlab.js create mode 100755 services/web/public/js/ace/mode/matlab_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/mushcode.js create mode 100755 services/web/public/js/ace/mode/mushcode_high_rules.js create mode 100755 services/web/public/js/ace/mode/mysql.js create mode 100755 services/web/public/js/ace/mode/mysql_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/nix.js create mode 100755 services/web/public/js/ace/mode/nix_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/objectivec.js create mode 100755 services/web/public/js/ace/mode/objectivec_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/ocaml.js create mode 100755 services/web/public/js/ace/mode/ocaml_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/pascal.js create mode 100755 services/web/public/js/ace/mode/pascal_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/perl.js create mode 100755 services/web/public/js/ace/mode/perl_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/pgsql.js create mode 100755 services/web/public/js/ace/mode/pgsql_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/php.js create mode 100755 services/web/public/js/ace/mode/php/php.js create mode 100755 services/web/public/js/ace/mode/php_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/php_worker.js create mode 100755 services/web/public/js/ace/mode/plain_text.js create mode 100755 services/web/public/js/ace/mode/plain_text_test.js create mode 100755 services/web/public/js/ace/mode/powershell.js create mode 100755 services/web/public/js/ace/mode/powershell_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/prolog.js create mode 100755 services/web/public/js/ace/mode/prolog_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/properties.js create mode 100755 services/web/public/js/ace/mode/properties_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/protobuf.js create mode 100755 services/web/public/js/ace/mode/protobuf_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/python.js create mode 100755 services/web/public/js/ace/mode/python_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/python_test.js create mode 100755 services/web/public/js/ace/mode/r.js create mode 100755 services/web/public/js/ace/mode/r_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/rdoc.js create mode 100755 services/web/public/js/ace/mode/rdoc_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/rhtml.js create mode 100755 services/web/public/js/ace/mode/rhtml_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/ruby.js create mode 100755 services/web/public/js/ace/mode/ruby_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/ruby_test.js create mode 100755 services/web/public/js/ace/mode/rust.js create mode 100755 services/web/public/js/ace/mode/rust_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/sass.js create mode 100755 services/web/public/js/ace/mode/sass_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/scad.js create mode 100755 services/web/public/js/ace/mode/scad_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/scala.js create mode 100755 services/web/public/js/ace/mode/scala_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/scheme.js create mode 100755 services/web/public/js/ace/mode/scheme_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/scss.js create mode 100755 services/web/public/js/ace/mode/scss_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/sh.js create mode 100755 services/web/public/js/ace/mode/sh_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/sjs.js create mode 100755 services/web/public/js/ace/mode/sjs_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/snippets.js create mode 100755 services/web/public/js/ace/mode/soy_template.js create mode 100755 services/web/public/js/ace/mode/soy_template_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/space.js create mode 100755 services/web/public/js/ace/mode/space_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/sql.js create mode 100755 services/web/public/js/ace/mode/sql_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/stylus.js create mode 100755 services/web/public/js/ace/mode/stylus_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/svg.js create mode 100755 services/web/public/js/ace/mode/svg_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/tcl.js create mode 100755 services/web/public/js/ace/mode/tcl_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/tex.js create mode 100755 services/web/public/js/ace/mode/tex_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/text.js create mode 100755 services/web/public/js/ace/mode/text_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/text_test.js create mode 100755 services/web/public/js/ace/mode/textile.js create mode 100755 services/web/public/js/ace/mode/textile_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/toml.js create mode 100755 services/web/public/js/ace/mode/toml_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/twig.js create mode 100755 services/web/public/js/ace/mode/twig_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/typescript.js create mode 100755 services/web/public/js/ace/mode/typescript_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/vbscript.js create mode 100755 services/web/public/js/ace/mode/vbscript_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/velocity.js create mode 100755 services/web/public/js/ace/mode/velocity_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/verilog.js create mode 100755 services/web/public/js/ace/mode/verilog_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/vhdl.js create mode 100755 services/web/public/js/ace/mode/vhdl_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/xml.js create mode 100755 services/web/public/js/ace/mode/xml_highlight_rules.js create mode 100755 services/web/public/js/ace/mode/xml_test.js create mode 100755 services/web/public/js/ace/mode/xml_util.js create mode 100755 services/web/public/js/ace/mode/xquery.js create mode 100755 services/web/public/js/ace/mode/xquery/JSONParseTreeHandler.js create mode 100755 services/web/public/js/ace/mode/xquery/JSONiqLexer.js create mode 100755 services/web/public/js/ace/mode/xquery/JSONiqTokenizer.ebnf create mode 100755 services/web/public/js/ace/mode/xquery/JSONiqTokenizer.js create mode 100755 services/web/public/js/ace/mode/xquery/Readme.md create mode 100755 services/web/public/js/ace/mode/xquery/XQueryLexer.js create mode 100755 services/web/public/js/ace/mode/xquery/XQueryParser.ebnf create mode 100755 services/web/public/js/ace/mode/xquery/XQueryParser.js create mode 100755 services/web/public/js/ace/mode/xquery/XQueryTokenizer.ebnf create mode 100755 services/web/public/js/ace/mode/xquery/XQueryTokenizer.js create mode 100755 services/web/public/js/ace/mode/xquery/visitors/SemanticHighlighter.js create mode 100755 services/web/public/js/ace/mode/xquery_worker.js create mode 100755 services/web/public/js/ace/mode/yaml.js create mode 100755 services/web/public/js/ace/mode/yaml_highlight_rules.js create mode 100755 services/web/public/js/ace/model/editor.js create mode 100755 services/web/public/js/ace/mouse/default_gutter_handler.js create mode 100755 services/web/public/js/ace/mouse/default_handlers.js create mode 100755 services/web/public/js/ace/mouse/dragdrop_handler.js create mode 100755 services/web/public/js/ace/mouse/fold_handler.js create mode 100755 services/web/public/js/ace/mouse/mouse_event.js create mode 100755 services/web/public/js/ace/mouse/mouse_handler.js create mode 100755 services/web/public/js/ace/mouse/multi_select_handler.js create mode 100755 services/web/public/js/ace/multi_select.js create mode 100755 services/web/public/js/ace/multi_select_test.js create mode 100755 services/web/public/js/ace/occur.js create mode 100755 services/web/public/js/ace/occur_test.js create mode 100755 services/web/public/js/ace/placeholder.js create mode 100755 services/web/public/js/ace/placeholder_test.js create mode 100755 services/web/public/js/ace/range.js create mode 100755 services/web/public/js/ace/range_list.js create mode 100755 services/web/public/js/ace/range_list_test.js create mode 100755 services/web/public/js/ace/range_test.js create mode 100755 services/web/public/js/ace/renderloop.js create mode 100755 services/web/public/js/ace/requirejs/text.js create mode 100755 services/web/public/js/ace/scrollbar.js create mode 100755 services/web/public/js/ace/search.js create mode 100755 services/web/public/js/ace/search_highlight.js create mode 100755 services/web/public/js/ace/search_test.js create mode 100755 services/web/public/js/ace/selection.js create mode 100755 services/web/public/js/ace/selection_test.js create mode 100755 services/web/public/js/ace/snippets.js create mode 100755 services/web/public/js/ace/snippets/_.snippets create mode 100755 services/web/public/js/ace/snippets/abap.js create mode 100755 services/web/public/js/ace/snippets/abap.snippets create mode 100755 services/web/public/js/ace/snippets/actionscript.js create mode 100755 services/web/public/js/ace/snippets/actionscript.snippets create mode 100755 services/web/public/js/ace/snippets/ada.js create mode 100755 services/web/public/js/ace/snippets/ada.snippets create mode 100755 services/web/public/js/ace/snippets/apache.snippets create mode 100755 services/web/public/js/ace/snippets/apache_conf.js create mode 100755 services/web/public/js/ace/snippets/apache_conf.snippets create mode 100755 services/web/public/js/ace/snippets/asciidoc.js create mode 100755 services/web/public/js/ace/snippets/asciidoc.snippets create mode 100755 services/web/public/js/ace/snippets/assembly_x86.js create mode 100755 services/web/public/js/ace/snippets/assembly_x86.snippets create mode 100755 services/web/public/js/ace/snippets/autohotkey.js create mode 100755 services/web/public/js/ace/snippets/autohotkey.snippets create mode 100755 services/web/public/js/ace/snippets/autoit.snippets create mode 100755 services/web/public/js/ace/snippets/batchfile.js create mode 100755 services/web/public/js/ace/snippets/batchfile.snippets create mode 100755 services/web/public/js/ace/snippets/c.snippets create mode 100755 services/web/public/js/ace/snippets/c9search.js create mode 100755 services/web/public/js/ace/snippets/c9search.snippets create mode 100755 services/web/public/js/ace/snippets/c_cpp.js create mode 100755 services/web/public/js/ace/snippets/c_cpp.snippets create mode 100755 services/web/public/js/ace/snippets/chef.snippets create mode 100755 services/web/public/js/ace/snippets/clojure.js create mode 100755 services/web/public/js/ace/snippets/clojure.snippets create mode 100755 services/web/public/js/ace/snippets/cmake.snippets create mode 100755 services/web/public/js/ace/snippets/cobol.js create mode 100755 services/web/public/js/ace/snippets/cobol.snippets create mode 100755 services/web/public/js/ace/snippets/coffee.js create mode 100755 services/web/public/js/ace/snippets/coffee.snippets create mode 100755 services/web/public/js/ace/snippets/coldfusion.js create mode 100755 services/web/public/js/ace/snippets/coldfusion.snippets create mode 100755 services/web/public/js/ace/snippets/cs.snippets create mode 100755 services/web/public/js/ace/snippets/csharp.js create mode 100755 services/web/public/js/ace/snippets/csharp.snippets create mode 100755 services/web/public/js/ace/snippets/css.js create mode 100755 services/web/public/js/ace/snippets/css.snippets create mode 100755 services/web/public/js/ace/snippets/curly.js create mode 100755 services/web/public/js/ace/snippets/curly.snippets create mode 100755 services/web/public/js/ace/snippets/d.js create mode 100755 services/web/public/js/ace/snippets/d.snippets create mode 100755 services/web/public/js/ace/snippets/dart.js create mode 100755 services/web/public/js/ace/snippets/dart.snippets create mode 100755 services/web/public/js/ace/snippets/diff.js create mode 100755 services/web/public/js/ace/snippets/diff.snippets create mode 100755 services/web/public/js/ace/snippets/django.js create mode 100755 services/web/public/js/ace/snippets/django.snippets create mode 100755 services/web/public/js/ace/snippets/dot.js create mode 100755 services/web/public/js/ace/snippets/dot.snippets create mode 100755 services/web/public/js/ace/snippets/ejs.js create mode 100755 services/web/public/js/ace/snippets/ejs.snippets create mode 100755 services/web/public/js/ace/snippets/erlang.js create mode 100755 services/web/public/js/ace/snippets/erlang.snippets create mode 100755 services/web/public/js/ace/snippets/eruby.snippets create mode 100755 services/web/public/js/ace/snippets/falcon.snippets create mode 100755 services/web/public/js/ace/snippets/forth.js create mode 100755 services/web/public/js/ace/snippets/forth.snippets create mode 100755 services/web/public/js/ace/snippets/ftl.js create mode 100755 services/web/public/js/ace/snippets/ftl.snippets create mode 100755 services/web/public/js/ace/snippets/glsl.js create mode 100755 services/web/public/js/ace/snippets/glsl.snippets create mode 100755 services/web/public/js/ace/snippets/go.snippets create mode 100755 services/web/public/js/ace/snippets/golang.js create mode 100755 services/web/public/js/ace/snippets/golang.snippets create mode 100755 services/web/public/js/ace/snippets/groovy.js create mode 100755 services/web/public/js/ace/snippets/groovy.snippets create mode 100755 services/web/public/js/ace/snippets/haml.js create mode 100755 services/web/public/js/ace/snippets/haml.snippets create mode 100755 services/web/public/js/ace/snippets/handlebars.js create mode 100755 services/web/public/js/ace/snippets/handlebars.snippets create mode 100755 services/web/public/js/ace/snippets/haskell.js create mode 100755 services/web/public/js/ace/snippets/haskell.snippets create mode 100755 services/web/public/js/ace/snippets/haxe.js create mode 100755 services/web/public/js/ace/snippets/haxe.snippets create mode 100755 services/web/public/js/ace/snippets/html.js create mode 100755 services/web/public/js/ace/snippets/html.snippets create mode 100755 services/web/public/js/ace/snippets/html_ruby.js create mode 100755 services/web/public/js/ace/snippets/html_ruby.snippets create mode 100755 services/web/public/js/ace/snippets/htmldjango.snippets create mode 100755 services/web/public/js/ace/snippets/htmltornado.snippets create mode 100755 services/web/public/js/ace/snippets/ini.js create mode 100755 services/web/public/js/ace/snippets/ini.snippets create mode 100755 services/web/public/js/ace/snippets/jade.js create mode 100755 services/web/public/js/ace/snippets/jade.snippets create mode 100755 services/web/public/js/ace/snippets/java.js create mode 100755 services/web/public/js/ace/snippets/java.snippets create mode 100755 services/web/public/js/ace/snippets/javascript-jquery.snippets create mode 100755 services/web/public/js/ace/snippets/javascript.js create mode 100755 services/web/public/js/ace/snippets/javascript.snippets create mode 100755 services/web/public/js/ace/snippets/json.js create mode 100755 services/web/public/js/ace/snippets/json.snippets create mode 100755 services/web/public/js/ace/snippets/jsoniq.js create mode 100755 services/web/public/js/ace/snippets/jsoniq.snippets create mode 100755 services/web/public/js/ace/snippets/jsp.js create mode 100755 services/web/public/js/ace/snippets/jsp.snippets create mode 100755 services/web/public/js/ace/snippets/jsx.js create mode 100755 services/web/public/js/ace/snippets/jsx.snippets create mode 100755 services/web/public/js/ace/snippets/julia.js create mode 100755 services/web/public/js/ace/snippets/julia.snippets create mode 100755 services/web/public/js/ace/snippets/latex.js create mode 100755 services/web/public/js/ace/snippets/latex.snippets create mode 100755 services/web/public/js/ace/snippets/ledger.snippets create mode 100755 services/web/public/js/ace/snippets/less.js create mode 100755 services/web/public/js/ace/snippets/less.snippets create mode 100755 services/web/public/js/ace/snippets/liquid.js create mode 100755 services/web/public/js/ace/snippets/liquid.snippets create mode 100755 services/web/public/js/ace/snippets/lisp.js create mode 100755 services/web/public/js/ace/snippets/lisp.snippets create mode 100755 services/web/public/js/ace/snippets/livescript.js create mode 100755 services/web/public/js/ace/snippets/livescript.snippets create mode 100755 services/web/public/js/ace/snippets/logiql.js create mode 100755 services/web/public/js/ace/snippets/logiql.snippets create mode 100755 services/web/public/js/ace/snippets/lsl.js create mode 100755 services/web/public/js/ace/snippets/lsl.snippets create mode 100755 services/web/public/js/ace/snippets/lua.js create mode 100755 services/web/public/js/ace/snippets/lua.snippets create mode 100755 services/web/public/js/ace/snippets/luapage.js create mode 100755 services/web/public/js/ace/snippets/luapage.snippets create mode 100755 services/web/public/js/ace/snippets/lucene.js create mode 100755 services/web/public/js/ace/snippets/lucene.snippets create mode 100755 services/web/public/js/ace/snippets/makefile.js create mode 100755 services/web/public/js/ace/snippets/makefile.snippets create mode 100755 services/web/public/js/ace/snippets/mako.snippets create mode 100755 services/web/public/js/ace/snippets/markdown.js create mode 100755 services/web/public/js/ace/snippets/markdown.snippets create mode 100755 services/web/public/js/ace/snippets/matlab.js create mode 100755 services/web/public/js/ace/snippets/matlab.snippets create mode 100755 services/web/public/js/ace/snippets/mushcode.js create mode 100755 services/web/public/js/ace/snippets/mushcode.snippets create mode 100755 services/web/public/js/ace/snippets/mushcode_high_rules.js create mode 100755 services/web/public/js/ace/snippets/mushcode_high_rules.snippets create mode 100755 services/web/public/js/ace/snippets/mysql.js create mode 100755 services/web/public/js/ace/snippets/mysql.snippets create mode 100755 services/web/public/js/ace/snippets/nix.js create mode 100755 services/web/public/js/ace/snippets/nix.snippets create mode 100755 services/web/public/js/ace/snippets/objc.snippets create mode 100755 services/web/public/js/ace/snippets/objectivec.js create mode 100755 services/web/public/js/ace/snippets/objectivec.snippets create mode 100755 services/web/public/js/ace/snippets/ocaml.js create mode 100755 services/web/public/js/ace/snippets/ocaml.snippets create mode 100755 services/web/public/js/ace/snippets/pascal.js create mode 100755 services/web/public/js/ace/snippets/pascal.snippets create mode 100755 services/web/public/js/ace/snippets/perl.js create mode 100755 services/web/public/js/ace/snippets/perl.snippets create mode 100755 services/web/public/js/ace/snippets/pgsql.js create mode 100755 services/web/public/js/ace/snippets/pgsql.snippets create mode 100755 services/web/public/js/ace/snippets/php.js create mode 100755 services/web/public/js/ace/snippets/php.snippets create mode 100755 services/web/public/js/ace/snippets/powershell.js create mode 100755 services/web/public/js/ace/snippets/powershell.snippets create mode 100755 services/web/public/js/ace/snippets/prolog.js create mode 100755 services/web/public/js/ace/snippets/prolog.snippets create mode 100755 services/web/public/js/ace/snippets/properties.js create mode 100755 services/web/public/js/ace/snippets/properties.snippets create mode 100755 services/web/public/js/ace/snippets/protobuf.js create mode 100755 services/web/public/js/ace/snippets/python.js create mode 100755 services/web/public/js/ace/snippets/python.snippets create mode 100755 services/web/public/js/ace/snippets/r.js create mode 100755 services/web/public/js/ace/snippets/r.snippets create mode 100755 services/web/public/js/ace/snippets/rdoc.js create mode 100755 services/web/public/js/ace/snippets/rdoc.snippets create mode 100755 services/web/public/js/ace/snippets/rhtml.js create mode 100755 services/web/public/js/ace/snippets/rhtml.snippets create mode 100755 services/web/public/js/ace/snippets/rst.snippets create mode 100755 services/web/public/js/ace/snippets/ruby.js create mode 100755 services/web/public/js/ace/snippets/ruby.snippets create mode 100755 services/web/public/js/ace/snippets/rust.js create mode 100755 services/web/public/js/ace/snippets/rust.snippets create mode 100755 services/web/public/js/ace/snippets/sass.js create mode 100755 services/web/public/js/ace/snippets/sass.snippets create mode 100755 services/web/public/js/ace/snippets/scad.js create mode 100755 services/web/public/js/ace/snippets/scad.snippets create mode 100755 services/web/public/js/ace/snippets/scala.js create mode 100755 services/web/public/js/ace/snippets/scala.snippets create mode 100755 services/web/public/js/ace/snippets/scheme.js create mode 100755 services/web/public/js/ace/snippets/scheme.snippets create mode 100755 services/web/public/js/ace/snippets/scss.js create mode 100755 services/web/public/js/ace/snippets/scss.snippets create mode 100755 services/web/public/js/ace/snippets/sh.js create mode 100755 services/web/public/js/ace/snippets/sh.snippets create mode 100755 services/web/public/js/ace/snippets/snippets.js create mode 100755 services/web/public/js/ace/snippets/snippets.snippets create mode 100755 services/web/public/js/ace/snippets/soy_template.js create mode 100755 services/web/public/js/ace/snippets/soy_template.snippets create mode 100755 services/web/public/js/ace/snippets/sql.js create mode 100755 services/web/public/js/ace/snippets/sql.snippets create mode 100755 services/web/public/js/ace/snippets/stylus.js create mode 100755 services/web/public/js/ace/snippets/stylus.snippets create mode 100755 services/web/public/js/ace/snippets/svg.js create mode 100755 services/web/public/js/ace/snippets/svg.snippets create mode 100755 services/web/public/js/ace/snippets/tcl.js create mode 100755 services/web/public/js/ace/snippets/tcl.snippets create mode 100755 services/web/public/js/ace/snippets/tex.js create mode 100755 services/web/public/js/ace/snippets/tex.snippets create mode 100755 services/web/public/js/ace/snippets/text.js create mode 100755 services/web/public/js/ace/snippets/text.snippets create mode 100755 services/web/public/js/ace/snippets/textile.js create mode 100755 services/web/public/js/ace/snippets/textile.snippets create mode 100755 services/web/public/js/ace/snippets/tmsnippet.snippets create mode 100755 services/web/public/js/ace/snippets/toml.js create mode 100755 services/web/public/js/ace/snippets/toml.snippets create mode 100755 services/web/public/js/ace/snippets/twig.js create mode 100755 services/web/public/js/ace/snippets/twig.snippets create mode 100755 services/web/public/js/ace/snippets/typescript.js create mode 100755 services/web/public/js/ace/snippets/typescript.snippets create mode 100755 services/web/public/js/ace/snippets/vbscript.js create mode 100755 services/web/public/js/ace/snippets/vbscript.snippets create mode 100755 services/web/public/js/ace/snippets/velocity.js create mode 100755 services/web/public/js/ace/snippets/velocity.snippets create mode 100755 services/web/public/js/ace/snippets/verilog.js create mode 100755 services/web/public/js/ace/snippets/verilog.snippets create mode 100755 services/web/public/js/ace/snippets/vhdl.js create mode 100755 services/web/public/js/ace/snippets/vhdl.snippets create mode 100755 services/web/public/js/ace/snippets/xml.js create mode 100755 services/web/public/js/ace/snippets/xml.snippets create mode 100755 services/web/public/js/ace/snippets/xquery.js create mode 100755 services/web/public/js/ace/snippets/xquery.snippets create mode 100755 services/web/public/js/ace/snippets/xslt.snippets create mode 100755 services/web/public/js/ace/snippets/yaml.js create mode 100755 services/web/public/js/ace/snippets/yaml.snippets create mode 100755 services/web/public/js/ace/snippets_test.js create mode 100755 services/web/public/js/ace/split.js create mode 100755 services/web/public/js/ace/test/all.js create mode 100755 services/web/public/js/ace/test/all_browser.js create mode 100755 services/web/public/js/ace/test/assertions.js create mode 100755 services/web/public/js/ace/test/asyncjs/assert.js create mode 100755 services/web/public/js/ace/test/asyncjs/async.js create mode 100755 services/web/public/js/ace/test/asyncjs/index.js create mode 100755 services/web/public/js/ace/test/asyncjs/test.js create mode 100755 services/web/public/js/ace/test/asyncjs/utils.js create mode 100755 services/web/public/js/ace/test/benchmark.js create mode 100755 services/web/public/js/ace/test/mockdom.js create mode 100755 services/web/public/js/ace/test/mockrenderer.js create mode 100755 services/web/public/js/ace/test/tests.html create mode 100755 services/web/public/js/ace/theme/ambiance.css create mode 100755 services/web/public/js/ace/theme/ambiance.js create mode 100755 services/web/public/js/ace/theme/chaos.css create mode 100755 services/web/public/js/ace/theme/chaos.js create mode 100755 services/web/public/js/ace/theme/chrome.css create mode 100755 services/web/public/js/ace/theme/chrome.js create mode 100755 services/web/public/js/ace/theme/clouds.css create mode 100755 services/web/public/js/ace/theme/clouds.js create mode 100755 services/web/public/js/ace/theme/clouds_midnight.css create mode 100755 services/web/public/js/ace/theme/clouds_midnight.js create mode 100755 services/web/public/js/ace/theme/cobalt.css create mode 100755 services/web/public/js/ace/theme/cobalt.js create mode 100755 services/web/public/js/ace/theme/crimson_editor.css create mode 100755 services/web/public/js/ace/theme/crimson_editor.js create mode 100755 services/web/public/js/ace/theme/dawn.css create mode 100755 services/web/public/js/ace/theme/dawn.js create mode 100755 services/web/public/js/ace/theme/dreamweaver.css create mode 100755 services/web/public/js/ace/theme/dreamweaver.js create mode 100755 services/web/public/js/ace/theme/eclipse.css create mode 100755 services/web/public/js/ace/theme/eclipse.js create mode 100755 services/web/public/js/ace/theme/github.css create mode 100755 services/web/public/js/ace/theme/github.js create mode 100755 services/web/public/js/ace/theme/idle_fingers.css create mode 100755 services/web/public/js/ace/theme/idle_fingers.js create mode 100755 services/web/public/js/ace/theme/katzenmilch.css create mode 100755 services/web/public/js/ace/theme/katzenmilch.js create mode 100755 services/web/public/js/ace/theme/kr_theme.css create mode 100755 services/web/public/js/ace/theme/kr_theme.js create mode 100755 services/web/public/js/ace/theme/kuroir.css create mode 100755 services/web/public/js/ace/theme/kuroir.js create mode 100755 services/web/public/js/ace/theme/merbivore.css create mode 100755 services/web/public/js/ace/theme/merbivore.js create mode 100755 services/web/public/js/ace/theme/merbivore_soft.css create mode 100755 services/web/public/js/ace/theme/merbivore_soft.js create mode 100755 services/web/public/js/ace/theme/mono_industrial.css create mode 100755 services/web/public/js/ace/theme/mono_industrial.js create mode 100755 services/web/public/js/ace/theme/monokai.css create mode 100755 services/web/public/js/ace/theme/monokai.js create mode 100755 services/web/public/js/ace/theme/pastel_on_dark.css create mode 100755 services/web/public/js/ace/theme/pastel_on_dark.js create mode 100755 services/web/public/js/ace/theme/solarized_dark.css create mode 100755 services/web/public/js/ace/theme/solarized_dark.js create mode 100755 services/web/public/js/ace/theme/solarized_light.css create mode 100755 services/web/public/js/ace/theme/solarized_light.js create mode 100755 services/web/public/js/ace/theme/terminal.css create mode 100755 services/web/public/js/ace/theme/terminal.js create mode 100755 services/web/public/js/ace/theme/textmate.css create mode 100755 services/web/public/js/ace/theme/textmate.js create mode 100755 services/web/public/js/ace/theme/tomorrow.css create mode 100755 services/web/public/js/ace/theme/tomorrow.js create mode 100755 services/web/public/js/ace/theme/tomorrow_night.css create mode 100755 services/web/public/js/ace/theme/tomorrow_night.js create mode 100755 services/web/public/js/ace/theme/tomorrow_night_blue.css create mode 100755 services/web/public/js/ace/theme/tomorrow_night_blue.js create mode 100755 services/web/public/js/ace/theme/tomorrow_night_bright.css create mode 100755 services/web/public/js/ace/theme/tomorrow_night_bright.js create mode 100755 services/web/public/js/ace/theme/tomorrow_night_eighties.css create mode 100755 services/web/public/js/ace/theme/tomorrow_night_eighties.js create mode 100755 services/web/public/js/ace/theme/twilight.css create mode 100755 services/web/public/js/ace/theme/twilight.js create mode 100755 services/web/public/js/ace/theme/vibrant_ink.css create mode 100755 services/web/public/js/ace/theme/vibrant_ink.js create mode 100755 services/web/public/js/ace/theme/xcode.css create mode 100755 services/web/public/js/ace/theme/xcode.js create mode 100755 services/web/public/js/ace/token_iterator.js create mode 100755 services/web/public/js/ace/token_iterator_test.js create mode 100755 services/web/public/js/ace/tokenizer.js create mode 100755 services/web/public/js/ace/tokenizer_dev.js create mode 100755 services/web/public/js/ace/tokenizer_test.js create mode 100755 services/web/public/js/ace/undomanager.js create mode 100755 services/web/public/js/ace/unicode.js create mode 100755 services/web/public/js/ace/virtual_renderer.js create mode 100755 services/web/public/js/ace/virtual_renderer_test.js create mode 100755 services/web/public/js/ace/worker/mirror.js create mode 100755 services/web/public/js/ace/worker/worker.js create mode 100755 services/web/public/js/ace/worker/worker_client.js create mode 100755 services/web/public/js/ace/worker/worker_test.js create mode 100644 services/web/public/js/codeprettifyer.js create mode 100644 services/web/public/js/documentUpdater.js create mode 100644 services/web/public/js/libs/backbone.js create mode 100644 services/web/public/js/libs/bootstrap.js create mode 100644 services/web/public/js/libs/bootstrap/bootstrap2full.js create mode 100644 services/web/public/js/libs/chai.js create mode 100644 services/web/public/js/libs/codemirror.css create mode 100644 services/web/public/js/libs/compatibility.js create mode 100644 services/web/public/js/libs/fileuploader.js create mode 100644 services/web/public/js/libs/fineuploader.js create mode 100644 services/web/public/js/libs/google-code-prettify/latex.js create mode 100644 services/web/public/js/libs/google-code-prettify/prettify.js create mode 100644 services/web/public/js/libs/intro.js create mode 100644 services/web/public/js/libs/jquery-layout.js create mode 100644 services/web/public/js/libs/jquery.color.js create mode 100644 services/web/public/js/libs/jquery.dataTables.js create mode 100644 services/web/public/js/libs/jquery.js create mode 100755 services/web/public/js/libs/jquery.slides.min.js create mode 100644 services/web/public/js/libs/jquery.storage.js create mode 100644 services/web/public/js/libs/jquery.tablesorter.js create mode 100644 services/web/public/js/libs/jquery.validate.js create mode 100644 services/web/public/js/libs/latex-log-parser.js create mode 100644 services/web/public/js/libs/linkify.js create mode 100644 services/web/public/js/libs/mocha.js create mode 100755 services/web/public/js/libs/moment.js create mode 100644 services/web/public/js/libs/mustache.js create mode 100644 services/web/public/js/libs/orchard.js create mode 100644 services/web/public/js/libs/pdf.js create mode 100644 services/web/public/js/libs/pdf.worker.js create mode 100644 services/web/public/js/libs/pdfListView/.AnnotationsLayerBuilder.js.swp create mode 100644 services/web/public/js/libs/pdfListView/.PdfListView.js.swp create mode 100644 services/web/public/js/libs/pdfListView/AnnotationsLayer.css create mode 100644 services/web/public/js/libs/pdfListView/AnnotationsLayerBuilder.js create mode 100644 services/web/public/js/libs/pdfListView/PdfListView.js create mode 100644 services/web/public/js/libs/pdfListView/TextLayer.css create mode 100644 services/web/public/js/libs/pdfListView/TextLayerBuilder.js create mode 100755 services/web/public/js/libs/recurly.min.js create mode 100644 services/web/public/js/libs/require.js create mode 100644 services/web/public/js/libs/sinon.js create mode 100755 services/web/public/js/libs/tagmanager.js create mode 100644 services/web/public/js/libs/typeahead.js create mode 100755 services/web/public/js/libs/underscore.js create mode 100644 services/web/public/js/models/revisionHistoryModel.js create mode 100644 services/web/public/js/revisionHistoryModel.js create mode 100644 services/web/public/js/search/searchbox.js create mode 100644 services/web/public/js/text.js create mode 100644 services/web/public/r.js create mode 100755 services/web/public/recurly/images/check.png create mode 100755 services/web/public/recurly/images/coupon_check.png create mode 100755 services/web/public/recurly/images/coupon_checking.gif create mode 100755 services/web/public/recurly/images/coupon_invalid.png create mode 100755 services/web/public/recurly/images/coupon_valid.png create mode 100755 services/web/public/recurly/images/credit_cards/american_express.png create mode 100755 services/web/public/recurly/images/credit_cards/diners_club.png create mode 100755 services/web/public/recurly/images/credit_cards/discover.png create mode 100755 services/web/public/recurly/images/credit_cards/jcb.png create mode 100755 services/web/public/recurly/images/credit_cards/laser.png create mode 100755 services/web/public/recurly/images/credit_cards/maestro.png create mode 100755 services/web/public/recurly/images/credit_cards/mastercard.png create mode 100755 services/web/public/recurly/images/credit_cards/visa.png create mode 100755 services/web/public/recurly/images/dash.png create mode 100755 services/web/public/recurly/images/due_now.png create mode 100755 services/web/public/recurly/images/error.png create mode 100755 services/web/public/recurly/images/loading.gif create mode 100755 services/web/public/recurly/images/paypal_logo.png create mode 100755 services/web/public/recurly/images/submitting.gif create mode 100755 services/web/public/recurly/images/uncheck.png create mode 100755 services/web/public/recurly/recurly.css create mode 100755 services/web/public/recurly/recurly.styl create mode 100644 services/web/public/robots.txt create mode 100644 services/web/public/sharelatex-security.pub create mode 100755 services/web/public/stylesheets/bootstrap-select.css create mode 100644 services/web/public/stylesheets/codemirror.css create mode 100644 services/web/public/stylesheets/fileuploader.css create mode 100644 services/web/public/stylesheets/less/blog.less create mode 100644 services/web/public/stylesheets/less/bonus.less create mode 100644 services/web/public/stylesheets/less/core.less create mode 100644 services/web/public/stylesheets/less/editor.less create mode 100755 services/web/public/stylesheets/less/elements.less create mode 100644 services/web/public/stylesheets/less/faq.less create mode 100644 services/web/public/stylesheets/less/fileuploader.less create mode 100644 services/web/public/stylesheets/less/footer.less create mode 100644 services/web/public/stylesheets/less/home.less create mode 100644 services/web/public/stylesheets/less/intro.less create mode 100644 services/web/public/stylesheets/less/list.less create mode 100644 services/web/public/stylesheets/less/navbar.less create mode 100644 services/web/public/stylesheets/less/orchard.less create mode 100644 services/web/public/stylesheets/less/plans.less create mode 100644 services/web/public/stylesheets/less/prettify.less create mode 100644 services/web/public/stylesheets/less/revisions.less create mode 100644 services/web/public/stylesheets/less/style.less create mode 100644 services/web/public/stylesheets/less/subscriptions.less create mode 100755 services/web/public/stylesheets/less/tagmanager.less create mode 100644 services/web/public/stylesheets/loading.gif create mode 100644 services/web/public/stylesheets/mainStyle.less create mode 100644 services/web/public/stylesheets/mocha.css create mode 100644 services/web/public/stylesheets/orbit-1.2.3.css create mode 100644 services/web/public/stylesheets/prices.less create mode 100644 services/web/public/stylesheets/variables.less create mode 100644 services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Downloads/ProjectDownloadsControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Downloads/ProjectZipStreamManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Dropbox/DropboxHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Editor/EditorRealTimeControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Editor/EditorUpdatesControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/DocLinesComparitorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectDuplicatorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectOptionsHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectRootDocManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Referal/ReferalAllocatorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Referal/ReferalConnectTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Referal/ReferalControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Referal/ReferalHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Security/LoginRateLimiter.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionBackgroundTasksTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionLocatorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Subscription/UserFeaturesUpdaterTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Tags/TagsControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Tags/TagsHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Templates/TemplatesControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Templates/TemplatesPublisherTests.coffee create mode 100644 services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsPollingBackgroundTasksTests.coffee create mode 100644 services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee create mode 100644 services/web/test/UnitTests/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Uploads/FileSystemImportManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Uploads/FileTypeManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Uploads/ProjectUploadManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/User/UserControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/User/UserCreatorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/User/UserDeleterTests.coffee create mode 100644 services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee create mode 100644 services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Versioning/AutomaticSnapshotManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Versioning/VersioningApiControllerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Versioning/VersioningApiHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/helpers/MockClient.coffee create mode 100644 services/web/test/UnitTests/coffee/helpers/MockRequest.coffee create mode 100644 services/web/test/UnitTests/coffee/helpers/MockResponse.coffee create mode 100644 services/web/test/smoke/coffee/SmokeTests.coffee diff --git a/services/web/.gitignore b/services/web/.gitignore new file mode 100644 index 0000000000..99ed201c92 --- /dev/null +++ b/services/web/.gitignore @@ -0,0 +1,69 @@ +Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store? +ehthumbs.db +Icon? +Thumbs.db + +node_modules/* +data/* + +app.js +app/js/* +test/UnitTests/js/* +test/smoke/js/* +cookies.txt +requestQueueWorker.js +TpdsWorker.js +BackgroundJobsWorker.js + +public/js/history/versiondetail.js +!public/js/libs/* +public/js/* +!public/js/ace/* +!public/js/libs/* +public/js/editor.js +public/js/home.js +public/js/forms.js +public/js/gui.js +public/js/admin.js +public/js/history/* +public/stylesheets/mainStyle.css +public/stylesheets/plans.css +public/minjs/ + +public/js/main.js +Gemfile.lock + +*.swp +.DS_Store + + diff --git a/services/web/.npmignore b/services/web/.npmignore new file mode 100644 index 0000000000..d2716d7a38 --- /dev/null +++ b/services/web/.npmignore @@ -0,0 +1,4 @@ +node_modules +data +log +public/minjs diff --git a/services/web/BackgroundJobsWorker.coffee b/services/web/BackgroundJobsWorker.coffee new file mode 100644 index 0000000000..47ec9a3d33 --- /dev/null +++ b/services/web/BackgroundJobsWorker.coffee @@ -0,0 +1,27 @@ +settings = require('settings-sharelatex') +SubscriptionBackgroundTasks = require("./app/js/Features/Subscription/SubscriptionBackgroundTasks") +TpdsPollingBackgroundTasks = require("./app/js/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks") +AutomaticSnapshotManager = require("./app/js/Features/Versioning/AutomaticSnapshotManager") + +time = + oneHour : 60 * 60 * 1000 + fifteenMinutes : 15 * 60 * 1000 + thirtySeconds : 30 * 1000 + betweenThirtyAndFiveHundredSeconds: => + random = Math.floor(Math.random() * 500) * 1000 + if random < time.thirtySeconds + return time.betweenThirtyAndFiveHundredSeconds() + else + return random + +runPeriodically = (funcToRun, periodLength)-> + recursiveReference = -> + funcToRun -> + setTimeout recursiveReference, periodLength + setTimeout recursiveReference, 0 + +# TODO: Remove this one month after the ability to start free trials was removed +runPeriodically ((cb) -> SubscriptionBackgroundTasks.downgradeExpiredFreeTrials(cb)), time.oneHour + +runPeriodically ((cb) -> TpdsPollingBackgroundTasks.pollUsersWithDropbox(cb)), time.fifteenMinutes +runPeriodically ((cb) -> AutomaticSnapshotManager.takeAutomaticSnapshots(cb)), time.thirtySeconds diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee new file mode 100644 index 0000000000..c91e166a12 --- /dev/null +++ b/services/web/Gruntfile.coffee @@ -0,0 +1,197 @@ +fs = require "fs" + +module.exports = (grunt) -> + grunt.loadNpmTasks 'grunt-contrib-coffee' + grunt.loadNpmTasks 'grunt-contrib-less' + grunt.loadNpmTasks 'grunt-contrib-clean' + grunt.loadNpmTasks 'grunt-mocha-test' + grunt.loadNpmTasks 'grunt-available-tasks' + grunt.loadNpmTasks 'grunt-contrib-requirejs' + grunt.loadNpmTasks 'grunt-execute' + grunt.loadNpmTasks 'grunt-bunyan' + + grunt.initConfig + execute: + app: + src: "app.js" + + coffee: + app_dir: + expand: true, + flatten: false, + cwd: 'app/coffee', + src: ['**/*.coffee'], + dest: 'app/js/', + ext: '.js' + + app: + src: 'app.coffee' + dest: 'app.js' + + sharejs: + options: + join: true + files: + "public/js/libs/sharejs.js": [ + "public/coffee/editor/ShareJSHeader.coffee" + "public/coffee/editor/sharejs/types/helpers.coffee" + "public/coffee/editor/sharejs/types/text.coffee" + "public/coffee/editor/sharejs/types/text-api.coffee" + "public/coffee/editor/sharejs/types/json.coffee" + "public/coffee/editor/sharejs/types/json-api.coffee" + "public/coffee/editor/sharejs/client/microevent.coffee" + "public/coffee/editor/sharejs/client/doc.coffee" + "public/coffee/editor/sharejs/client/ace.coffee" + ] + + client: + expand: true, + flatten: false, + cwd: 'public/coffee', + src: ['**/*.coffee'], + dest: 'public/js/', + ext: '.js' + + smoke_tests: + expand: true, + flatten: false, + cwd: 'test/smoke/coffee', + src: ['**/*.coffee'], + dest: 'test/smoke/js/', + ext: '.js' + + unit_tests: + expand: true, + flatten: false, + cwd: 'test/UnitTests/coffee', + src: ['**/*.coffee'], + dest: 'test/UnitTests/js/', + ext: '.js' + + less: + app: + files: + "public/stylesheets/mainStyle.css": "public/stylesheets/mainStyle.less" + plans: + files: + "public/stylesheets/plans.css": "public/stylesheets/less/plans.less" + + requirejs: + compile: + options: + appDir: "public/js" + baseUrl: "./" + dir: "public/minjs" + inlineText: false + preserveLicenseComments: false + paths: + "underscore": "libs/underscore" + "jquery": "libs/jquery" + shim: + "libs/backbone": + deps: ["libs/underscore"] + "libs/pdfListView/PdfListView": + deps: ["libs/pdf"] + "libs/pdf": + deps: ["libs/compatibility"] + + skipDirOptimize: true + modules: [ + { + name: "main", + exclude: ["jquery"] + }, { + name: "ide", + exclude: ["jquery"] + }, { + name: "home", + exclude: ["jquery"] + }, { + name: "list", + exclude: ["jquery"] + } + ] + + clean: + app: ["app/js"] + unit_tests: ["test/UnitTests/js"] + + mochaTest: + unit: + src: ["test/UnitTests/js/#{grunt.option('feature') or '**'}/*.js"] + options: + reporter: grunt.option('reporter') or 'spec' + grep: grunt.option("grep") + smoke: + src: ['test/smoke/js/**/*.js'] + options: + reporter: grunt.option('reporter') or 'spec' + grep: grunt.option("grep") + + + availabletasks: + tasks: + options: + filter: 'exclude', + tasks: [ + 'coffee' + 'less' + 'clean' + 'mochaTest' + 'availabletasks' + 'wrap_sharejs' + 'requirejs' + 'execute' + 'bunyan' + ] + groups: + "Compile tasks": [ + "compile:server" + "compile:client" + "compile:tests" + "compile" + "compile:unit_tests" + "compile:smoke_tests" + "compile:css" + "compile:minify" + "install" + ] + "Test tasks": [ + "test:unit" + ] + "Run tasks": [ + "run" + "default" + ] + "Misc": [ + "help" + ] + + grunt.registerTask 'wrap_sharejs', 'Wrap the compiled ShareJS code for AMD module loading', () -> + content = fs.readFileSync "public/js/libs/sharejs.js" + fs.writeFileSync "public/js/libs/sharejs.js", """ + define(["ace/range"], function() { + #{content} + return window.sharejs; + }); + """ + + grunt.registerTask 'help', 'Display this help list', 'availabletasks' + + grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir'] + grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs'] + grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] + grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs'] + grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] + grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests'] + grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests'] + grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css'] + + grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile'] + + grunt.registerTask 'test:unit', 'Run the unit tests (use --grep= or --feature= for individual tests)', ['compile:server', 'compile:unit_tests', 'mochaTest:unit'] + grunt.registerTask 'test:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke'] + + grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'bunyan', 'execute'] + grunt.registerTask 'default', 'run' + diff --git a/services/web/TpdsWorker.coffee b/services/web/TpdsWorker.coffee new file mode 100644 index 0000000000..5ffc1e303d --- /dev/null +++ b/services/web/TpdsWorker.coffee @@ -0,0 +1,98 @@ +async = require('async') +request = require('request') +keys = require('./app/js/infrastructure/Keys') +settings = require('settings-sharelatex') +logger = require('logger-sharelatex') +_ = require('underscore') +childProcess = require("child_process") +metrics = require("./app/js/infrastructure/Metrics") + +fiveMinutes = 5 * 60 * 1000 + + +processingFuncs = + + sendDoc : (options, callback)-> + if !options.docLines? || options.docLines.length == 0 + logger.err options:options, "doc lines not added to options for processing" + return callback() + docLines = options.docLines.reduce (singleLine, line)-> "#{singleLine}\n#{line}" + post = request(options) + post.on 'error', (err)-> + if err? + callback(err) + else + callback() + post.on 'end', callback + post.write(docLines, 'utf-8') + + standardHttpRequest: (options, callback)-> + request options, (err, reponse, body)-> + if err? + callback(err) + else + callback() + + pipeStreamFrom: (options, callback)-> + if options.filePath == "/droppy/main.tex" + request options.streamOrigin, (err,res, body)-> + logger.log options:options, body:body + origin = request(options.streamOrigin) + origin.on 'error', (err)-> + logger.error err:err, options:options, "something went wrong in pipeStreamFrom origin" + if err? + callback(err) + else + callback() + dest = request(options) + origin.pipe(dest) + dest.on "error", (err)-> + logger.error err:err, options:options, "something went wrong in pipeStreamFrom dest" + if err? + callback(err) + else + callback() + dest.on 'end', callback + + +workerRegistration = (groupKey, method, options, callback)-> + callback = _.once callback + setTimeout callback, fiveMinutes + metrics.inc "tpds-worker-processing" + logger.log groupKey:groupKey, method:method, options:options, "processing http request from queue" + processingFuncs[method] options, (err)-> + if err? + logger.err err:err, user_id:groupKey, method:method, options:options, "something went wrong processing tpdsUpdateSender update" + return callback("skip-after-retry") + callback() + + +setupWebToTpdsWorkers = (queueName)-> + logger.log worker_count:worker_count, queueName:queueName, "fairy workers" + worker_count = 4 + while worker_count-- > 0 + workerQueueRef = require('fairy').connect(settings.redis.fairy).queue(queueName) + workerQueueRef.polling_interval = 100 + workerQueueRef.regist workerRegistration + + +cleanupPreviousQueues = (queueName, callback)-> + #cleanup queues then setup workers + fairy = require('fairy').connect(settings.redis.fairy) + queuePrefix = "FAIRY:QUEUED:#{queueName}:" + fairy.redis.keys "#{queuePrefix}*", (err, keys)-> + logger.log "#{keys.length} fairy queues need cleanup" + queueNames = keys.map (key)-> + key.replace queuePrefix, "" + cleanupJobs = queueNames.map (projectQueueName)-> + return (cb)-> + cleanup = childProcess.fork(__dirname + '/cleanup.js', [queueName, projectQueueName]) + cleanup.on 'exit', cb + async.series cleanupJobs, callback + + +cleanupPreviousQueues keys.queue.web_to_tpds_http_requests, -> + setupWebToTpdsWorkers keys.queue.web_to_tpds_http_requests + +cleanupPreviousQueues keys.queue.tpds_to_web_http_requests, -> + setupWebToTpdsWorkers keys.queue.tpds_to_web_http_requests diff --git a/services/web/app.coffee b/services/web/app.coffee new file mode 100644 index 0000000000..74605fc27e --- /dev/null +++ b/services/web/app.coffee @@ -0,0 +1,43 @@ +Settings = require('settings-sharelatex') +logger = require 'logger-sharelatex' +logger.initialize("web-sharelatex") +logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user +logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project +Server = require("./app/js/infrastructure/Server") +BackgroundTasks = require("./app/js/infrastructure/BackgroundTasks") +Errors = require "./app/js/errors" + +argv = require("optimist") + .options("user", {alias : "u", description : "Run the server with permissions of the specified user"}) + .options("group", {alias : "g", description : "Run the server with permissions of the specified group"}) + .usage("Usage: $0") + .argv + +Server.app.use (error, req, res, next) -> + logger.error err: error + res.statusCode = error.status or 500 + if res.statusCode == 500 + res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at team@sharelatex.com") + else + res.end() + +if Settings.catchErrors + # fairy cleans then exits on an uncaughtError, but we don't want + # to exit so it doesn't need to do this. + require "fairy" + process.removeAllListeners "uncaughtException" + process.on "uncaughtException", (error) -> + logger.error err: error, "uncaughtException" + +BackgroundTasks.run() + +port = Settings.port or Settings.internal?.web?.port or 3000 +Server.server.listen port, -> + logger.info("web-sharelatex listening on port #{port}") + logger.info("#{require('http').globalAgent.maxSockets} sockets enabled") + if argv.user + process.setuid argv.user + logger.info "Running as user: #{argv.user}" + if argv.group + process.setgid argv.group + logger.info "Running as group: #{argv.group}" diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee new file mode 100644 index 0000000000..05438c15ba --- /dev/null +++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee @@ -0,0 +1,69 @@ +Settings = require 'settings-sharelatex' +if Settings.analytics?.mixpanel? + Mixpanel = require("mixpanel").init(Settings.analytics.mixpanel.token) +else + Mixpanel = null +logger = require "logger-sharelatex" +async = require 'async' + +module.exports = AnalyticsManager = + + track: (user, event, properties, callback = (error)->) -> + properties.distinct_id = @getDistinctId user + properties.mp_name_tag = user.email if user.email? + logger.log user_id: properties.distinct_id, event: event, properties: properties, "tracking event" + Mixpanel?.track event, properties + callback() + + set: (user, properties, callback = (error)->) -> + properties["$first_name"] = user.first_name if user.first_name? + properties["$last_name"] = user.last_name if user.last_name? + properties["$email"] = user.email if user.email? + Mixpanel?.people.set @getDistinctId(user), properties + callback() + + increment: (user, property, amount, callback = (error)->) -> + Mixpanel?.people.increment @getDistinctId(user), property, amount + callback() + + # TODO: Remove this one month after the ability to start free trials was removed + trackFreeTrialExpired: (user, callback = (error)->) -> + async.series [ + (callback) => @track user, "free trial expired", {}, callback + (callback) => @set user, { free_trial_expired_at: new Date() }, callback + ], callback + + trackSubscriptionStarted: (user, plan_code, callback = (error)->) -> + async.series [ + (callback) => @track user, "subscribed", plan_code: plan_code, callback + (callback) => @set user, { plan_code: plan_code, subscribed_at: new Date() }, callback + ], callback + + trackSubscriptionCancelled: (user, callback = (error)->) -> + async.series [ + (callback) => @track user, "cancelled", callback + (callback) => @set user, { cancelled_at: new Date() }, callback + ], callback + + trackLogIn: (user, callback = (error)->) -> + async.series [ + (callback) => @track user, "logged in", {}, callback + (callback) => @set user, { last_logged_id: new Date() }, callback + ], callback + + trackOpenEditor: (user, project, callback = (error)->) -> + async.series [ + (callback) => @set user, { last_opened_editor: new Date() }, callback + (callback) => @increment user, "editor_opens", 1, callback + ], callback + + trackReferral: (user, referal_source, referal_medium, callback = (error) ->) -> + async.series [ + (callback) => + @track user, "Referred another user", { source: referal_source, medium: referal_medium }, callback + (callback) => + @track user, "Referred another user via #{referal_source}", { medium: referal_medium }, callback + ], callback + + getDistinctId: (user) -> user.id || user._id || user + diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee new file mode 100644 index 0000000000..180f05738e --- /dev/null +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -0,0 +1,113 @@ +AuthenticationManager = require ("./AuthenticationManager") +LoginRateLimiter = require("../Security/LoginRateLimiter") +UserGetter = require "../User/UserGetter" +UserUpdater = require "../User/UserUpdater" +Metrics = require('../../infrastructure/Metrics') +logger = require("logger-sharelatex") +querystring = require('querystring') +Url = require("url") + +module.exports = AuthenticationController = + login: (req, res, next = (error) ->) -> + email = req.body?.email?.toLowerCase() + password = req.body?.password + redir = Url.parse(req.body?.redir or "/project").path + LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> + if !isAllowed + logger.log email:email, "too many login requests" + res.statusCode = 429 + return res.send + message: + text: 'This account has had too many login requests,
please wait 2 minutes before trying to log in again', + type: 'error' + AuthenticationManager.authenticate email: email, password, (error, user) -> + return next(error) if error? + if user? + LoginRateLimiter.recordSuccessfulLogin email + AuthenticationController._recordSuccessfulLogin user._id + AuthenticationController._establishUserSession req, user, (error) -> + return next(error) if error? + logger.log email: email, user_id: user._id.toString(), "successful log in" + res.send redir: redir + else + AuthenticationController._recordFailedLogin() + logger.log email: email, "failed log in" + res.send message: + text: 'Your email or password were incorrect. Please try again', + type: 'error' + + getAuthToken: (req, res, next = (error) ->) -> + AuthenticationController.getLoggedInUserId req, (error, user_id) -> + return next(error) if error? + AuthenticationManager.getAuthToken user_id, (error, auth_token) -> + return next(error) if error? + res.send(auth_token) + + getLoggedInUserId: (req, callback = (error, user_id) ->) -> + callback null, req.session.user._id.toString() + + getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) -> + if req.session?.user?._id? + query = req.session.user._id + else if req.query?.auth_token? and options.allow_auth_token + query = { auth_token: req.query.auth_token } + else + return callback null, null + + UserGetter.getUser query, callback + + requireLogin: (options = {allow_auth_token: false, load_from_db: false}) -> + doRequest = (req, res, next = (error) ->) -> + load_from_db = options.load_from_db + if req.query?.auth_token? and options.allow_auth_token + load_from_db = true + if load_from_db + AuthenticationController.getLoggedInUser req, { allow_auth_token: options.allow_auth_token }, (error, user) -> + return next(error) if error? + return AuthenticationController._redirectToRegisterPage(req, res) if !user? + req.user = user + return next() + else + if !req.session.user? + return AuthenticationController._redirectToRegisterPage(req, res) + else + req.user = req.session.user + return next() + + return doRequest + + _redirectToRegisterPage: (req, res) -> + logger.log url: req.url, "user not logged in so redirecting to register page" + req.query.redir = req.path + url = "/register?#{querystring.stringify(req.query)}" + res.redirect url + Metrics.inc "security.login-redirect" + + _recordSuccessfulLogin: (user_id, callback = (error) ->) -> + UserUpdater.updateUser user_id.toString(), { + $set: { "lastLoggedIn": new Date() }, + $inc: { "loginCount": 1 } + }, (error) -> + callback(error) if error? + Metrics.inc "user.login.success" + callback() + + _recordFailedLogin: (callback = (error) ->) -> + Metrics.inc "user.login.failed" + callback() + + _establishUserSession: (req, user, callback = (error) ->) -> + lightUser = + _id: user._id + first_name: user.first_name + last_name: user.last_name + isAdmin: user.isAdmin + email: user.email + referal_id: user.referal_id + req.session.user = lightUser + req.session.justLoggedIn = true + req.session.save callback + + + + diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee new file mode 100644 index 0000000000..254c5c6c41 --- /dev/null +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -0,0 +1,54 @@ +Settings = require 'settings-sharelatex' +User = require("../../models/User").User +{db, ObjectId} = require("../../infrastructure/mongojs") +crypto = require 'crypto' +bcrypt = require 'bcrypt' + +module.exports = AuthenticationManager = + authenticate: (query, password, callback = (error, user) ->) -> + # Using Mongoose for legacy reasons here. The returned User instance + # gets serialized into the session and there may be subtle differences + # between the user returned by Mongoose vs mongojs (such as default values) + User.findOne query, (error, user) => + return callback(error) if error? + if user? + if user.hashedPassword? + bcrypt.compare password, user.hashedPassword, (error, match) -> + return callback(error) if error? + if match + callback null, user + else + callback null, null + else + callback null, null + else + callback null, null + + setUserPassword: (user_id, password, callback = (error) ->) -> + bcrypt.genSalt 7, (error, salt) -> + return callback(error) if error? + bcrypt.hash password, salt, (error, hash) -> + return callback(error) if error? + db.users.update({ + _id: ObjectId(user_id.toString()) + }, { + $set: hashedPassword: hash + $unset: password: true + }, callback) + + getAuthToken: (user_id, callback = (error, auth_token) ->) -> + db.users.findOne { _id: ObjectId(user_id.toString()) }, { auth_token : true }, (error, user) => + return callback(error) if error? + return callback(new Error("user could not be found: #{user_id}")) if !user? + if user.auth_token? + callback null, user.auth_token + else + @_createSecureToken (error, auth_token) -> + db.users.update { _id: ObjectId(user_id.toString()) }, { $set : auth_token: auth_token }, (error) -> + return callback(error) if error? + callback null, auth_token + + _createSecureToken: (callback = (error, token) ->) -> + crypto.randomBytes 48, (error, buffer) -> + return callback(error) if error? + callback null, buffer.toString("hex") diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee new file mode 100644 index 0000000000..3c80b26ab9 --- /dev/null +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -0,0 +1,49 @@ +ProjectGetter = require "../Project/ProjectGetter" +ProjectHandler = require "../../handlers/ProjectHandler" + + +module.exports = CollaboratorsController = + getCollaborators: (req, res, next = (error) ->) -> + req.session.destroy() + ProjectGetter.getProject req.params.Project_id, { owner_ref: true, collaberator_refs: true, readOnly_refs: true}, (error, project) -> + return next(error) if error? + ProjectGetter.populateProjectWithUsers project, (error, project) -> + return next(error) if error? + CollaboratorsController._formatCollaborators project, (error, collaborators) -> + return next(error) if error? + res.send(JSON.stringify(collaborators)) + + removeSelfFromProject: (req, res, next = (error) ->) -> + user_id = req.session?.user?._id + if !user_id? + return next(new Error("User should be logged in")) + ProjectHandler::removeUserFromProject req.params.project_id, user_id, (error) -> + return next(error) if error? + res.redirect "/project" + + _formatCollaborators: (project, callback = (error, collaborators) ->) -> + collaborators = [] + + pushCollaborator = (user, permissions, owner) -> + collaborators.push { + id: user._id.toString() + first_name: user.first_name + last_name: user.last_name + email: user.email + permissions: permissions + owner: owner + } + + if project.owner_ref? + pushCollaborator(project.owner_ref, ["read", "write", "admin"], true) + + if project.collaberator_refs? and project.collaberator_refs.length > 0 + for user in project.collaberator_refs + pushCollaborator(user, ["read", "write"], false) + + if project.readOnly_refs? and project.readOnly_refs.length > 0 + for user in project.readOnly_refs + pushCollaborator(user, ["read"], false) + + callback null, collaborators + diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee new file mode 100644 index 0000000000..379fc5a58c --- /dev/null +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -0,0 +1,97 @@ +Path = require "path" +async = require "async" +Settings = require "settings-sharelatex" +request = require('request') +Project = require("../../models/Project").Project +logger = require "logger-sharelatex" +url = require("url") + +module.exports = ClsiManager = + sendRequest: (project_id, callback = (error, success) ->) -> + Project.findById project_id, (error, project) -> + return callback(error) if error? + ClsiManager._buildRequest project, (error, req) -> + return callback(error) if error? + logger.log project_id: project_id, "sending compile to CLSI" + ClsiManager._postToClsi project_id, req, (error, response) -> + return callback(error) if error? + logger.log project_id: project_id, response: response, "received compile response from CLSI" + callback( + null + (response?.compile?.status == "success") + ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles) + ) + + getLogLines: (project_id, callback = (error, lines) ->) -> + request "#{Settings.apis.clsi.url}/project/#{project_id}/output/output.log", (error, response, body) -> + return callback(error) if error? + callback null, body?.split("\n") or [] + + _postToClsi: (project_id, req, callback = (error, response) ->) -> + request.post { + url: "#{Settings.apis.clsi.url}/project/#{project_id}/compile" + json: req + jar: false + }, (error, response, body) -> + callback error, body + + _parseOutputFiles: (project_id, rawOutputFiles = []) -> + outputFiles = [] + for file in rawOutputFiles + outputFiles.push + path: url.parse(file.url).path.replace("/project/#{project_id}/output/", "") + type: file.type + return outputFiles + + VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] + _buildRequest: (project, callback = (error, request) ->) -> + if project.compiler not in @VALID_COMPILERS + project.compiler = "pdflatex" + + resources = [] + rootResourcePath = null + + addDoc = (basePath, doc, callback = (error) ->) -> + path = Path.join(basePath, doc.name) + resources.push + path: path + content: doc.lines.join("\n") + if doc._id.toString() == project.rootDoc_id.toString() + rootResourcePath = path + callback() + + addFile = (basePath, file, callback = (error) ->) -> + resources.push + path: Path.join(basePath, file.name) + url: "#{Settings.apis.filestore.url}/project/#{project._id}/file/#{file._id}" + modified: file.created?.getTime() + callback() + + addFolder = (basePath, folder, callback = (error) ->) -> + jobs = [] + for doc in folder.docs + do (doc) -> + jobs.push (callback) -> addDoc(basePath, doc, callback) + + for file in folder.fileRefs + do (file) -> + jobs.push (callback) -> addFile(basePath, file, callback) + + for childFolder in folder.folders + do (childFolder) -> + jobs.push (callback) -> addFolder(Path.join(basePath, childFolder.name), childFolder, callback) + + async.series jobs, callback + + addFolder "", project.rootFolder[0], (error) -> + if !rootResourcePath? + callback new Error("no root document exists") + else + callback null, { + compile: + options: + compiler: project.compiler + rootResourcePath: rootResourcePath + resources: resources + } + diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee new file mode 100644 index 0000000000..44f538771b --- /dev/null +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -0,0 +1,45 @@ +Metrics = require "../../infrastructure/Metrics" +Project = require("../../models/Project").Project +CompileManager = require("./CompileManager") +logger = require "logger-sharelatex" +request = require "request" +Settings = require "settings-sharelatex" + +module.exports = CompileController = + downloadPdf: (req, res, next = (error) ->)-> + Metrics.inc "pdf-downloads" + project_id = req.params.Project_id + Project.findById project_id, {name: 1}, (err, project)-> + res.contentType("application/pdf") + if !!req.query.popupDownload + logger.log project_id: project_id, "download pdf as popup download" + res.header('Content-Disposition', "attachment; filename=#{project.getSafeProjectName()}.pdf") + else + logger.log project_id: project_id, "download pdf to embed in browser" + res.header('Content-Disposition', "filename=#{project.getSafeProjectName()}.pdf") + CompileController.proxyToClsi("/project/#{project_id}/output/output.pdf", req, res, next) + + + compileAndDownloadPdf: (req, res, next)-> + project_id = req.params.project_id + CompileManager.compile project_id, null, {}, (err)-> + if err? + logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf" + res.send 500 + url = "/project/#{project_id}/output/output.pdf" + CompileController.proxyToClsi url, req, res, next + + + + getFileFromClsi: (req, res, next = (error) ->) -> + CompileController.proxyToClsi("/project/#{req.params.Project_id}/output/#{req.params.file}", req, res, next) + + proxyToClsi: (url, req, res, next = (error) ->) -> + logger.log url: url, "proxying to CLSI" + url = "#{Settings.apis.clsi.url}#{url}" + oneMinute = 60 * 1000 + proxy = request.get(url: url, timeout: oneMinute) + proxy.pipe(res) + proxy.on "error", (error) -> + logger.error err: error, url: url, "CLSI proxy error" + diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee new file mode 100644 index 0000000000..8a5ee7bd4f --- /dev/null +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -0,0 +1,82 @@ +Settings = require('settings-sharelatex') +redis = require('redis') +rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host) +rclient.auth(Settings.redis.web.password) +DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" +Project = require("../../models/Project").Project +ProjectRootDocManager = require "../Project/ProjectRootDocManager" +ClsiManager = require "./ClsiManager" +Metrics = require('../../infrastructure/Metrics') +logger = require("logger-sharelatex") +RateLimiter = require("ratelimiter") + + +module.exports = CompileManager = + compile: (project_id, user_id, opt = {}, _callback = (error) ->) -> + timer = new Metrics.Timer("editor.compile") + callback = (args...) -> + timer.done() + _callback(args...) + + @_checkIfAutoCompileLimitHasBeenHit opt.isAutoCompile, (err, canCompile)-> + if !canCompile + err = {rateLimitHit:true} + return callback(err) + logger.log project_id: project_id, user_id: user_id, "compiling project" + CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) -> + return callback(error) if error? + if recentlyCompiled + return callback new Error("project was recently compiled so not continuing") + + CompileManager._ensureRootDocumentIsSet project_id, (error) -> + return callback(error) if error? + DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> + return callback(error) if error? + ClsiManager.sendRequest project_id, (error, success, outputFiles) -> + return callback(error) if error? + logger.log files: outputFiles, "output files" + callback(null, success, outputFiles) + + getLogLines: (project_id, callback)-> + Metrics.inc "editor.raw-logs" + ClsiManager.getLogLines project_id, (error, logLines)-> + return callback(error) if error? + callback null, logLines + + COMPILE_DELAY: 1 # seconds + _checkIfRecentlyCompiled: (project_id, user_id, callback = (error, recentlyCompiled) ->) -> + key = "compile:#{project_id}:#{user_id}" + rclient.set key, true, "EX", @COMPILE_DELAY, "NX", (error, ok) -> + return callback(error) if error? + if ok == "OK" + return callback null, false + else + return callback null, true + + _checkIfAutoCompileLimitHasBeenHit: (isAutoCompile, callback = (err, canCompile)->)-> + if !isAutoCompile + return callback(null, true) + key = "auto_compile_rate_limit" + ten_seconds = (10 * 1000) + limit = new RateLimiter(db:rclient, id:key, max:7, duration:ten_seconds) + limit.get (err, limit)-> + Metrics.inc("compile.autocompile.rateLimitCheck") + if limit.remaining > 0 and !err? + canCompile = true + else + canCompile = false + Metrics.inc("compile.autocompile.rateLimitHit") + logger.log canCompile:canCompile, limit:limit, "checking if auto compile limit has been hit" + callback err, canCompile + + _ensureRootDocumentIsSet: (project_id, callback = (error) ->) -> + Project.findById project_id, 'rootDoc_id', (error, project)=> + return callback(error) if error? + if !project? + return callback new Error("project not found") + + if project.rootDoc_id? + callback() + else + ProjectRootDocManager.setRootDocAutomatically project_id, callback + diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee new file mode 100644 index 0000000000..e5c8186811 --- /dev/null +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -0,0 +1,141 @@ +request = require 'request' +request = request.defaults() +async = require 'async' +settings = require 'settings-sharelatex' +_ = require 'underscore' +async = require 'async' +logger = require('logger-sharelatex') +metrics = require('../../infrastructure/Metrics') +slReqIdHelper = require('soa-req-id') +redis = require('redis') +rclient = redis.createClient(settings.redis.web.port, settings.redis.web.host) +rclient.auth(settings.redis.web.password) +Project = require("../../models/Project").Project +ProjectLocator = require('../../Features/Project/ProjectLocator') + +module.exports = + + queueChange : (project_id, doc_id, change, sl_req_id, callback = ()->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + jsonChange = JSON.stringify change + rclient.rpush keys.pendingUpdates(doc_id:doc_id), jsonChange, (error)-> + return callback(error) if error? + doc_key = keys.combineProjectIdAndDocId(project_id, doc_id) + rclient.sadd keys.docsWithPendingUpdates, doc_key, (error) -> + return callback(error) if error? + rclient.publish "pending-updates", doc_key, callback + + flushProjectToMongo: (project_id, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id:project_id, sl_req_id:sl_req_id, "flushing project from document updater" + timer = new metrics.Timer("flushing.mongo.project") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}/flush" + request.post url, (error, res, body)-> + if error? + logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "error flushing project from document updater" + return callback(error) + else if res.statusCode >= 200 and res.statusCode < 300 + logger.log project_id: project_id, sl_req_id: sl_req_id, "flushed project from document updater" + return callback(null) + else + error = new Error("document updater returned a failure status code: #{res.statusCode}") + logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}" + return callback(error) + + flushProjectToMongoAndDelete: (project_id, sl_req_id, callback = ()->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id:project_id, sl_req_id:sl_req_id, "deleting project from document updater" + timer = new metrics.Timer("delete.mongo.project") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}" + request.del url, (error, res, body)-> + if error? + logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "error deleting project from document updater" + return callback(error) + else if res.statusCode >= 200 and res.statusCode < 300 + logger.log project_id: project_id, sl_req_id: sl_req_id, "deleted project from document updater" + return callback(null) + else + error = new Error("document updater returned a failure status code: #{res.statusCode}") + logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}" + return callback(error) + + deleteDoc : (project_id, doc_id, sl_req_id, callback = ()->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id:project_id, doc_id: doc_id, sl_req_id:sl_req_id, "deleting doc from document updater" + timer = new metrics.Timer("delete.mongo.doc") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}" + request.del url, (error, res, body)-> + if error? + logger.error err: error, project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "error deleting doc from document updater" + return callback(error) + else if res.statusCode >= 200 and res.statusCode < 300 + logger.log project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "deleted doc from document updater" + return callback(null) + else + error = new Error("document updater returned a failure status code: #{res.statusCode}") + logger.error err: error, project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}" + return callback(error) + + getDocument: (project_id, doc_id, fromVersion, sl_req_id, callback = (error, exists, doclines, version) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + timer = new metrics.Timer("get-document") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}" + logger.log project_id:project_id, doc_id: doc_id, sl_req_id:sl_req_id, "getting doc from document updater" + request.get url, (error, res, body)-> + timer.done() + if error? + logger.error err:error, url:url, project_id:project_id, doc_id:doc_id, "error getting doc from doc updater" + return callback(error) + if res.statusCode >= 200 and res.statusCode < 300 + logger.log project_id:project_id, doc_id:doc_id, "got doc from document document updater" + try + body = JSON.parse(body) + catch error + return callback(error) + callback null, body.lines, body.version, body.ops + else + logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" + callback new Error("doc updater returned a non-success status code: #{res.statusCode}") + + setDocument : (project_id, doc_id, docLines, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + timer = new metrics.Timer("set-document") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}" + body = + url: url + json: + lines: docLines + logger.log project_id:project_id, doc_id: doc_id, sl_req_id:sl_req_id, "setting doc in document updater" + request.post body, (error, res, body)-> + timer.done() + if error? + logger.error err:error, url:url, project_id:project_id, doc_id:doc_id, "error setting doc in doc updater" + return callback(error) + if res.statusCode >= 200 and res.statusCode < 300 + logger.log project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "set doc in document updater" + return callback(null) + else + logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" + callback new Error("doc updater returned a non-success status code: #{res.statusCode}") + + getNumberOfDocsInMemory : (callback)-> + request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)-> + try + body = JSON.parse body + catch err + logger.err err:err, "error parsing response from doc updater about the total number of docs" + callback(err, body?.total) + + + +PENDINGUPDATESKEY = "PendingUpdates" +DOCLINESKEY = "doclines" +DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates" + +keys = + pendingUpdates : (op) -> "#{PENDINGUPDATESKEY}:#{op.doc_id}" + docsWithPendingUpdates: DOCIDSWITHPENDINGUPDATES + docLines : (op) -> "#{DOCLINESKEY}:#{op.doc_id}" + combineProjectIdAndDocId: (project_id, doc_id) -> "#{project_id}:#{doc_id}" + + diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee new file mode 100644 index 0000000000..bc6fda789b --- /dev/null +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -0,0 +1,36 @@ +ProjectLocator = require "../Project/ProjectLocator" +ProjectEntityHandler = require "../Project/ProjectEntityHandler" +Errors = require "../../errors" +logger = require("logger-sharelatex") + +module.exports = + + getDocument: (req, res, next = (error) ->) -> + project_id = req.params.Project_id + doc_id = req.params.doc_id + logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)" + ProjectLocator.findElement project_id: project_id, element_id: doc_id, type: "doc", (error, doc) -> + if error? + logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" + return next(error) + res.type "json" + res.send JSON.stringify { + lines: doc.lines + } + req.session.destroy() + + + setDocument: (req, res, next = (error) ->) -> + project_id = req.params.Project_id + doc_id = req.params.doc_id + lines = req.body.lines + logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)" + ProjectEntityHandler.updateDocLines project_id, doc_id, lines, (error) -> + if error? + logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" + return next(error) + res.send 200 + req.session.destroy() + + + diff --git a/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee b/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee new file mode 100644 index 0000000000..4a817e2988 --- /dev/null +++ b/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee @@ -0,0 +1,25 @@ +logger = require "logger-sharelatex" +Metrics = require "../../infrastructure/Metrics" +Project = require("../../models/Project").Project +ProjectZipStreamManager = require "./ProjectZipStreamManager" +DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" + +module.exports = ProjectDownloadsController = + downloadProject: (req, res, next) -> + project_id = req.params.Project_id + Metrics.inc "zip-downloads" + logger.log project_id: project_id, "downloading project" + DocumentUpdaterHandler.flushProjectToMongo project_id, (error)-> + return next(error) if error? + Project.findById project_id, "name", (error, project) -> + return next(error) if error? + ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) -> + return next(error) if error? + res.header( + "Content-Disposition", + "attachment; filename=#{encodeURIComponent(project.name)}.zip" + ) + res.contentType('application/zip') + stream.pipe(res) + + diff --git a/services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee b/services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee new file mode 100644 index 0000000000..03ad2b864a --- /dev/null +++ b/services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee @@ -0,0 +1,46 @@ +archiver = require "archiver" +async = require "async" +logger = require "logger-sharelatex" +ProjectEntityHandler = require "../Project/ProjectEntityHandler" +FileStoreHandler = require("../FileStore/FileStoreHandler") + +module.exports = ProjectZipStreamManager = + createZipStreamForProject: (project_id, callback = (error, stream) ->) -> + archive = archiver("zip") + # return stream immediately before we start adding things to it + callback(null, archive) + @addAllDocsToArchive project_id, archive, (error) => + if error? + logger.error err: error, project_id: project_id, "error adding docs to zip stream" + @addAllFilesToArchive project_id, archive, (error) => + if error? + logger.error err: error, project_id: project_id, "error adding files to zip stream" + archive.finalize() + + + addAllDocsToArchive: (project_id, archive, callback = (error) ->) -> + ProjectEntityHandler.getAllDocs project_id, (error, docs) -> + return callback(error) if error? + jobs = [] + for path, doc of docs + do (path, doc) -> + path = path.slice(1) if path[0] == "/" + jobs.push (callback) -> + logger.log project_id: project_id, "Adding doc" + archive.append doc.lines.join("\n"), name: path, callback + async.series jobs, callback + + addAllFilesToArchive: (project_id, archive, callback = (error) ->) -> + ProjectEntityHandler.getAllFiles project_id, (error, files) -> + return callback(error) if error? + jobs = [] + for path, file of files + do (path, file) -> + jobs.push (callback) -> + FileStoreHandler.getFileStream project_id, file._id, {}, (error, stream) -> + if error? + logger.err err:error, project_id:project_id, file_id:file._id, "something went wrong adding file to zip archive" + return callback(err) + path = path.slice(1) if path[0] == "/" + archive.append stream, name: path, callback + async.series jobs, callback diff --git a/services/web/app/coffee/Features/Dropbox/DropboxHandler.coffee b/services/web/app/coffee/Features/Dropbox/DropboxHandler.coffee new file mode 100644 index 0000000000..eb6859f801 --- /dev/null +++ b/services/web/app/coffee/Features/Dropbox/DropboxHandler.coffee @@ -0,0 +1,81 @@ +request = require('request') +settings = require('settings-sharelatex') +logger = require('logger-sharelatex') +Project = require('../../models/Project').Project +projectEntityHandler = require '../Project/ProjectEntityHandler' +_ = require('underscore') +async = require('async') + +module.exports = + + getUserRegistrationStatus: (user_id, callback)-> + logger.log user_id:user_id, "getting dropbox registration status from tpds" + opts = + url : "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox/status" + timeout: 5000 + request.get opts, (err, response, body)-> + safelyGetResponse err, response, body, (err, body)-> + if err? + logger.err err:err, response:response, "getUserRegistrationStatus problem" + return callback err + logger.log status:body, "getting dropbox registration status for user #{user_id}" + callback err, body + + getDropboxRegisterUrl: (user_id, callback)-> + opts = + url: "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox/register" + timeout: 5000 + request.get opts, (err, response, body)-> + safelyGetResponse err, response, body, (err, body)-> + if err? + logger.err err:err, response:response, "getUserRegistrationStatus problem" + return callback err + url = "#{body.authorize_url}&oauth_callback=#{settings.siteUrl}/dropbox/completeRegistration" + logger.log user_id:user_id, url:url, "starting dropbox register" + callback err, url + + completeRegistration: (user_id, callback)-> + opts = + url: "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox/getaccesstoken" + timeout: 5000 + request.get opts, (err, response, body)=> + safelyGetResponse err, response, body, (err, body)=> + if err? + logger.err err:err, response:response, "getUserRegistrationStatus problem" + return callback err + success = body.success + logger.log user_id:user_id, success:body.success, "completing dropbox register" + if success + @flushUsersProjectToDropbox user_id + callback err, body.success + + + unlinkAccount: (user_id, callback)-> + opts = + url: "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox" + timeout: 5000 + request.del opts, (err, response, body)=> + callback(err) + + flushUsersProjectToDropbox: (user_id, callback)-> + Project.findAllUsersProjects user_id, '_id', (projects = [], collabertions = [], readOnlyProjects = [])-> + projectList = [] + projectList = projectList.concat(projects) + projectList = projectList.concat(collabertions) + projectList = projectList.concat(readOnlyProjects) + projectIds = _.pluck(projectList, "_id") + logger.log projectIds:projectIds, user_id:user_id, "flushing all a users projects to tpds" + jobs = projectIds.map (project_id)-> + return (cb)-> + projectEntityHandler.flushProjectToThirdPartyDataStore project_id, cb + async.series jobs, callback + +safelyGetResponse = (err, res, body, callback)-> + statusCode = if res? then res.statusCode else 500 + if err? or statusCode != 200 + e = new Error("something went wrong getting response from dropbox, #{err}, #{statusCode}") + logger.err err:err + callback(e, []) + else + body = JSON.parse body + callback(null, body) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee new file mode 100644 index 0000000000..fb2818b914 --- /dev/null +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -0,0 +1,245 @@ +logger = require('logger-sharelatex') +Metrics = require('../../infrastructure/Metrics') +sanitize = require('validator').sanitize +ProjectEditorHandler = require('../Project/ProjectEditorHandler') +ProjectEntityHandler = require('../Project/ProjectEntityHandler') +ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') +ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +ProjectGetter = require('../Project/ProjectGetter') +ProjectHandler = new (require('../../handlers/ProjectHandler'))() +DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +LimitationsManager = require("../Subscription/LimitationsManager") +AuthorizationManager = require("../Security/AuthorizationManager") +AutomaticSnapshotManager = require("../Versioning/AutomaticSnapshotManager") +VersioningApiHandler = require("../Versioning/VersioningApiHandler") +AnalyticsManager = require("../Analytics/AnalyticsManager") +EditorRealTimeController = require("./EditorRealTimeController") +settings = require('settings-sharelatex') +slReqIdHelper = require('soa-req-id') +tpdsPollingBackgroundTasks = require('../ThirdPartyDataStore/TpdsPollingBackgroundTasks') +async = require('async') +_ = require('underscore') +rclientPub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host) +rclientPub.auth(settings.redis.web.password) +rclientSub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host) +rclientSub.auth(settings.redis.web.password) + +module.exports = EditorController = + protocolVersion: 2 + + reportError: (client, clientError, callback = () ->) -> + client.get "project_id", (error, project_id) -> + client.get "user_id", (error, user_id) -> + logger.error err: clientError, project_id: project_id, user_id: user_id, "client error" + callback() + + joinProject: (client, user, project_id, callback) -> + logger.log user_id:user._id, project_id:project_id, "user joining project" + Metrics.inc "editor.join-project" + ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> + return callback(error) if error? + ProjectGetter.populateProjectWithUsers project, (error, project) -> + return callback(error) if error? + VersioningApiHandler.enableVersioning project, (error) -> + return callback(error) if error? + AuthorizationManager.getPrivilegeLevelForProject project, user, + (error, canAccess, privilegeLevel) -> + if error? or !canAccess + callback new Error("Not authorized") + else + AnalyticsManager.trackOpenEditor user, project + client.join(project_id) + client.set("project_id", project_id) + client.set("owner_id", project.owner_ref._id) + client.set("user_id", user._id) + client.set("first_name", user.first_name) + client.set("last_name", user.last_name) + client.set("email", user.email) + client.set("connected_time", new Date()) + client.set("signup_date", user.signUpDate) + client.set("login_count", user.loginCount) + client.set("take_snapshots", project.existsInVersioningApi) + AuthorizationManager.setPrivilegeLevelOnClient client, privilegeLevel + callback null, ProjectEditorHandler.buildProjectModelView(project), privilegeLevel, EditorController.protocolVersion + + leaveProject: (client, user) -> + self = @ + client.get "project_id", (error, project_id) -> + return if error? or !project_id? + EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientDisconnected", client.id) + logger.log user_id:user._id, project_id:project_id, "user leaving project" + self.flushProjectIfEmpty(project_id) + + joinDoc: (client, project_id, doc_id, fromVersion, callback = (error, docLines, version) ->) -> + # fromVersion is optional + if typeof fromVersion == "function" + callback = fromVersion + fromVersion = -1 + + client.get "user_id", (error, user_id) -> + logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user joining doc" + Metrics.inc "editor.join-doc" + client.join doc_id + DocumentUpdaterHandler.getDocument project_id, doc_id, fromVersion, (err, docLines, version, ops)-> + # Encode any binary bits of data so it can go via WebSockets + # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html + if docLines? + docLines = for line in docLines + if line.text? + line.text = unescape(encodeURIComponent(line.text)) + else + line = unescape(encodeURIComponent(line)) + line + callback(err, docLines, version, ops) + + leaveDoc: (client, project_id, doc_id, callback = (error) ->) -> + client.get "user_id", (error, user_id) -> + logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user leaving doc" + Metrics.inc "editor.leave-doc" + client.leave doc_id + callback() + + flushProjectIfEmpty: (project_id, callback = ->)-> + setTimeout (-> + io = require('../../infrastructure/Server').io + peopleStillInProject = io.sockets.clients(project_id).length + logger.log project_id: project_id, connectedCount: peopleStillInProject, "flushing if empty" + if peopleStillInProject == 0 + DocumentUpdaterHandler.flushProjectToMongoAndDelete(project_id) + callback() + ), 500 + + updateClientPosition: (client, cursorData, callback = (error) ->) -> + client.get "project_id", (error, project_id) -> + return callback(error) if error? + client.get "first_name", (error, first_name) -> + return callback(error) if error? + client.get "last_name", (error, last_name) -> + return callback(error) if error? + cursorData.id = client.id + if first_name? and last_name? + cursorData.name = first_name + " " + last_name + else + cursorData.name = "Anonymous" + EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData) + + addUserToProject: (project_id, email, privlages, callback = (error, collaborator_added)->)-> + email = email.toLowerCase() + LimitationsManager.isCollaboratorLimitReached project_id, (error, limit_reached) => + if error? + logger.error err:error, "error adding user to to project when checking if collaborator limit has been reached" + return callback(new Error("Something went wrong")) + + if limit_reached + callback null, false + else + ProjectHandler.addUserToProject project_id, email, privlages, (user)=> + EditorRealTimeController.emitToRoom(project_id, 'userAddedToProject', user, privlages) + callback null, true + + removeUserFromProject: (project_id, user_id, callback)-> + ProjectHandler.removeUserFromProject project_id, user_id, => + EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) + if callback? + callback() + + setCompiler : (project_id, compiler, callback = ()->)-> + ProjectOptionsHandler.setCompiler project_id, compiler, (err)-> + logger.log compiler:compiler, project_id:project_id, "setting compiler" + callback() + + setSpellCheckLanguage : (project_id, languageCode, callback = ()->)-> + ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err)-> + logger.log languageCode:languageCode, project_id:project_id, "setting languageCode for spell check" + callback() + + setDoc: (project_id, doc_id, docLines, sl_req_id, callback = (err)->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + DocumentUpdaterHandler.setDocument project_id, doc_id, docLines, (err)=> + logger.log project_id:project_id, doc_id:doc_id, "notifying users that the document has been updated" + EditorRealTimeController.emitToRoom(project_id, "entireDocUpdate", doc_id) + ProjectEntityHandler.updateDocLines project_id, doc_id, docLines, sl_req_id, (err)-> + callback(err) + + addDoc: (project_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc)->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + docName = sanitize(docName).xss() + logger.log sl_req_id:sl_req_id, "sending new doc to project #{project_id}" + Metrics.inc "editor.add-doc" + ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, sl_req_id, (err, doc, folder_id)=> + EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc) + callback(err, doc) + + addFile: (project_id, folder_id, fileName, path, sl_req_id, callback = (error, file)->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + fileName = sanitize(fileName).xss() + logger.log sl_req_id:sl_req_id, "sending new file to project #{project_id} with folderid: #{folder_id}" + Metrics.inc "editor.add-file" + ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=> + EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef) + callback(err, fileRef) + + replaceFile: (project_id, file_id, fsPath, callback)-> + ProjectEntityHandler.replaceFile project_id, file_id, fsPath, (err) -> + callback() + + addFolder: (project_id, folder_id, folderName, sl_req_id, callback = (error, folder)->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + folderName = sanitize(folderName).xss() + logger.log "sending new folder to project #{project_id}" + Metrics.inc "editor.add-folder" + ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=> + @p.notifyProjectUsersOfNewFolder project_id, folder_id, folder, (error) -> + callback error, folder + + mkdirp: (project_id, path, callback)-> + logger.log project_id:project_id, path:path, "making directories if they don't exist" + ProjectEntityHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=> + self = @ + jobs = _.map newFolders, (folder, index)-> + return (cb)-> + self.p.notifyProjectUsersOfNewFolder project_id, folder.parentFolder_id, folder, cb + async.series jobs, (err)-> + callback err, newFolders, lastFolder + + deleteEntity: (project_id, entity_id, entityType, sl_req_id, callback)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id:project_id, entity_id:entity_id, entityType:entityType, "start delete process of entity" + Metrics.inc "editor.delete-entity" + ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, => + logger.log sl_req_id: sl_req_id, project_id:project_id, entity_id:entity_id, entityType:entityType, "telling users entity has been deleted" + EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id) + if callback? + callback() + + getListOfDocPaths: (project_id, callback)-> + ProjectEntityHandler.getAllDocs project_id, (err, docs)-> + docList = _.map docs, (doc, path)-> + return {_id:doc._id, path:path.substring(1)} + callback(null, docList) + + forceResyncOfDropbox: (project_id, callback)-> + ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, callback + + notifyUsersProjectHasBeenDeletedOrRenamed: (project_id, callback)-> + EditorRealTimeController.emitToRoom(project_id, 'projectRenamedOrDeletedByExternalSource') + callback() + + getLastTimePollHappned: (callback)-> + tpdsPollingBackgroundTasks.getLastTimePollHappned callback + + updateProjectDescription: (project_id, description, callback = ->)-> + logger.log project_id:project_id, description:description, "updating project description" + ProjectDetailsHandler.setProjectDescription project_id, description, (err)-> + if err? + logger.err err:err, project_id:project_id, description:description, "something went wrong setting the project description" + return callback(err) + EditorRealTimeController.emitToRoom(project_id, 'projectDescriptionUpdated', description) + callback() + + p: + notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)-> + logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users" + EditorRealTimeController.emitToRoom(project_id, "reciveNewFolder", folder_id, folder) + callback() + diff --git a/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee b/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee new file mode 100644 index 0000000000..5b1b452c93 --- /dev/null +++ b/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee @@ -0,0 +1,31 @@ +settings = require 'settings-sharelatex' +rclientPub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host) +rclientPub.auth(settings.redis.web.password) +rclientSub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host) +rclientSub.auth(settings.redis.web.password) + +module.exports = EditorRealTimeController = + rclientPub: rclientPub + rclientSub: rclientSub + + emitToRoom: (room_id, message, payload...) -> + @rclientPub.publish "editor-events", JSON.stringify + room_id: room_id + message: message + payload: payload + + emitToAll: (message, payload...) -> + @emitToRoom "all", message, payload... + + listenForEditorEvents: () -> + @rclientSub.subscribe "editor-events" + @rclientSub.on "message", @_processEditorEvent.bind(@) + + _processEditorEvent: (channel, message) -> + io = require('../../infrastructure/Server').io + message = JSON.parse(message) + if message.room_id == "all" + io.sockets.emit(message.message, message.payload...) + else + io.sockets.in(message.room_id).emit(message.message, message.payload...) + diff --git a/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee b/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee new file mode 100644 index 0000000000..4a0162eca0 --- /dev/null +++ b/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee @@ -0,0 +1,68 @@ +logger = require "logger-sharelatex" +metrics = require('../../infrastructure/Metrics') +Settings = require 'settings-sharelatex' +rclient = require("redis").createClient(Settings.redis.web.port, Settings.redis.web.host) +rclient.auth(Settings.redis.web.password) +DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +AutomaticSnapshotManager = require("../Versioning/AutomaticSnapshotManager") +EditorRealTimeController = require("./EditorRealTimeController") + +module.exports = EditorUpdatesController = + _applyUpdate: (client, project_id, doc_id, update, callback = (error) ->) -> + metrics.inc "editor.doc-update", 0.3 + metrics.set "editor.active-projects", project_id, 0.3 + client.get "user_id", (error, user_id) -> + metrics.set "editor.active-users", user_id, 0.3 + + client.get "take_snapshots", (error, takeSnapshot) -> + if takeSnapshot + AutomaticSnapshotManager.markProjectAsUpdated(project_id) + + DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) -> + if error? + logger.error err:error, project_id: project_id, "document was not available for update" + client.disconnect() + callback(error) + + applyOtUpdate: (client, project_id, doc_id, update) -> + update.meta ||= {} + update.meta.source = client.id + client.get "user_id", (error, user_id) -> + update.meta.user_id = user_id + EditorUpdatesController._applyUpdate client, project_id, doc_id, update + + applyAceUpdate: (client, project_id, doc_id, window_name, update) -> + # This is deprecated now and should never be used. Kick the client off if they call it. + # After the initial deploy this can be removed safely + logger.err project_id: project_id, doc_id: doc_id, "client using old Ace Update method" + client.disconnect() + + listenForUpdatesFromDocumentUpdater: () -> + rclient.subscribe "applied-ops" + rclient.on "message", @_processMessageFromDocumentUpdater.bind(@) + + _processMessageFromDocumentUpdater: (channel, message) -> + message = JSON.parse message + if message.op? + @_applyUpdateFromDocumentUpdater(message.doc_id, message.op) + else if message.error? + @_processErrorFromDocumentUpdater(message.doc_id, message.error, message) + + _applyUpdateFromDocumentUpdater: (doc_id, update) -> + io = require('../../infrastructure/Server').io + for client in io.sockets.clients(doc_id) + if client.id == update.meta.source + client.emit "otUpdateApplied", v: update.v, doc: update.doc + else + client.emit "otUpdateApplied", update + + _processErrorFromDocumentUpdater: (doc_id, error, message) -> + io = require('../../infrastructure/Server').io + logger.error err: error, doc_id: doc_id, "error from document updater" + for client in io.sockets.clients(doc_id) + client.emit "otUpdateError", error, message + client.disconnect() + + + + diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee new file mode 100644 index 0000000000..ba1bb5b3dc --- /dev/null +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -0,0 +1,61 @@ +logger = require("logger-sharelatex") +fs = require("fs") +request = require("request") +settings = require("settings-sharelatex") + +module.exports = + + uploadFileFromDisk: (project_id, file_id, fsPath, callback)-> + logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk" + readStream = fs.createReadStream(fsPath) + opts = + method: "post" + uri: @_buildUrl(project_id, file_id) + writeStream = request(opts) + readStream.pipe writeStream + readStream.on "end", callback + readStream.on "error", (err)-> + logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk" + callback err + writeStream.on "error", (err)-> + logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk" + callback err + + getFileStream: (project_id, file_id, query, callback)-> + logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store" + queryString = "" + if query? and query["format"]? + queryString = "?format=#{query['format']}" + opts = + method : "get" + uri: "#{@_buildUrl(project_id, file_id)}#{queryString}" + readStream = request(opts) + callback(null, readStream) + + deleteFile: (project_id, file_id, callback)-> + logger.log project_id:project_id, file_id:file_id, "telling file store to delete file" + opts = + method : "delete" + uri: @_buildUrl(project_id, file_id) + request opts, (err, response)-> + if err? + logger.err err:err, project_id:project_id, file_id:file_id, "something went wrong deleting file from filestore" + callback(err) + + copyFile: (oldProject_id, oldFile_id, newProject_id, newFile_id, callback)-> + logger.log oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "telling filestore to copy a file" + opts = + method : "put" + json: + source: + project_id:oldProject_id + file_id:oldFile_id + uri: @_buildUrl(newProject_id, newFile_id) + + request opts, (err)-> + if err? + logger.err err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "something went wrong telling filestore api to copy file" + callback(err) + + _buildUrl: (project_id, file_id)-> + return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}" \ No newline at end of file diff --git a/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee b/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee new file mode 100644 index 0000000000..066e32ede9 --- /dev/null +++ b/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee @@ -0,0 +1,42 @@ +Mocha = require "mocha" +Base = require("mocha/lib/reporters/base") + +module.exports = HealthCheckController = + check: (req, res, next = (error) ->) -> + mocha = new Mocha(reporter: Reporter(res), timeout: 10000) + mocha.addFile("test/smoke/js/SmokeTests.js") + mocha.run () -> + path = require.resolve(__dirname + "/../../../../test/smoke/js/SmokeTests.js") + delete require.cache[path] + +Reporter = (res) -> + (runner) -> + Base.call(this, runner) + + tests = [] + passes = [] + failures = [] + + runner.on 'test end', (test) -> tests.push(test) + runner.on 'pass', (test) -> passes.push(test) + runner.on 'fail', (test) -> failures.push(test) + + runner.on 'end', () => + clean = (test) -> + title: test.fullTitle() + duration: test.duration + err: test.err + timedOut: test.timedOut + + results = { + stats: @stats + failures: failures.map(clean) + passes: passes.map(clean) + } + + res.contentType("application/json") + if failures.length > 0 + res.send 500, JSON.stringify(results, null, 2) + else + res.send 200, JSON.stringify(results, null, 2) + diff --git a/services/web/app/coffee/Features/Project/DocLinesComparitor.coffee b/services/web/app/coffee/Features/Project/DocLinesComparitor.coffee new file mode 100644 index 0000000000..4a286e66c7 --- /dev/null +++ b/services/web/app/coffee/Features/Project/DocLinesComparitor.coffee @@ -0,0 +1,10 @@ +_ = require "underscore" + +module.exports = + + areSame: (lines1, lines2)-> + if !Array.isArray(lines1) or !Array.isArray(lines2) + return false + + return _.isEqual(lines1, lines2) + diff --git a/services/web/app/coffee/Features/Project/ProjectApiController.coffee b/services/web/app/coffee/Features/Project/ProjectApiController.coffee new file mode 100644 index 0000000000..fc1b5c20a7 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectApiController.coffee @@ -0,0 +1,13 @@ +ProjectDetailsHandler = require("./ProjectDetailsHandler") +logger = require("logger-sharelatex") + + +module.exports = + + getProjectDetails : (req, res)-> + {project_id} = req.params + ProjectDetailsHandler.getDetails project_id, (err, projDetails)-> + if err? + logger.log err:err, project_id:project_id, "something went wrong getting project details" + return res.send 500 + res.json(projDetails) \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee new file mode 100644 index 0000000000..960c04ffd0 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -0,0 +1,82 @@ +logger = require('logger-sharelatex') +async = require("async") +metrics = require('../../infrastructure/Metrics') +Settings = require('settings-sharelatex') +ObjectId = require('mongoose').Types.ObjectId +Project = require('../../models/Project').Project +Folder = require('../../models/Folder').Folder +VersioningApiHandler = require('../Versioning/VersioningApiHandler') +ProjectEntityHandler = require('./ProjectEntityHandler') +User = require('../../models/User').User +fs = require('fs') +Path = require "path" +_ = require "underscore" + +module.exports = + createBlankProject : (owner_id, projectName, callback = (error, project) ->)-> + metrics.inc("project-creation") + logger.log owner_id:owner_id, projectName:projectName, "creating blank project" + rootFolder = new Folder {'name':'rootFolder'} + project = new Project + owner_ref : new ObjectId(owner_id) + name : projectName + useClsi2 : true + project.rootFolder[0] = rootFolder + User.findById owner_id, "ace.spellCheckLanguage", (err, user)-> + project.spellCheckLanguage = user.ace.spellCheckLanguage + project.save (err)-> + return callback(err) if err? + VersioningApiHandler.enableVersioning project._id, (err) -> + callback err, project + + createBasicProject : (owner_id, projectName, callback = (error, project) ->)-> + self = @ + @createBlankProject owner_id, projectName, (error, project)-> + return callback(error) if error? + self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)-> + return callback(error) if error? + ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, "", (error, doc)-> + return callback(error) if error? + ProjectEntityHandler.setRootDoc project._id, doc._id, (error) -> + callback(error, project) + + createExampleProject: (owner_id, projectName, callback = (error, project) ->)-> + self = @ + @createBlankProject owner_id, projectName, (error, project)-> + return callback(error) if error? + async.series [ + (callback) -> + self._buildTemplate "main.tex", owner_id, projectName, (error, docLines)-> + return callback(error) if error? + ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, "", (error, doc)-> + return callback(error) if error? + ProjectEntityHandler.setRootDoc project._id, doc._id, callback + (callback) -> + self._buildTemplate "references.bib", owner_id, projectName, (error, docLines)-> + return callback(error) if error? + ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, "", (error, doc)-> + callback(error) + (callback) -> + universePath = Path.resolve(__dirname + "/../../../templates/project_files/universe.jpg") + ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, callback + ], (error) -> + callback(error, project) + + _buildTemplate: (template_name, user_id, project_name, callback = (error, output) ->)-> + User.findById user_id, "first_name last_name", (error, user)-> + return callback(error) if error? + monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] + + templatePath = Path.resolve(__dirname + "/../../../templates/project_files/#{template_name}") + fs.readFile templatePath, (error, template) -> + return callback(error) if error? + data = + project_name: project_name + user: user + year: new Date().getUTCFullYear() + month: monthNames[new Date().getUTCMonth()] + output = _.template(template.toString(), data) + callback null, output.split("\n") + + + diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee new file mode 100644 index 0000000000..69b9289552 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -0,0 +1,19 @@ +Project = require('../../models/Project').Project +logger = require('logger-sharelatex') +editorController = require('../Editor/EditorController') + + +module.exports = + + markAsDeletedByExternalSource : (project_id, callback)-> + logger.log project_id:project_id, "marking project as deleted by external data source" + conditions = {_id:project_id} + update = {deletedByExternalDataSource:true} + + Project.update conditions, update, {}, (err)-> + editorController.notifyUsersProjectHasBeenDeletedOrRenamed project_id, -> + callback() + + deleteUsersProjects: (owner_id, callback)-> + logger.log owner_id:owner_id, "deleting users projects" + Project.remove owner_ref:owner_id, callback diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee new file mode 100644 index 0000000000..7e3960db23 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -0,0 +1,26 @@ +ProjectGetter = require("./ProjectGetter") +Project = require('../../models/Project').Project +logger = require("logger-sharelatex") + +module.exports = + + getDetails: (project_id, callback)-> + ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> + if err? + logger.err err:err, project_id:project_id, "error getting project" + return callback(err) + details = + name : project.name + description: project.description + compiler: project.compiler + logger.log project_id:project_id, details:details, "getting project details" + callback(err, details) + + setProjectDescription: (project_id, description, callback)-> + conditions = _id:project_id + update = description:description + logger.log conditions:conditions, update:update, project_id:project_id, description:description, "setting project description" + Project.update conditions, update, (err)-> + if err? + logger.err err:err, "something went wrong setting project description" + callback(err) diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee new file mode 100644 index 0000000000..555a786007 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee @@ -0,0 +1,53 @@ +projectCreationHandler = require('./ProjectCreationHandler') +projectEntityHandler = require('./ProjectEntityHandler') +projectLocator = require('./ProjectLocator') +projectOptionsHandler = require('./ProjectOptionsHandler') +DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler") +Project = require("../../models/Project").Project +_ = require('underscore') +async = require('async') + +module.exports = + duplicate: (owner, originalProjectId, newProjectName, callback)-> + DocumentUpdaterHandler.flushProjectToMongo originalProjectId, (err) -> + return callback(err) if err? + Project.findById originalProjectId, (err, originalProject) -> + return callback(err) if err? + projectCreationHandler.createBlankProject owner._id, newProjectName, (err, newProject)-> + return callback(err) if err? + projectLocator.findRootDoc {project:originalProject}, (err, originalRootDoc)-> + projectOptionsHandler.setCompiler newProject._id, originalProject.compiler + + setRootDoc = _.once (doc_id)-> + projectEntityHandler.setRootDoc newProject, doc_id + + copyDocs = (originalFolder, newParentFolder, callback)-> + jobs = originalFolder.docs.map (doc)-> + return (callback)-> + projectEntityHandler.addDoc newProject, newParentFolder._id, doc.name, doc.lines, (err, newDoc)-> + if originalRootDoc? and newDoc.name == originalRootDoc.name + setRootDoc newDoc._id + callback() + async.series jobs, callback + + copyFiles = (originalFolder, newParentFolder, callback)-> + jobs = originalFolder.fileRefs.map (file)-> + return (callback)-> + projectEntityHandler.copyFileFromExistingProject newProject, newParentFolder._id, originalProject._id, file, callback + async.parallelLimit jobs, 5, callback + + copyFolder = (folder, desFolder, callback)-> + jobs = folder.folders.map (childFolder)-> + return (callback)-> + projectEntityHandler.addFolder newProject, desFolder._id, childFolder.name, (err, newFolder)-> + copyFolder childFolder, newFolder, callback + jobs.push (cb)-> + copyDocs folder, desFolder, cb + jobs.push (cb)-> + copyFiles folder, desFolder, cb + + async.series jobs, callback + + copyFolder originalProject.rootFolder[0], newProject.rootFolder[0], -> + callback(err, newProject) + diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee new file mode 100644 index 0000000000..fe54138752 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -0,0 +1,63 @@ +module.exports = ProjectEditorHandler = + buildProjectModelView: (project, options) -> + options ||= {} + if !options.includeUsers? + options.includeUsers = true + + result = + _id : project._id + name : project.name + rootDoc_id : project.rootDoc_id + rootFolder : [@buildFolderModelView project.rootFolder[0]] + publicAccesLevel : project.publicAccesLevel + versioningVisible : !!project.existsInVersioningApi + dropboxEnabled : !!project.existsInDropbox + compiler : project.compiler + description: project.description + spellCheckLanguage: project.spellCheckLanguage + deletedByExternalDataSource : project.deletedByExternalDataSource || false + + if options.includeUsers + result.features = + collaborators: -1 # Infinite + versioning: false + dropbox:false + + if project.owner_ref.features? + if project.owner_ref.features.collaborators? + result.features.collaborators = project.owner_ref.features.collaborators + if project.owner_ref.features.versioning? + result.features.versioning = project.owner_ref.features.versioning + if project.owner_ref.features.dropbox? + result.features.dropbox = project.owner_ref.features.dropbox + + result.owner = @buildUserModelView project.owner_ref, "owner" + result.members = [] + for ref in project.readOnly_refs + result.members.push @buildUserModelView ref, "readOnly" + for ref in project.collaberator_refs + result.members.push @buildUserModelView ref, "readAndWrite" + return result + + buildUserModelView: (user, privileges) -> + _id : user._id + first_name : user.first_name + last_name : user.last_name + email : user.email + privileges : privileges + signUpDate : user.signUpDate + + buildFolderModelView: (folder) -> + _id : folder._id + name : folder.name + folders : @buildFolderModelView childFolder for childFolder in folder.folders + fileRefs : @buildFileModelView file for file in folder.fileRefs + docs : @buildDocModelView doc for doc in folder.docs + + buildFileModelView: (file) -> + _id : file._id + name : file.name + + buildDocModelView: (doc) -> + _id : doc._id + name : doc.name diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee new file mode 100644 index 0000000000..19675c70cf --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -0,0 +1,355 @@ +Project = require('../../models/Project').Project +Doc = require('../../models/Doc').Doc +Folder = require('../../models/Folder').Folder +File = require('../../models/File').File +FileStoreHandler = require("../FileStore/FileStoreHandler") +Errors = require "../../errors" +tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +projectLocator = require('./ProjectLocator') +path = require "path" +async = require "async" +_ = require('underscore') +logger = require('logger-sharelatex') +slReqIdHelper = require('soa-req-id') +docComparitor = require('./DocLinesComparitor') +projectUpdateHandler = require('./ProjectUpdateHandler') + +module.exports = ProjectEntityHandler = + getAllFolders: (project_id, sl_req_id, callback) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log sl_req_id: sl_req_id, project_id:project_id, "getting all folders for project" + folders = {} + processFolder = (basePath, folder) -> + folders[basePath] = folder + processFolder path.join(basePath, childFolder.name), childFolder for childFolder in folder.folders + + Project.findById project_id, (err, project) -> + return callback(err) if err? + return callback("no project") if !project? + processFolder "/", project.rootFolder[0] + callback null, folders + + getAllDocs: (project_id, sl_req_id, callback) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id:project_id, "getting all docs for project" + @getAllFolders project_id, sl_req_id, (err, folders) -> + return callback(err) if err? + docs = {} + for folderPath, folder of folders + for doc in folder.docs + docs[path.join(folderPath, doc.name)] = doc + logger.log count:_.keys(docs).length, project_id:project_id, "returning docs for project" + callback null, docs + + getAllFiles: (project_id, sl_req_id, callback) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id:project_id, "getting all files for project" + @getAllFolders project_id, sl_req_id, (err, folders) -> + return callback(err) if err? + files = {} + for folderPath, folder of folders + for file in folder.fileRefs + files[path.join(folderPath, file.name)] = file + callback null, files + + flushProjectToThirdPartyDataStore: (project_id, sl_req_id, callback) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + self = @ + logger.log sl_req_id: sl_req_id, project_id:project_id, "flushing project to tpds" + documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') + documentUpdaterHandler.flushProjectToMongo project_id, undefined, (error) -> + Project.findById project_id, (err, project) -> + return callback(error) if error? + requests = [] + self.getAllDocs project_id, (err, docs) -> + for docPath, doc of docs + do (docPath, doc) -> + requests.push (callback) -> + tpdsUpdateSender.addDoc {project_id:project_id, docLines:doc.lines, path:docPath, project_name:project.name, rev:doc.rev||0}, + sl_req_id, + callback + self.getAllFiles project_id, (err, files) -> + for filePath, file of files + do (filePath, file) -> + requests.push (callback) -> + tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, + sl_req_id, + callback + async.series requests, (err) -> + logger.log sl_req_id: sl_req_id, project_id:project_id, "finished flushing project to tpds" + callback(err) + + setRootDoc: (project_id, newRootDocID, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log sl_req_id: sl_req_id, project_id: project_id, rootDocId: newRootDocID, "setting root doc" + Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback + + unsetRootDoc: (project_id, sl_req_id, callback = (error) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log sl_req_id: sl_req_id, project_id: project_id, "removing root doc" + Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback + + addDoc: (project_or_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc, folder_id) ->)=> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + Project.getProject project_or_id, "", (err, project) -> + logger.log sl_req_id: sl_req_id, project: project._id, folder_id: folder_id, doc_name: docName, "adding doc" + return callback(err) if err? + confirmFolder project, folder_id, (folder_id)=> + doc = new Doc name: docName, lines: docLines + Project.putElement project._id, folder_id, doc, "doc", (err, result)=> + tpdsUpdateSender.addDoc {project_id:project._id, docLines:docLines, path:result.path.fileSystem, project_name:project.name, rev:doc.rev}, sl_req_id, -> + callback(err, doc, folder_id) + + addFile: (project_or_id, folder_id, fileName, path, sl_req_id, callback = (error, fileRef, folder_id) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + Project.getProject project_or_id, "", (err, project) -> + logger.log sl_req_id: sl_req_id, project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file" + return callback(err) if err? + confirmFolder project, folder_id, (folder_id)-> + fileRef = new File name : fileName + FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)-> + if err? + logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" + return callback(err) + Project.putElement project._id, folder_id, fileRef, "file", (err, result)=> + tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, project_name:project.name, rev:fileRef.rev}, "sl_req_id_here", -> + callback(err, fileRef, folder_id) + + replaceFile: (project_or_id, file_id, fsPath, callback)-> + Project.getProject project_or_id, "", (err, project) -> + findOpts = + project_id:project._id + element_id:file_id + type:"file" + projectLocator.findElement findOpts, (err, fileRef, path)=> + FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, fsPath, (err)-> + tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, "sl_req_id_here", (error) -> + conditons = _id:project._id + inc = {} + inc["#{path.mongo}.rev"] = 1 + set = {} + set["#{path.mongo}.created"] = new Date() + update = + "$inc": inc + "$set": set + Project.update conditons, update, {}, (err, second)-> + callback() + + copyFileFromExistingProject: (project_or_id, folder_id, originalProject_id, origonalFileRef, sl_req_id, callback = (error, fileRef, folder_id) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + Project.getProject project_or_id, "", (err, project) -> + logger.log sl_req_id: sl_req_id, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3" + return callback(err) if err? + confirmFolder project, folder_id, (folder_id)=> + fileRef = new File name : origonalFileRef.name + FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)-> + if err? + logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3" + Project.putElement project._id, folder_id, fileRef, "file", (err, result)=> + tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, rev:fileRef.rev, project_name:project.name}, sl_req_id, (error) -> + callback(error, fileRef, folder_id) + + mkdirp: (project_or_id, path, sl_req_id, callback = (err, newlyCreatedFolders, lastFolderInPath)->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + self = @ + folders = path.split('/') + folders = _.select folders, (folder)-> + return folder.length != 0 + + Project.getProject project_or_id, "", (err, project)=> + if path == '/' + logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" + return callback(null, [], project.rootFolder[0]) + logger.log project_id: project._id, path:path, folders:folders, "running mkdirp" + + builtUpPath = '' + procesFolder = (previousFolders, folderName, callback)=> + previousFolders = previousFolders || [] + parentFolder = previousFolders[previousFolders.length-1] + if parentFolder? + parentFolder_id = parentFolder._id + builtUpPath = "#{builtUpPath}/#{folderName}" + projectLocator.findElementByPath project_or_id, builtUpPath, (err, foundFolder)=> + if !foundFolder? + logger.log sl_req_id: sl_req_id, path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" + @addFolder project_or_id, parentFolder_id, folderName, sl_req_id, (err, newFolder, parentFolder_id)-> + newFolder.parentFolder_id = parentFolder_id + previousFolders.push newFolder + callback null, previousFolders + else + foundFolder.filterOut = true + previousFolders.push foundFolder + callback null, previousFolders + + + async.reduce folders, [], procesFolder, (err, folders)-> + lastFolder = folders[folders.length-1] + folders = _.select folders, (folder)-> + !folder.filterOut + callback(null, folders, lastFolder) + + addFolder: (project_or_id, parentFolder_id, folderName, sl_req_id, callback)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + folder = new Folder name: folderName + Project.getProject project_or_id, "", (err, project) -> + return callback(err) if err? + confirmFolder project, parentFolder_id, (parentFolder_id)=> + logger.log sl_req_id: sl_req_id, project: project_or_id, parentFolder_id:parentFolder_id, folderName:folderName, "new folder added" + Project.putElement project._id, parentFolder_id, folder, "folder", (err, result)=> + if callback? + callback(err, folder, parentFolder_id) + + updateDocLines : (project_or_id, doc_id, docLines, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + Project.getProject project_or_id, "", (err, project)-> + return callback(err) if err? + return callback(new Errors.NotFoundError("project not found")) if !project? + project_id = project._id + if err? + logger.err err:err,project_id:project_id, "error finding project" + callback err + else if !project? + logger.err project_id:project_id, doc_id:doc_id, err: new Error("project #{project_id} could not be found for doc #{doc_id}") + callback "could not find project #{project_id}" + else + projectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)-> + if err? + logger.err "error putting doc #{doc_id} in project #{project_id} #{err}" + callback err + else if docComparitor.areSame docLines, doc.lines + logger.log sl_req_id: sl_req_id, docLines:docLines, project_id:project_id, doc_id:doc_id, rev:doc.rev, "old doc lines are same as the new doc lines, not updating them" + callback() + else + logger.log sl_req_id: sl_req_id, project_id:project_id, doc_id:doc_id, docLines: docLines, oldDocLines: doc.lines, rev:doc.rev, "updating doc lines" + conditons = _id:project_id + update = {$set:{}, $inc:{}} + changeLines = {} + changeLines["#{path.mongo}.lines"] = docLines + inc = {} + inc["#{path.mongo}.rev"] = 1 + update["$set"] = changeLines + update["$inc"] = inc + Project.update conditons, update, {}, (err, second)-> + if(err) + logger.err(sl_req_id:sl_req_id, doc_id:doc_id, project_id:project_id, err:err, "error saving doc to mongo") + callback(err) + else + logger.log sl_req_id:sl_req_id, doc_id:doc_id, project_id:project_id, newDocLines:docLines, oldDocLines:doc.lines, "doc saved to mongo" + rev = doc.rev+1 + projectUpdateHandler.markAsUpdated project_id + tpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, docLines:docLines, project_name:project.name, rev:rev}, sl_req_id, callback + + moveEntity: (project_id, entity_id, folder_id, entityType, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + self = @ + destinationFolder_id = folder_id + logger.log sl_req_id: sl_req_id, entityType:entityType, entity_id:entity_id, project_id:project_id, folder_id:folder_id, "moving entity" + if !entityType? + logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id + return callback("No entityType set") + entityType = entityType.toLowerCase() + Project.findById project_id, (err, project)=> + projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)-> + return callback(err) if err? + self._removeElementFromMongoArray Project, project_id, path.mongo, (err)-> + return callback(err) if err? + Project.putElement project_id, destinationFolder_id, entity, entityType, (err, result)-> + return callback(err) if err? + opts = + project_id:project_id + project_name:project.name + startPath:path.fileSystem + endPath:result.path.fileSystem, + rev:entity.rev + tpdsUpdateSender.moveEntity opts, sl_req_id, callback + + deleteEntity: (project_id, entity_id, entityType, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + self = @ + logger.log entity_id:entity_id, type:entityType, project_id:project_id, "deleting project entity" + if !entityType? + logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id + return callback("No entityType set") + entityType = entityType.toLowerCase() + Project.findById project_id, (err, project)=> + return callback(error) if error? + projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> + return callback(error) if error? + ProjectEntityHandler._cleanUpEntity project, entity, entityType, (error) -> + return callback(error) if error? + tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, sl_req_id, (error) -> + return callback(error) if error? + self._removeElementFromMongoArray Project, project_id, path.mongo, (error) -> + return callback(error) if error? + callback null + + _cleanUpEntity: (project, entity, entityType, sl_req_id, callback = (error) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + + if(entityType.indexOf("file") != -1) + ProjectEntityHandler._cleanUpFile project, entity, sl_req_id, callback + else if (entityType.indexOf("doc") != -1) + ProjectEntityHandler._cleanUpDoc project, entity, sl_req_id, callback + else if (entityType.indexOf("folder") != -1) + ProjectEntityHandler._cleanUpFolder project, entity, sl_req_id, callback + else + callback() + + _cleanUpDoc: (project, doc, sl_req_id, callback = (error) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + project_id = project._id.toString() + doc_id = doc._id.toString() + unsetRootDocIfRequired = (callback) => + if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id + @unsetRootDoc project_id, callback + else + callback() + + unsetRootDocIfRequired (error) -> + return callback(error) if error? + require('../../Features/DocumentUpdater/DocumentUpdaterHandler').deleteDoc project_id, doc_id, callback + + _cleanUpFile: (project, file, sl_req_id, callback = (error) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + project_id = project._id.toString() + file_id = file._id.toString() + FileStoreHandler.deleteFile project_id, file_id, callback + + _cleanUpFolder: (project, folder, sl_req_id, callback = (error) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + + jobs = [] + for doc in folder.docs + do (doc) -> + jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, sl_req_id, callback + + for file in folder.fileRefs + do (file) -> + jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, sl_req_id, callback + + for childFolder in folder.folders + do (childFolder) -> + jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, sl_req_id, callback + + async.series jobs, callback + + _removeElementFromMongoArray : (model, model_id, path, callback)-> + conditons = {_id:model_id} + update = {"$unset":{}} + update["$unset"][path] = 1 + model.update conditons, update, {}, (err)-> + pullUpdate = {"$pull":{}} + nonArrayPath = path.slice(0, path.lastIndexOf(".")) + pullUpdate["$pull"][nonArrayPath] = null + model.update conditons, pullUpdate, {}, (err)-> + if callback? + callback(err) + +confirmFolder = (project, folder_id, callback)-> + logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project" + if folder_id+'' == 'undefined' + callback(project.rootFolder[0]._id) + else if folder_id != null + callback folder_id + else + callback(project.rootFolder[0]._id) diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee new file mode 100644 index 0000000000..a85239f69d --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -0,0 +1,61 @@ +mongojs = require("../../infrastructure/mongojs") +db = mongojs.db +ObjectId = mongojs.ObjectId +async = require "async" + +module.exports = ProjectGetter = + EXCLUDE_DEPTH: 8 + + getProjectWithoutDocLines: (project_id, callback=(error, project) ->) -> + excludes = {} + for i in [1..@EXCLUDE_DEPTH] + excludes["rootFolder#{Array(i).join(".folder")}.docs.lines"] = 0 + db.projects.find _id: ObjectId(project_id), excludes, (error, projects = []) -> + callback error, projects[0] + + getProject: (query, projection, callback = (error, project) ->) -> + if typeof query == "string" + query = _id: ObjectId(query) + else if query instanceof ObjectId + query = _id: query + db.projects.findOne query, projection, callback + + populateProjectWithUsers: (project, callback=(error, project) ->) -> + # eventually this should be in a UserGetter.getUser module + getUser = (user_id, callback=(error, user) ->) -> + unless user_id instanceof ObjectId + user_id = ObjectId(user_id) + db.users.find _id: user_id, (error, users = []) -> + callback error, users[0] + + jobs = [] + jobs.push (callback) -> + getUser project.owner_ref, (error, user) -> + return callback(error) if error? + if user? + project.owner_ref = user + callback null, project + + readOnly_refs = project.readOnly_refs + project.readOnly_refs = [] + for readOnly_ref in readOnly_refs + do (readOnly_ref) -> + jobs.push (callback) -> + getUser readOnly_ref, (error, user) -> + return callback(error) if error? + if user? + project.readOnly_refs.push user + callback null, project + + collaberator_refs = project.collaberator_refs + project.collaberator_refs = [] + for collaberator_ref in collaberator_refs + do (collaberator_ref) -> + jobs.push (callback) -> + getUser collaberator_ref, (error, user) -> + return callback(error) if error? + if user? + project.collaberator_refs.push user + callback null, project + + async.series jobs, (error) -> callback error, project diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee new file mode 100644 index 0000000000..26f4b3b3cd --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -0,0 +1,141 @@ +Project = require('../../models/Project').Project +Errors = require "../../errors" +_ = require('underscore') +logger = require('logger-sharelatex') +async = require('async') + +module.exports = + findElement: (options, callback = (err, element, path, parentFolder)->)-> + {project, project_id, element_id, type} = options + elementType = sanitizeTypeOfElement type + + count = 0 + endOfBranch = -> + if --count == 0 + logger.warn "element #{element_id} could not be found for project #{project_id || project._id}" + return callback(new Errors.NotFoundError("entity not found")) + + search = (searchFolder, path)-> + count++ + element = _.find searchFolder[elementType], (el)-> el._id+'' == element_id+'' #need to ToString both id's for robustness + if !element? && searchFolder.folders? && searchFolder.folders.length != 0 + _.each searchFolder.folders, (folder, index)-> + newPath = {} + newPath[key] = value for own key,value of path #make a value copy of the string + newPath.fileSystem += "/#{folder.name}" + newPath.mongo += ".folders.#{index}" + search folder, newPath + endOfBranch() + return + else if element? + elementPlaceInArray = getIndexOf(searchFolder[elementType], element_id) + path.fileSystem += "/#{element.name}" + path.mongo +=".#{elementType}.#{elementPlaceInArray}" + callback(null, element, path, searchFolder) + else if !element? + return endOfBranch() + + path = {fileSystem:'',mongo:'rootFolder.0'} + + startSearch = (project)-> + if element_id+'' == project.rootFolder[0]._id+'' + callback(null, project.rootFolder[0], path, null) + else + search project.rootFolder[0], path + + if project? + startSearch(project) + else + Project.findById project_id, (err, project)-> + return callback(err) if err? + if !project? + return callback(new Errors.NotFoundError("project not found")) + startSearch project + + findRootDoc : (opts, callback)-> + getRootDoc = (project)=> + @findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback + {project, project_id} = opts + if project? + getRootDoc project + else + Project.findById project_id, (err, project)-> + getRootDoc project + + findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)-> + + getParentFolder = (haystackFolder, foldersList, level, cb)-> + if foldersList.length == 0 + return cb null, haystackFolder + needleFolderName = foldersList[level] + found = false + _.each haystackFolder.folders, (folder)-> + if folder.name.toLowerCase() == needleFolderName.toLowerCase() + found = true + if level == foldersList.length-1 + cb null, folder + else + getParentFolder(folder, foldersList, ++level, cb) + if !found + cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, folder #{foldersList[level]} could not be found") + + getEntity = (folder, entityName, cb)-> + if !entityName? + return cb null, folder + enteties = _.union folder.fileRefs, folder.docs, folder.folders + result = _.find enteties, (entity)-> + entity.name.toLowerCase() == entityName.toLowerCase() + if result? + cb null, result + else + cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found") + + + Project.getProject project_or_id, "", (err, project)-> + if needlePath == '' || needlePath == '/' + return callback(null, project.rootFolder[0]) + + if needlePath.indexOf('/') == 0 + needlePath = needlePath.substring(1) + foldersList = needlePath.split('/') + needleName = foldersList.pop() + rootFolder = project.rootFolder[0] + + logger.log project_id:project._id, path:needlePath, foldersList:foldersList, "looking for element by path" + jobs = new Array() + jobs.push( + (cb)-> + getParentFolder rootFolder, foldersList, 0, cb + ) + jobs.push( + (folder, cb)-> + getEntity folder, needleName, cb + ) + async.waterfall jobs, callback + + findUsersProjectByName: (user_id, projectName, callback)-> + Project.findAllUsersProjects user_id, 'name', (projects, collabertions=[])-> + projects = projects.concat(collabertions) + projectName = projectName.toLowerCase() + project = _.find projects, (project)-> + project.name.toLowerCase() == projectName + logger.log user_id:user_id, projectName:projectName, totalProjects:projects.length, project:project, "looking for project by name" + callback(null, project) + + +sanitizeTypeOfElement = (elementType)-> + lastChar = elementType.slice -1 + if lastChar != "s" + elementType +="s" + if elementType == "files" + elementType = "fileRefs" + return elementType + + +getIndexOf = (searchEntity, id)-> + length = searchEntity.length + count = 0 + while(count < length) + if searchEntity[count]._id+"" == id+"" + return count + count++ diff --git a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee new file mode 100644 index 0000000000..0a0b02e40e --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee @@ -0,0 +1,38 @@ +Project = require('../../models/Project').Project +logger = require('logger-sharelatex') +_ = require('underscore') +settings = require("settings-sharelatex") + +safeCompilers = ["xelatex", "pdflatex", "latex", "lualatex"] + +module.exports = + setCompiler : (project_id, compiler, callback = ()->)-> + logger.log project_id:project_id, compiler:compiler, "setting the compiler" + compiler = compiler.toLowerCase() + if !_.contains safeCompilers, compiler + return callback() + conditions = {_id:project_id} + update = {compiler:compiler} + Project.update conditions, update, {}, (err)-> + if callback? + callback() + + + setSpellCheckLanguage: (project_id, languageCode, callback = ()->)-> + logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language" + languageIsSafe = false + settings.languages.forEach (safeLang)-> + if safeLang.code == languageCode + languageIsSafe = true + + if languageCode == "" + languageIsSafe = true + + if languageIsSafe + conditions = {_id:project_id} + update = {spellCheckLanguage:languageCode} + Project.update conditions, update, {}, (err)-> + callback() + else + logger.err project_id:project_id, languageCode:languageCode, "tryed to set unsafe language" + callback() diff --git a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee new file mode 100644 index 0000000000..80f2c70eb3 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee @@ -0,0 +1,19 @@ +slReqIdHelper = require('soa-req-id') +ProjectEntityHandler = require "./ProjectEntityHandler" +Path = require "path" + +module.exports = ProjectRootDocManager = + setRootDocAutomatically: (project_id, sl_req_id, callback = (error) ->) -> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + ProjectEntityHandler.getAllDocs project_id, sl_req_id, (error, docs) -> + return callback(error) if error? + root_doc_id = null + for path, doc of docs + for line in doc.lines || [] + if Path.extname(path).match(/\.R?tex$/) and line.match(/\\documentclass/) + root_doc_id = doc._id + if root_doc_id? + ProjectEntityHandler.setRootDoc project_id, root_doc_id, sl_req_id, callback + else + callback() + diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee new file mode 100644 index 0000000000..ffabf60800 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -0,0 +1,10 @@ +Project = require('../../models/Project').Project +logger = require('logger-sharelatex') + +module.exports = + markAsUpdated : (project_id, callback)-> + conditions = {_id:project_id} + update = {lastUpdated:Date.now()} + Project.update conditions, update, {}, (err)-> + if callback? + callback() diff --git a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee new file mode 100644 index 0000000000..5010b08eed --- /dev/null +++ b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee @@ -0,0 +1,59 @@ +logger = require('logger-sharelatex') +User = require('../../models/User').User +AnalyticsManager = require("../Analytics/AnalyticsManager") +SubscriptionLocator = require "../Subscription/SubscriptionLocator" +Settings = require "settings-sharelatex" + +module.exports = ReferalAllocator = + allocate: (referal_id, new_user_id, referal_source, referal_medium, callback = ->)-> + if !referal_id? + return logger.log new_user_id:new_user_id, "no referal for user" + logger.log referal_id:referal_id, new_user_id:new_user_id, "allocating users referal" + + query = {"referal_id":referal_id} + User.findOne query, (error, user) -> + return callback(error) if error? + return callback(new Error("user not found")) if !user? or !user._id? + + # Can be backgrounded + AnalyticsManager.trackReferral user, referal_source, referal_medium + + if referal_source == "bonus" + User.update query, { + $push: + refered_users: new_user_id + $inc: + refered_user_count: 1 + }, {}, (err)-> + if err? + logger.err err:err, referal_id:referal_id, new_user_id:new_user_id, "something went wrong allocating referal" + return callback(err) + ReferalAllocator.assignBonus user._id, callback + else + callback() + + assignBonus: (user_id, callback = (error) ->) -> + SubscriptionLocator.getUsersSubscription user_id, (error, subscription) -> + return callback(error) if error? + logger.log + subscription: subscription, + user_id: user_id, + "checking user doesn't have a subsciption before assigning bonus" + if !subscription? or !subscription.planCode? + query = _id: user_id + User.findOne query, (error, user) -> + return callback(error) if error + return callback(new Error("user not found")) if !user? + logger.log + user_id: user_id, + refered_user_count: user.refered_user_count, + bonus_features: Settings.bonus_features[user.refered_user_count], + "assigning bonus" + if user.refered_user_count? and Settings.bonus_features[user.refered_user_count]? + User.update query, { $set: features: Settings.bonus_features[user.refered_user_count] }, callback + else + callback() + else + callback() + + diff --git a/services/web/app/coffee/Features/Referal/ReferalConnect.coffee b/services/web/app/coffee/Features/Referal/ReferalConnect.coffee new file mode 100644 index 0000000000..e84474a47e --- /dev/null +++ b/services/web/app/coffee/Features/Referal/ReferalConnect.coffee @@ -0,0 +1,34 @@ +module.exports = + + use: (req, res, next)-> + if req.query? + if req.query.referal? + req.session.referal_id = req.query.referal + else if req.query.r? # Short hand for referal + req.session.referal_id = req.query.r + else if req.query.fb_ref? + req.session.referal_id = req.query.fb_ref + + if req.query.rm? # referal medium e.g. twitter, facebook, email + switch req.query.rm + when "fb" + req.session.referal_medium = "facebook" + when "t" + req.session.referal_medium = "twitter" + when "gp" + req.session.referal_medium = "google_plus" + when "e" + req.session.referal_medium = "email" + when "d" + req.session.referal_medium = "direct" + + if req.query.rs? # referal source e.g. project share, bonus + switch req.query.rs + when "b" + req.session.referal_source = "bonus" + when "ps" + req.session.referal_source = "public_share" + when "ci" + req.session.referal_source = "collaborator_invite" + + next() diff --git a/services/web/app/coffee/Features/Referal/ReferalController.coffee b/services/web/app/coffee/Features/Referal/ReferalController.coffee new file mode 100644 index 0000000000..65247febd7 --- /dev/null +++ b/services/web/app/coffee/Features/Referal/ReferalController.coffee @@ -0,0 +1,10 @@ +logger = require('logger-sharelatex') +ReferalHandler = require('./ReferalHandler') + +module.exports = + bonus: (req, res)-> + ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)-> + res.render "referal/bonus", + title: "Bonus - Please recommend us" + refered_users: refered_users + refered_user_count: (refered_users or []).length diff --git a/services/web/app/coffee/Features/Referal/ReferalHandler.coffee b/services/web/app/coffee/Features/Referal/ReferalHandler.coffee new file mode 100644 index 0000000000..5e016b5afa --- /dev/null +++ b/services/web/app/coffee/Features/Referal/ReferalHandler.coffee @@ -0,0 +1,7 @@ +User = require('../../models/User').User + +module.exports = + getReferedUserIds: (user_id, callback)-> + User.findById user_id, (err, user)-> + refered_users = user.refered_users || [] + callback "null", refered_users \ No newline at end of file diff --git a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee new file mode 100644 index 0000000000..c6f02d34bf --- /dev/null +++ b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee @@ -0,0 +1,11 @@ +User = require("../../models/User").User + +module.exports = RefererMiddleware = + getUserReferalId: (req, res, next) -> + if req.session? and req.session.user? + User.findById req.session.user._id, (error, user) -> + return next(error) if error? + req.session.user.referal_id = user.referal_id + next() + else + next() diff --git a/services/web/app/coffee/Features/Security/AuthorizationManager.coffee b/services/web/app/coffee/Features/Security/AuthorizationManager.coffee new file mode 100644 index 0000000000..0ec4985f35 --- /dev/null +++ b/services/web/app/coffee/Features/Security/AuthorizationManager.coffee @@ -0,0 +1,38 @@ +SecurityManager = require '../../managers/SecurityManager' + +module.exports = AuthorizationManager = + getPrivilegeLevelForProject: ( + project, user, + callback = (error, canAccess, privilegeLevel)-> + ) -> + # This is not tested because eventually this function should be brought into + # this module. + SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel) -> + if canAccess + callback null, true, privilegeLevel + else + callback null, false + + setPrivilegeLevelOnClient: (client, privilegeLevel) -> + client.set("privilege_level", privilegeLevel) + + ensureClientCanViewProject: (client, callback = (error, project_id)->) -> + @ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite", "readOnly"], callback + + ensureClientCanEditProject: (client, callback = (error, project_id)->) -> + @ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite"], callback + + ensureClientCanAdminProject: (client, callback = (error, project_id)->) -> + @ensureClientHasPrivilegeLevelForProject client, ["owner"], callback + + ensureClientHasPrivilegeLevelForProject: (client, levels, callback = (error, project_id)->) -> + client.get "privilege_level", (error, level) -> + return callback(error) if error? + if level? + client.get "project_id", (error, project_id) -> + return callback(error) if error? + if project_id? + if levels.indexOf(level) > -1 + callback null, project_id + + diff --git a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee new file mode 100644 index 0000000000..023fcdc0ef --- /dev/null +++ b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee @@ -0,0 +1,24 @@ +Settings = require('settings-sharelatex') +redis = require('redis') +rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host) +rclient.auth(Settings.redis.web.password) + +buildKey = (k)-> + return "LoginRateLimit:#{k}" + +ONE_MIN = 60 +ATTEMPT_LIMIT = 10 + +module.exports = + processLoginRequest: (email, callback)-> + multi = rclient.multi() + multi.incr(buildKey(email)) + multi.get(buildKey(email)) + multi.expire(buildKey(email), ONE_MIN * 2) + multi.exec (err, results)-> + loginCount = results[1] + allow = loginCount <= ATTEMPT_LIMIT + callback err, allow + + recordSuccessfulLogin: (email, callback = ->)-> + rclient.del buildKey(email), callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee new file mode 100644 index 0000000000..0a1c8f0917 --- /dev/null +++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee @@ -0,0 +1,14 @@ +request = require 'request' +Settings = require 'settings-sharelatex' +logger = require 'logger-sharelatex' + +module.exports = SpellingController = + proxyRequestToSpellingApi: (req, res, next) -> + url = req.url.slice("/spelling".length) + url = "/user/#{req.session.user._id}#{url}" + req.headers["Host"] = Settings.apis.spelling.host + getReq = request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body) + getReq.pipe(res) + getReq.on "error", (error) -> + logger.error err: error, "Spelling API error" + res.send 500 diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee new file mode 100644 index 0000000000..7f661a522e --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -0,0 +1,62 @@ +logger = require("logger-sharelatex") +Project = require("../../models/Project").Project +User = require("../../models/User").User +SubscriptionLocator = require("./SubscriptionLocator") +Settings = require("settings-sharelatex") + +module.exports = + + allowedNumberOfCollaboratorsInProject: (project_id, callback) -> + getOwnerOfProject project_id, (error, owner)-> + return callback(error) if error? + if owner.features? and owner.features.collaborators? + callback null, owner.features.collaborators + else + callback null, Settings.defaultPlanCode.collaborators + + currentNumberOfCollaboratorsInProject: (project_id, callback) -> + Project.findById project_id, 'collaberator_refs readOnly_refs', (error, project) -> + return callback(error) if error? + callback null, (project.collaberator_refs.length + project.readOnly_refs.length) + + isCollaboratorLimitReached: (project_id, callback = (error, limit_reached)->) -> + @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => + return callback(error) if error? + @currentNumberOfCollaboratorsInProject project_id, (error, current_number) => + return callback(error) if error? + if current_number < allowed_number or allowed_number < 0 + callback null, false + else + callback null, true + + userHasSubscriptionOrFreeTrial: (user, callback = (err, hasSubscriptionOrTrial, subscription)->) -> + @userHasSubscription user, (err, hasSubscription, subscription)=> + @userHasFreeTrial user, (err, hasFreeTrial)=> + logger.log user_id:user._id, subscription:subscription, hasFreeTrial:hasFreeTrial, hasSubscription:hasSubscription, "checking if user has subscription or free trial" + callback null, hasFreeTrial or hasSubscription, subscription + + userHasSubscription: (user, callback = (err, hasSubscription, subscription)->) -> + logger.log user_id:user._id, "checking if user has subscription" + SubscriptionLocator.getUsersSubscription user._id, (err, subscription)-> + logger.log user:user, subscription:subscription, "checking if user has subscription" + hasValidSubscription = subscription? and subscription.recurlySubscription_id? and subscription?.state != "expired" + callback err, hasValidSubscription, subscription + + userHasFreeTrial: (user, callback = (err, hasFreeTrial, subscription)->) -> + logger.log user_id:user._id, "checking if user has free trial" + SubscriptionLocator.getUsersSubscription user, (err, subscription)-> + callback err, subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?, subscription + + hasGroupMembersLimitReached: (user_id, callback)-> + SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> + limitReached = subscription.member_ids.length >= subscription.membersLimit + logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached" + + callback(null, limitReached) + +getOwnerOfProject = (project_id, callback)-> + Project.findById project_id, 'owner_ref', (error, project) -> + return callback(error) if error? + User.findById project.owner_ref, (error, owner) -> + callback(error, owner) + diff --git a/services/web/app/coffee/Features/Subscription/PlansLocator.coffee b/services/web/app/coffee/Features/Subscription/PlansLocator.coffee new file mode 100644 index 0000000000..49d7a29b79 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/PlansLocator.coffee @@ -0,0 +1,9 @@ +Settings = require("settings-sharelatex") + +module.exports = + + findLocalPlanInSettings: (planCode) -> + for plan in Settings.plans + return plan if plan.planCode == planCode + return null + diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee new file mode 100644 index 0000000000..86130e1b28 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee @@ -0,0 +1,181 @@ +querystring = require 'querystring' +crypto = require 'crypto' +request = require 'request' +Settings = require "settings-sharelatex" +xml2js = require "xml2js" +logger = require("logger-sharelatex") + +module.exports = RecurlyWrapper = + apiUrl : "https://api.recurly.com/v2" + + apiRequest : (options, callback) -> + options.url = @apiUrl + "/" + options.url + options.headers = + "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64") + "Accept" : "application/xml" + "Content-Type" : "application/xml; charset=utf-8" + request options, (error, response, body) -> + unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 + error = "Recurly API returned with status code: #{response.statusCode}" + callback(error, response, body) + + sign : (parameters, callback) -> + nestAttributesForQueryString = (attributes, base) -> + newAttributes = {} + for key, value of attributes + if base? + newKey = "#{base}[#{key}]" + else + newKey = key + + if typeof value == "object" + for key, value of nestAttributesForQueryString(value, newKey) + newAttributes[key] = value + else + newAttributes[newKey] = value + + return newAttributes + + crypto.randomBytes 32, (error, buffer) -> + return callback error if error? + parameters.nonce = buffer.toString "base64" + parameters.timestamp = Math.round((new Date()).getTime() / 1000) + + unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters) + + signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex") + signature = "#{signed}|#{unsignedQuery}" + + callback null, signature + + getSubscription: (subscriptionId, options, callback) -> + callback = options unless callback? + options ||= {} + + if options.recurlyJsResult + url = "recurly_js/result/#{subscriptionId}" + else + url = "subscriptions/#{subscriptionId}" + + @apiRequest({ + url: url + }, (error, response, body) => + return callback(error) if error? + @_parseSubscriptionXml body, (error, recurlySubscription) => + return callback(error) if error? + if options.includeAccount + if recurlySubscription.account? and recurlySubscription.account.url? + accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1] + else + return callback "I don't understand the response from Recurly" + + @getAccount accountId, (error, account) -> + return callback(error) if error? + recurlySubscription.account = account + callback null, recurlySubscription + + else + callback null, recurlySubscription + ) + + getAccount: (accountId, callback) -> + @apiRequest({ + url: "accounts/#{accountId}" + }, (error, response, body) => + return callback(error) if error? + @_parseAccountXml body, callback + ) + + updateSubscription: (subscriptionId, options, callback) -> + logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription" + requestBody = """ + + #{options.plan_code} + #{options.timeframe} + + """ + @apiRequest({ + url : "subscriptions/#{subscriptionId}" + method : "put" + body : requestBody + }, (error, response, responseBody) => + return callback(error) if error? + @_parseSubscriptionXml responseBody, callback + ) + + cancelSubscription: (subscriptionId, callback) -> + logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription" + @apiRequest({ + url: "subscriptions/#{subscriptionId}/cancel", + method: "put" + }, (error, response, body) -> + callback(error) + ) + + reactivateSubscription: (subscriptionId, callback) -> + logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription" + @apiRequest({ + url: "subscriptions/#{subscriptionId}/reactivate", + method: "put" + }, (error, response, body) -> + callback(error) + ) + + _parseSubscriptionXml: (xml, callback) -> + @_parseXml xml, (error, data) -> + return callback(error) if error? + if data? and data.subscription? + recurlySubscription = data.subscription + else + return callback "I don't understand the response from Recurly" + callback null, recurlySubscription + + _parseAccountXml: (xml, callback) -> + @_parseXml xml, (error, data) -> + return callback(error) if error? + if data? and data.account? + account = data.account + else + return callback "I don't understand the response from Recurly" + callback null, account + + _parseXml: (xml, callback) -> + convertDataTypes = (data) -> + if data? and data["$"]? + if data["$"]["nil"] == "nil" + data = null + else if data["$"].href? + data.url = data["$"].href + delete data["$"] + else if data["$"]["type"] == "integer" + data = parseInt(data["_"], 10) + else if data["$"]["type"] == "datetime" + data = new Date(data["_"]) + else if data["$"]["type"] == "array" + delete data["$"] + array = [] + for key, value of data + if value instanceof Array + array = array.concat(convertDataTypes(value)) + else + array.push(convertDataTypes(value)) + data = array + + if data instanceof Array + data = (convertDataTypes(entry) for entry in data) + else if typeof data == "object" + for key, value of data + data[key] = convertDataTypes(value) + return data + + parser = new xml2js.Parser( + explicitRoot : true + explicitArray : false + ) + parser.parseString xml, (error, data) -> + return callback(error) if error? + result = convertDataTypes(data) + callback null, result + + + diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionBackgroundTasks.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionBackgroundTasks.coffee new file mode 100644 index 0000000000..7085a30b1d --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionBackgroundTasks.coffee @@ -0,0 +1,21 @@ +async = require 'async' +logger = require 'logger-sharelatex' +SubscriptionUpdater = require("./SubscriptionUpdater") +SubscriptionLocator = require("./SubscriptionLocator") +AnalyticsManager = require("../Analytics/AnalyticsManager") + +module.exports = SubscriptionBackgroundJobs = + # TODO: Remove this one month after the ability to start free trials was removed + downgradeExpiredFreeTrials: (callback = (error, subscriptions)->) -> + SubscriptionLocator.expiredFreeTrials (error, subscriptions) => + return callback(error) if error? + logger.log total_subscriptions:subscriptions.length, "downgraging subscriptions" + downgrades = [] + for subscription in subscriptions + do (subscription) => + downgrades.push (cb) => + logger.log subscription: subscription, "downgrading free trial" + AnalyticsManager.trackFreeTrialExpired subscription.admin_id + SubscriptionUpdater.downgradeFreeTrial(subscription, cb) + async.series downgrades, (error) -> callback(error, subscriptions) + diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee new file mode 100644 index 0000000000..e7a67a877e --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -0,0 +1,169 @@ +SecurityManager = require '../../managers/SecurityManager' +SubscriptionHandler = require './SubscriptionHandler' +PlansLocator = require("./PlansLocator") +SubscriptionFormatters = require("./SubscriptionFormatters") +SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder') +LimitationsManager = require("./LimitationsManager") +RecurlyWrapper = require './RecurlyWrapper' +Settings = require 'settings-sharelatex' +logger = require('logger-sharelatex') + + + +module.exports = SubscriptionController = + + plansPage: (req, res, next) -> + plans = SubscriptionViewModelBuilder.buildViewModel() + if !req.session.user? + for plan in plans + plan.href = "/register?redir=#{plan.href}" + viewName = "subscriptions/plans" + if req.query.variant? + viewName += req.query.variant + logger.log viewName:viewName, "showing plans page" + res.render viewName, + title: "Plans and Pricing" + plans: plans + + + #get to show the recurly.js page + paymentPage: (req, res, next) -> + SecurityManager.getCurrentUser req, (error, user) => + return next(error) if error? + plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) + LimitationsManager.userHasSubscription user, (err, hasSubscription)-> + if hasSubscription or !plan? + res.redirect "/user/subscription" + else + RecurlyWrapper.sign { + subscription: + plan_code : req.query.planCode + account_code: user.id + }, (error, signature) -> + return next(error) if error? + res.render "subscriptions/new", + title : "Subscribe" + plan_code: req.query.planCode + recurlyConfig: JSON.stringify + currency: "USD" + subdomain: Settings.apis.recurly.subdomain + subscriptionFormOptions: JSON.stringify + acceptedCards: ['discover', 'mastercard', 'visa'] + target : "#subscribeForm" + signature : signature + planCode : req.query.planCode + successURL : "#{Settings.siteUrl}/user/subscription/create?_csrf=#{req.session._csrf}" + accountCode : user.id + enableCoupons: true + acceptPaypal: true + account : + firstName : user.first_name + lastName : user.last_name + email : user.email + + + userSubscriptionPage: (req, res, next) -> + SecurityManager.getCurrentUser req, (error, user) => + return next(error) if error? + LimitationsManager.userHasSubscriptionOrFreeTrial user, (err, hasSubOrFreeTrial)-> + if !hasSubOrFreeTrial + logger.log user: user, "redirecting to plans" + res.redirect "/user/subscription/plans" + else + SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> + return next(error) if error? + logger.log user: user, subscription:subscription, hasSubOrFreeTrial:hasSubOrFreeTrial, "showing subscription dashboard" + plans = SubscriptionViewModelBuilder.buildViewModel() + res.render "subscriptions/dashboard", + title: "Your Subscription" + plans: plans + subscription: subscription + subscriptionTabActive: true + + + editBillingDetailsPage: (req, res, next) -> + SecurityManager.getCurrentUser req, (error, user) -> + return next(error) if error? + LimitationsManager.userHasSubscription user, (err, hasSubscription)-> + if !hasSubscription + res.redirect "/user/subscription" + else + RecurlyWrapper.sign { + account_code: user.id + }, (error, signature) -> + return next(error) if error? + res.render "subscriptions/edit-billing-details", + title : "Update Billing Details" + recurlyConfig: JSON.stringify + currency: "USD" + subdomain: Settings.apis.recurly.subdomain + signature : signature + successURL : "#{Settings.siteUrl}/user/subscription/update" + user : + id : user.id + + createSubscription: (req, res, next)-> + SecurityManager.getCurrentUser req, (error, user) -> + return callback(error) if error? + subscriptionId = req.body.recurly_token + logger.log subscription_id: subscriptionId, user_id:user._id, "creating subscription" + SubscriptionHandler.createSubscription user, subscriptionId, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong creating subscription" + res.redirect "/user/subscription/thank-you" + + successful_subscription: (req, res)-> + SecurityManager.getCurrentUser req, (error, user) => + SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> + res.render "subscriptions/successful_subscription", + title: "Thank you!" + subscription:subscription + + cancelSubscription: (req, res, next) -> + SecurityManager.getCurrentUser req, (error, user) -> + logger.log user_id:user._id, "canceling subscription" + return next(error) if error? + SubscriptionHandler.cancelSubscription user, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong canceling subscription" + res.redirect "/user/subscription" + + + updateSubscription: (req, res)-> + SecurityManager.getCurrentUser req, (error, user) -> + return next(error) if error? + planCode = req.body.plan_code + logger.log planCode: planCode, user_id:user._id, "updating subscription" + SubscriptionHandler.updateSubscription user, planCode, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong updating subscription" + res.redirect "/user/subscription" + + reactivateSubscription: (req, res)-> + SecurityManager.getCurrentUser req, (error, user) -> + logger.log user_id:user._id, "reactivating subscription" + return next(error) if error? + SubscriptionHandler.reactivateSubscription user, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong reactivating subscription" + res.redirect "/user/subscription" + + recurlyCallback: (req, res)-> + logger.log data: req.body, "received recurly callback" + # we only care if a subscription has exipired + if req.body? and req.body["expired_subscription_notification"]? + recurlySubscription = req.body["expired_subscription_notification"].subscription + SubscriptionHandler.recurlyCallback recurlySubscription, -> + res.send 200 + else + res.send 200 + + recurlyNotificationParser: (req, res, next) -> + xml = "" + req.on "data", (chunk) -> + xml += chunk + req.on "end", () -> + RecurlyWrapper._parseXml xml, (error, body) -> + return next(error) if error? + req.body = body + next() diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee new file mode 100644 index 0000000000..a5620c90b1 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee @@ -0,0 +1,15 @@ +dateformat = require 'dateformat' + +module.exports = + + formatPrice: (priceInCents) -> + string = priceInCents + "" + string = "0" + string if string.length == 2 + string = "00" + string if string.length == 1 + string = "000" if string.length == 0 + cents = string.slice(-2) + dollars = string.slice(0, -2) + return "$#{dollars}.#{cents}" + + formatDate: (date) -> + dateformat date, "dS mmmm yyyy" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee new file mode 100644 index 0000000000..4ba79f5757 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -0,0 +1,34 @@ +SubscriptionGroupHandler = require("./SubscriptionGroupHandler") +logger = require("logger-sharelatex") +SubscriptionLocator = require("./SubscriptionLocator") + +module.exports = + + addUserToGroup: (req, res)-> + adminUserId = req.session.user._id + newEmail = req.body.email + logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription" + SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)-> + result = + user:user + if err and err.limitReached + result.limitReached = true + res.json(result) + + removeUserFromGroup: (req, res)-> + adminUserId = req.session.user._id + userToRemove_id = req.params.user_id + logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription" + SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, -> + res.send() + + renderSubscriptionGroupAdminPage: (req, res)-> + user_id = req.session.user._id + SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> + if !subscription.groupPlan + return res.redirect("/") + SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)-> + res.render "subscriptions/group_admin", + title: 'Group Admin' + users: users + subscription: subscription diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee new file mode 100644 index 0000000000..a93286f73c --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -0,0 +1,45 @@ +async = require("async") +_ = require("underscore") +UserCreator = require("../User/UserCreator") +SubscriptionUpdater = require("./SubscriptionUpdater") +SubscriptionLocator = require("./SubscriptionLocator") +UserLocator = require("../User/UserLocator") +LimitationsManager = require("./LimitationsManager") + + +module.exports = + + addUserToGroup: (adminUser_id, newEmail, callback)-> + UserCreator.getUserOrCreateHoldingAccount newEmail, (err, user)-> + LimitationsManager.hasGroupMembersLimitReached adminUser_id, (err, limitReached)-> + if limitReached + return callback(limitReached:limitReached) + SubscriptionUpdater.addUserToGroup adminUser_id, user._id, (err)-> + userViewModel = buildUserViewModel(user) + callback(err, userViewModel) + + removeUserFromGroup: (adminUser_id, userToRemove_id, callback)-> + SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback + + + getPopulatedListOfMembers: (adminUser_id, callback)-> + SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> + users = [] + jobs = _.map subscription.member_ids, (user_id)-> + return (cb)-> + UserLocator.findById user_id, (err, user)-> + userViewModel = buildUserViewModel(user) + users.push(userViewModel) + cb() + async.series jobs, (err)-> + callback(err, users) + + +buildUserViewModel = (user)-> + u = + email: user.email + first_name: user.first_name + last_name: user.last_name + holdingAccount: user.holdingAccount + _id: user._id + return u diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee new file mode 100644 index 0000000000..cdcf4da724 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee @@ -0,0 +1,58 @@ +RecurlyWrapper = require("./RecurlyWrapper") +Settings = require "settings-sharelatex" +User = require('../../models/User').User +logger = require('logger-sharelatex') +AnalyticsManager = require '../../Features/Analytics/AnalyticsManager' +SubscriptionUpdater = require("./SubscriptionUpdater") +LimitationsManager = require('./LimitationsManager') + +module.exports = + + createSubscription: (user, recurlySubscriptionId, callback)-> + self = @ + RecurlyWrapper.getSubscription recurlySubscriptionId, {recurlyJsResult: true}, (error, recurlySubscription) -> + return callback(error) if error? + SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) -> + return callback(error) if error? + AnalyticsManager.trackSubscriptionStarted user, recurlySubscription?.plan?.plan_code + callback() + + updateSubscription: (user, plan_code, callback)-> + logger.log user:user, plan_code:plan_code, "updating subscription" + LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + if hasSubscription + RecurlyWrapper.updateSubscription subscription.recurlySubscription_id, {plan_code: plan_code, timeframe: "now"}, (error, recurlySubscription) -> + return callback(error) if error? + SubscriptionUpdater.syncSubscription recurlySubscription, user._id, callback + else + callback() + + cancelSubscription: (user, callback) -> + LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + if hasSubscription + RecurlyWrapper.cancelSubscription subscription.recurlySubscription_id, (error) -> + return callback(error) if error? + AnalyticsManager.trackSubscriptionCancelled user + callback() + else + callback() + + reactivateSubscription: (user, callback) -> + LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + if hasSubscription + RecurlyWrapper.reactivateSubscription subscription.recurlySubscription_id, (error) -> + return callback(error) if error? + callback() + else + callback() + + recurlyCallback: (recurlySubscription, callback) -> + RecurlyWrapper.getSubscription recurlySubscription.uuid, includeAccount: true, (error, recurlySubscription) -> + return callback(error) if error? + User.findById recurlySubscription.account.account_code, (error, user) -> + return callback(error) if error? + SubscriptionUpdater.syncSubscription recurlySubscription, user._id, callback + + + + diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee new file mode 100644 index 0000000000..08875aa062 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -0,0 +1,20 @@ +Subscription = require('../../models/Subscription').Subscription +logger = require("logger-sharelatex") +ObjectId = require('mongoose').Types.ObjectId + +module.exports = + + getUsersSubscription: (user_or_id, callback)-> + if user_or_id? and user_or_id._id? + user_id = user_or_id._id + else if user_or_id? + user_id = user_or_id + logger.log user_id:user_id, "getting users subscription" + Subscription.findOne admin_id:user_id, callback + + # TODO: Remove this one month after the ability to start free trials was removed + expiredFreeTrials: (callback = (error, subscriptions)->) -> + query = + "freeTrial.expiresAt": "$lt": new Date() + "freeTrial.downgraded": "$ne": true + Subscription.find query, callback diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee new file mode 100644 index 0000000000..cb23864dcf --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -0,0 +1,36 @@ +AuthenticationController = require('../Authentication/AuthenticationController') +SubscriptionController = require('./SubscriptionController') +SubscriptionGroupController = require './SubscriptionGroupController' +Settings = require "settings-sharelatex" + +module.exports = + apply: (app) -> + return unless Settings.enableSubscriptions + + app.get '/user/subscription/plans', SubscriptionController.plansPage + + app.get '/user/subscription', AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage + + app.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage + app.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage + + app.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription + + + app.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage + app.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup + app.del '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup + + + #recurly callback + app.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback + app.ignoreCsrf("post", '/user/subscription/callback') + + #user changes there account state + app.post '/user/subscription/create', AuthenticationController.requireLogin(), SubscriptionController.createSubscription + app.post '/user/subscription/update', AuthenticationController.requireLogin(), SubscriptionController.updateSubscription + app.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription + app.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription + + + diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee new file mode 100644 index 0000000000..cab7b5690b --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -0,0 +1,80 @@ +async = require("async") +_ = require("underscore") +Subscription = require('../../models/Subscription').Subscription +SubscriptionLocator = require("./SubscriptionLocator") +UserFeaturesUpdater = require("./UserFeaturesUpdater") +PlansLocator = require("./PlansLocator") +Settings = require("settings-sharelatex") +logger = require("logger-sharelatex") +ObjectId = require('mongoose').Types.ObjectId + +oneMonthInSeconds = 60 * 60 * 24 * 30 + +module.exports = + + syncSubscription: (recurlySubscription, adminUser_id, callback) -> + self = @ + logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "syncSubscription, creating new if subscription does not exist" + SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> + if subscription? + logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does exist" + self._updateSubscription recurlySubscription, subscription, callback + else + logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does not exist, creating a new one" + self._createNewSubscription adminUser_id, (err, subscription)-> + self._updateSubscription recurlySubscription, subscription, callback + + # TODO: Remove this one month after the ability to start free trials was removed + downgradeFreeTrial: (subscription, callback = (error)->) -> + UserFeaturesUpdater.updateFeatures subscription.admin_id, Settings.defaultPlanCode, -> + subscription.freeTrial.downgraded = true + subscription.save callback + + addUserToGroup: (adminUser_id, user_id, callback)-> + logger.log adminUser_id:adminUser_id, user_id:user_id, "adding user into mongo subscription" + searchOps = + admin_id: adminUser_id + insertOperation = + "$addToSet": {member_ids:user_id} + Subscription.findAndModify searchOps, insertOperation, (err, subscription)-> + UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback + + removeUserFromGroup: (adminUser_id, user_id, callback)-> + searchOps = + admin_id: adminUser_id + removeOperation = + "$pull": {member_ids:user_id} + Subscription.update searchOps, removeOperation, -> + UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, callback + + + _createNewSubscription: (adminUser_id, callback)-> + logger.log adminUser_id:adminUser_id, "creating new subscription" + subscription = new Subscription(admin_id:adminUser_id) + subscription.freeTrial.allowed = false + subscription.save (err)-> + callback err, subscription + + _updateSubscription: (recurlySubscription, subscription, callback)-> + logger.log recurlySubscription:recurlySubscription, subscription:subscription, "updaing subscription" + plan = PlansLocator.findLocalPlanInSettings(recurlySubscription.plan.plan_code) + if recurlySubscription.state == "expired" + subscription.recurlySubscription_id = undefined + subscription.planCode = Settings.defaultPlanCode + else + subscription.recurlySubscription_id = recurlySubscription.uuid + subscription.freeTrial.expiresAt = undefined + subscription.freeTrial.planCode = undefined + subscription.freeTrial.allowed = true + subscription.planCode = recurlySubscription.plan.plan_code + if plan.groupPlan + subscription.groupPlan = true + subscription.membersLimit = plan.membersLimit + subscription.save -> + allIds = _.union subscription.members_id, [subscription.admin_id] + jobs = allIds.map (user_id)-> + return (cb)-> + UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, cb + async.parallel jobs, callback + + diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee new file mode 100644 index 0000000000..bb2bfa5e6b --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee @@ -0,0 +1,63 @@ +Settings = require('settings-sharelatex') +RecurlyWrapper = require("./RecurlyWrapper") +PlansLocator = require("./PlansLocator") +SubscriptionFormatters = require("./SubscriptionFormatters") +LimitationsManager = require("./LimitationsManager") +SubscriptionLocator = require("./SubscriptionLocator") +_ = require("underscore") + +module.exports = + + buildUsersSubscriptionViewModel: (user, callback) -> + SubscriptionLocator.getUsersSubscription user, (err, subscription)-> + LimitationsManager.userHasFreeTrial user, (err, hasFreeTrial)-> + LimitationsManager.userHasSubscription user, (err, hasSubscription)-> + if hasSubscription + return callback(error) if error? + plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) + RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)-> + callback null, + name: plan.name + nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription.current_period_ends_at) + state: recurlySubscription.state + price: SubscriptionFormatters.formatPrice recurlySubscription.unit_amount_in_cents + planCode: subscription.planCode + groupPlan: subscription.groupPlan + else if hasFreeTrial + plan = PlansLocator.findLocalPlanInSettings(subscription.freeTrial.planCode) + callback null, + name: plan.name + state: "free-trial" + planCode: plan.planCode + groupPlan: subscription.groupPlan + expiresAt: SubscriptionFormatters.formatDate(subscription.freeTrial.expiresAt) + else + callback "User has no subscription" + + + buildViewModel : -> + plans = Settings.plans + + result = + allPlans: plans + + result.personalAccount = _.find plans, (plan)-> + plan.planCode == "personal" + + result.studentAccounts = _.filter plans, (plan)-> + plan.planCode.indexOf("student") != -1 + + result.groupMonthlyPlans = _.filter plans, (plan)-> + plan.groupPlan and !plan.annual + + result.groupAnnualPlans = _.filter plans, (plan)-> + plan.groupPlan and plan.annual + + result.individualMonthlyPlans = _.filter plans, (plan)-> + !plan.groupPlan and !plan.annual and plan.planCode != "personal" and plan.planCode.indexOf("student") == -1 + + result.individualAnnualPlans = _.filter plans, (plan)-> + !plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1 + + return result + diff --git a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee new file mode 100644 index 0000000000..2d84d69b10 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee @@ -0,0 +1,16 @@ +Settings = require "settings-sharelatex" +logger = require("logger-sharelatex") +User = require('../../models/User').User +PlansLocator = require("./PlansLocator") + +module.exports = + + updateFeatures: (user_id, plan_code, callback = (err, features)->)-> + conditions = _id:user_id + update = {} + plan = PlansLocator.findLocalPlanInSettings(plan_code) + logger.log user_id:user_id, plan:plan, plan_code:plan_code, "updating users features" + update["features.#{key}"] = value for key, value of plan.features + User.update conditions, update, (err)-> + callback err, plan.features + diff --git a/services/web/app/coffee/Features/Tags/TagsController.coffee b/services/web/app/coffee/Features/Tags/TagsController.coffee new file mode 100644 index 0000000000..aeef36ed47 --- /dev/null +++ b/services/web/app/coffee/Features/Tags/TagsController.coffee @@ -0,0 +1,21 @@ +TagsHandler = require("./TagsHandler") +logger = require("logger-sharelatex") + +module.exports = + + processTagsUpdate: (req, res)-> + user_id = req.session.user._id + project_id = req.params.project_id + if req.body.deletedTag? + tag = req.body.deletedTag + TagsHandler.deleteTag user_id, project_id, tag, -> + res.send() + else + tag = req.body.tag + TagsHandler.addTag user_id, project_id, tag, -> + res.send() + logger.log user_id:user_id, project_id:project_id, body:req.body, "processing tag update" + + getAllTags: (req, res)-> + TagsHandler.getAllTags req.session.user._id, (err, allTags)-> + res.send(allTags) diff --git a/services/web/app/coffee/Features/Tags/TagsHandler.coffee b/services/web/app/coffee/Features/Tags/TagsHandler.coffee new file mode 100644 index 0000000000..8bbce700a9 --- /dev/null +++ b/services/web/app/coffee/Features/Tags/TagsHandler.coffee @@ -0,0 +1,71 @@ +_ = require('underscore') +settings = require("settings-sharelatex") +request = require("request") +logger = require("logger-sharelatex") + +module.exports = + + + deleteTag: (user_id, project_id, tag, callback)-> + uri = buildUri(user_id, project_id) + opts = + uri:uri + json: + name:tag + logger.log user_id:user_id, project_id:project_id, tag:tag, "send delete tag to tags api" + request.del opts, callback + + addTag: (user_id, project_id, tag, callback)-> + uri = buildUri(user_id, project_id) + opts = + uri:uri + json: + name:tag + logger.log user_id:user_id, project_id:project_id, tag:tag, "send add tag to tags api" + request.post opts, callback + + requestTags: (user_id, callback)-> + opts = + uri: "#{settings.apis.tags.url}/user/#{user_id}/tag" + json: true + timeout: 2000 + request.get opts, (err, res, body)-> + statusCode = if res? then res.statusCode else 500 + if err? or statusCode != 200 + e = new Error("something went wrong getting tags, #{err}, #{statusCode}") + logger.err err:err + callback(e, []) + else + callback(null, body) + + getAllTags: (user_id, callback)-> + @requestTags user_id, (err, allTags)=> + if !allTags? + allTags = [] + @groupTagsByProject allTags, (err, groupedByProject)-> + logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "getting all tags from tags api" + callback err, allTags, groupedByProject + + removeProjectFromAllTags: (user_id, project_id, callback)-> + uri = buildUri(user_id, project_id) + opts = + uri:"#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}" + logger.log user_id:user_id, project_id:project_id, "removing project_id from tags" + request.del opts, callback + + groupTagsByProject: (tags, callback)-> + result = {} + _.each tags, (tag)-> + _.each tag.project_ids, (project_id)-> + result[project_id] = [] + + _.each tags, (tag)-> + _.each tag.project_ids, (project_id)-> + clonedTag = _.clone(tag) + delete clonedTag.project_ids + result[project_id].push(clonedTag) + callback null, result + + +buildUri = (user_id, project_id)-> + uri = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}/tag" diff --git a/services/web/app/coffee/Features/Templates/TemplatesController.coffee b/services/web/app/coffee/Features/Templates/TemplatesController.coffee new file mode 100644 index 0000000000..75b667d6e9 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesController.coffee @@ -0,0 +1,44 @@ +path = require('path') +ProjectUploadManager = require('../Uploads/ProjectUploadManager') +ProjectOptionsHandler = require("../Project/ProjectOptionsHandler") +TemplatesPublisher = require("./TemplatesPublisher") +settings = require('settings-sharelatex') +fs = require('fs') +request = require('request') +uuid = require('node-uuid') +logger = require('logger-sharelatex') + + +module.exports = + + createProjectFromZipTemplate: (req, res)-> + logger.log body:req.session.templateData, "creating project from zip" + if !req.session.templateData? + return res.redirect "/project" + + dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}" + writeStream = fs.createWriteStream(dumpPath) + zipUrl = req.session.templateData.zipUrl + if zipUrl.indexOf("www") == -1 + zipUrl = "www.sharelatex.com#{zipUrl}" + request("http://#{zipUrl}").pipe(writeStream) + writeStream.on 'close', -> + ProjectUploadManager.createProjectFromZipArchive req.session.user._id, req.session.templateData.templateName, dumpPath, (err, project)-> + setCompiler project._id, req.session.templateData.compiler, -> + fs.unlink dumpPath, -> + delete req.session.templateData + res.redirect "/project/#{project._id}" + + publishProject: (user_id, project_id, callback)-> + logger.log user_id:user_id, project_id:project_id, "reciving request to publish project as template" + TemplatesPublisher.publish user_id, project_id, callback + + unPublishProject: (user_id, project_id, callback)-> + logger.log user_id:user_id, project_id:project_id, "reciving request to unpublish project as template" + TemplatesPublisher.unpublish user_id, project_id, callback + +setCompiler = (project_id, compiler, callback)-> + if compiler? + ProjectOptionsHandler.setCompiler project_id, compiler, callback + else + callback() diff --git a/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee new file mode 100644 index 0000000000..cba54087fb --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee @@ -0,0 +1,7 @@ +module.exports = + saveTemplateDataInSession: (req, res, next)-> + if req.query.templateName + req.session.templateData = req.query + next() + + diff --git a/services/web/app/coffee/Features/Templates/TemplatesPublisher.coffee b/services/web/app/coffee/Features/Templates/TemplatesPublisher.coffee new file mode 100644 index 0000000000..b1b57dd2af --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesPublisher.coffee @@ -0,0 +1,21 @@ +request = require("request") +settings = require("settings-sharelatex") +logger = require("logger-sharelatex") + +module.exports = + + publish : (user_id, project_id, callback)-> + url = buildUrl(user_id, project_id) + request.post url, (err)-> + if err? + logger.err err:err, "something went wrong publishing project as template" + callback err + + unpublish: (user_id, project_id, callback)-> + url = buildUrl(user_id, project_id) + request.del url, (err)-> + callback() + + +buildUrl = (user_id, project_id)-> + url = "#{settings.apis.templates_api.url}/templates-api/user/#{user_id}/project/#{project_id}" diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee new file mode 100644 index 0000000000..1d3c16d8e2 --- /dev/null +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee @@ -0,0 +1,49 @@ +tpdsUpdateHandler = require('./TpdsUpdateHandler') +logger = require('logger-sharelatex') +Path = require('path') +metrics = require("../../infrastructure/Metrics") + +module.exports = + mergeUpdate: (req, res)-> + metrics.inc("tpds.merge-update") + {filePath, user_id, projectName} = parseParams(req) + logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], projectName:projectName, sl_req_id:req.sl_req_id, "reciving update request from tpds" + tpdsUpdateHandler.newUpdate user_id, projectName, filePath, req, req.sl_req_id, (err)-> + logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], sl_req_id:req.sl_req_id, "sending response that tpdsUpdate has been completed" + if err? + logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds" + res.send(500) + else + logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds update has been processed" + res.send 200 + req.session.destroy() + + + deleteUpdate: (req, res)-> + metrics.inc("tpds.delete-update") + {filePath, user_id, projectName} = parseParams(req) + logger.log user_id:user_id, filePath:filePath, sl_req_id:req.sl_req_id, projectName:projectName, fullPath:req.params[0], "reciving delete request from tpds" + tpdsUpdateHandler.deleteUpdate user_id, projectName, filePath, req.sl_req_id, (err)-> + if err? + logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds" + res.send(500) + else + logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds delete has been processed" + res.send 200 + req.session.destroy() + + parseParams: parseParams = (req)-> + path = req.params[0] + user_id = req.params.user_id + + path = Path.join("/",path) + if path.substring(1).indexOf('/') == -1 + filePath = "/" + projectName = path.substring(1) + else + filePath = path.substring(path.indexOf("/",1)) + projectName = path.substring(0, path.indexOf("/",1)) + projectName = projectName.replace("/","") + + return filePath:filePath, user_id:user_id, projectName:projectName + diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks.coffee new file mode 100644 index 0000000000..80597b27a2 --- /dev/null +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks.coffee @@ -0,0 +1,35 @@ +User = require('../../models/User').User +settings = require('settings-sharelatex') +request = require "request" +logger = require('logger-sharelatex') +redis = require('redis') +rclient = redis.createClient(settings.redis.web.port, settings.redis.web.host) +rclient.auth(settings.redis.web.password) + +LAST_TIME_POLL_HAPPEND_KEY = "LAST_TIME_POLL_HAPPEND_KEY" + +self = module.exports = + + pollUsersWithDropbox: (callback)-> + self._getUserIdsWithDropbox (err, user_ids)=> + logger.log user_ids:user_ids, userCount:user_ids.length, "telling tpds to poll users with dropbox" + self._markPollHappened() + self._sendToTpds user_ids, callback + + _sendToTpds : (user_ids, callback)-> + if user_ids.length > 0 + request.post {uri:"#{settings.apis.thirdPartyDataStore.url}/user/poll", json:{user_ids:user_ids}}, callback + else if callback? + callback() + + _getUserIdsWithDropbox: (callback)-> + User.find {"dropbox.access_token.oauth_token_secret":{"$exists":true}}, "_id", (err, users)-> + ids = users.map (user)-> + return user._id+"" + callback err, ids + + _markPollHappened: (callback)-> + rclient.set LAST_TIME_POLL_HAPPEND_KEY, new Date().getTime(), callback + + getLastTimePollHappned: (callback = (err, lastTimePollHappened)->)-> + rclient.get LAST_TIME_POLL_HAPPEND_KEY, callback diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee new file mode 100644 index 0000000000..6dc2fa22a5 --- /dev/null +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee @@ -0,0 +1,46 @@ +versioningApiHandler = require('../Versioning/VersioningApiHandler') +updateMerger = require('./UpdateMerger') +logger = require('logger-sharelatex') +projectLocator = require('../Project/ProjectLocator') +projectCreationHandler = require('../Project/ProjectCreationHandler') +projectDeleter = require('../Project/ProjectDeleter') +ProjectRootDocManager = require "../Project/ProjectRootDocManager" + +commitMessage = "Before update from Dropbox" + +module.exports = + + newUpdate: (user_id, projectName, path, updateRequest, sl_req_id, callback)-> + getOrCreateProject = (cb)=> + projectLocator.findUsersProjectByName user_id, projectName, (err, project)=> + logger.log user_id:user_id, filePath:path, projectName:projectName, "handling new update from tpds" + if !project? + projectCreationHandler.createBlankProject user_id, projectName, (err, project)=> + # have a crack at setting the root doc after a while, on creation we won't have it yet, but should have + # been sent it it within 30 seconds + setTimeout (-> ProjectRootDocManager.setRootDocAutomatically project._id, sl_req_id ), @_rootDocTimeoutLength + cb err, project + else + cb err, project + getOrCreateProject (err, project)-> + versioningApiHandler.takeSnapshot project._id, commitMessage, sl_req_id, -> + updateMerger.mergeUpdate project._id, path, updateRequest, sl_req_id, (err)-> + callback(err) + + + deleteUpdate: (user_id, projectName, path, sl_req_id, callback)-> + logger.log user_id:user_id, filePath:path, "handling delete update from tpds" + projectLocator.findUsersProjectByName user_id, projectName, (err, project)-> + if !project? + logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project not found from tpds update, ignoring folder or project" + return callback() + if path == "/" + logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted" + return projectDeleter.markAsDeletedByExternalSource project._id, callback + else + versioningApiHandler.takeSnapshot project._id, commitMessage, sl_req_id, -> + updateMerger.deleteUpdate project._id, path, sl_req_id, (err)-> + callback(err) + + + _rootDocTimeoutLength : 30 * 1000 diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee new file mode 100644 index 0000000000..51d8959362 --- /dev/null +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -0,0 +1,109 @@ +settings = require('settings-sharelatex') +logger = require('logger-sharelatex') +slReqIdHelper = require('soa-req-id') +path = require('path') +Project = require('../../models/Project').Project +keys = require('../../infrastructure/Keys') +metrics = require("../../infrastructure/Metrics") + +buildPath = (user_id, project_name, filePath)-> + projectPath = path.join(project_name, "/", filePath) + projectPath = encodeURIComponent(projectPath) + fullPath = path.join("/user/", "#{user_id}", "/entity/",projectPath) + return fullPath + +queue = require('fairy').connect(settings.redis.fairy).queue(keys.queue.web_to_tpds_http_requests) + +module.exports = +  + addFile : (options, sl_req_id, callback = (err)->)-> + metrics.inc("tpds.add-file") + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> + logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, sl_req_id:sl_req_id, rev:options.rev, "sending file to third party data store" + postOptions = + method : "post" + headers: + "sl_req_id":sl_req_id + sl_entity_rev:options.rev + sl_project_id:options.project_id + sl_all_user_ids:JSON.stringify(allUserIds) + uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}" + title:"addFile" + streamOrigin : settings.apis.filestore.url + path.join("/project/#{options.project_id}/file/","#{options.file_id}") + queue.enqueue options.project_id, "pipeStreamFrom", postOptions, -> + logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, sl_req_id:sl_req_id, rev:options.rev, "sending file to third party data store queued up for processing" + callback() + + addDoc : (options, sl_req_id, callback = (err)->)-> + metrics.inc("tpds.add-doc") + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> + return callback(err) if err? + logger.log project_id: options.project_id, user_id:user_id, path: options.path, rev:options.rev, uri:options.uri, project_name:options.project_name, docLines:options.docLines, sl_req_id:sl_req_id, "sending doc to third party data store" + postOptions = + method : "post" + headers: + "sl_req_id":sl_req_id, + sl_entity_rev:options.rev, + sl_project_id:options.project_id + sl_all_user_ids:JSON.stringify(allUserIds) + uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}" + title: "addDoc" + docLines: options.docLines + queue.enqueue options.project_id, "sendDoc", postOptions, callback +   + + moveEntity : (options, sl_req_id, callback = (err)->)-> + metrics.inc("tpds.move-entity") + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + if options.newProjectName? + startPath = path.join("/#{options.project_name}/") + endPath = path.join("/#{options.newProjectName}/") + else + startPath = mergeProjectNameAndPath(options.project_name, options.startPath) + endPath = mergeProjectNameAndPath(options.project_name, options.endPath) + getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> + logger.log project_id: options.project_id, user_id:user_id, startPath:startPath, endPath:endPath, uri:options.uri, sl_req_id:sl_req_id, "moving entity in third party data store" + moveOptions = + method : "put" + title:"moveEntity" + uri : "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/entity" + headers: + "sl_req_id":sl_req_id, + sl_project_id:options.project_id, + sl_entity_rev:options.rev + sl_all_user_ids:JSON.stringify(allUserIds) + json : + user_id : user_id + endPath: endPath + startPath: startPath + queue.enqueue options.project_id, "standardHttpRequest", moveOptions, callback + + deleteEntity : (options, sl_req_id, callback = (err)->)-> + metrics.inc("tpds.delete-entity") + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> + logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, sl_req_id:sl_req_id, "deleting entity in third party data store" + deleteOptions = + method : "DELETE" + headers: + "sl_req_id":sl_req_id, + sl_project_id:options.project_id + sl_all_user_ids:JSON.stringify(allUserIds) + uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}" + title:"deleteEntity" + sl_all_user_ids:JSON.stringify(allUserIds) + queue.enqueue options.project_id, "standardHttpRequest", deleteOptions, callback + + +getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> + Project.findById project_id, "_id owner_ref readOnly_refs collaberator_refs", (err, project)-> + allUserIds = [].concat(project.collaberator_refs).concat(project.readOnly_refs).concat(project.owner_ref) + callback err, project.owner_ref, allUserIds + +mergeProjectNameAndPath = (project_name, path)-> + if(path.indexOf('/') == 0) + path = path.substring(1) + fullPath = "/#{project_name}/#{path}" + return fullPath diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee new file mode 100644 index 0000000000..d4548b4a3e --- /dev/null +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -0,0 +1,116 @@ +_ = require('underscore') +projectLocator = require('../Project/ProjectLocator') +editorController = require('../Editor/EditorController') +logger = require('logger-sharelatex') +Settings = require('settings-sharelatex') +slReqIdHelper = require('soa-req-id') +FileTypeManager = require('../Uploads/FileTypeManager') +GuidManager = require '../../managers/GuidManager' +fs = require('fs') + +module.exports = + mergeUpdate: (project_id, path, updateRequest, sl_req_id, callback)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + self = @ + logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, "merging update from tpds" + projectLocator.findElementByPath project_id, path, (err, element)=> + logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, "found element by path for merging update from tpds" + elementId = undefined + if element? + elementId = element._id + self.p.writeStreamToDisk project_id, elementId, updateRequest, (err, fsPath)-> + FileTypeManager.shouldIgnore path, (err, shouldIgnore)-> + if shouldIgnore + return callback() + FileTypeManager.isBinary path, (err, isFile)-> + if isFile + self.p.processFile project_id, elementId, fsPath, path, callback #TODO clean up the stream written to disk here + else + self.p.processDoc project_id, elementId, fsPath, path, sl_req_id, callback + + deleteUpdate: (project_id, path, sl_req_id, callback)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + projectLocator.findElementByPath project_id, path, (err, element)-> + type = 'file' + if err? || !element? + logger.log sl_req_id: sl_req_id, element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted" + return callback() + if element.lines? + type = 'doc' + else if element.folders? + type = 'folder' + logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds" + editorController.deleteEntity project_id, element._id, type, sl_req_id, (err)-> + logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, "finished processing update to delete entity from tpds" + callback() + + p: + + processDoc: (project_id, doc_id, fsPath, path, sl_req_id, callback)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + readFileIntoTextArray fsPath, (err, docLines)-> + if err? + logger.err project_id:project_id, doc_id:doc_id, fsPath:fsPath, "error reading file into text array for process doc update" + return callback(err) + logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, sl_req_id:sl_req_id, "processing doc update from tpds" + if doc_id? + editorController.setDoc project_id, doc_id, docLines, sl_req_id, (err)-> + callback() + else + setupNewEntity project_id, path, (err, folder, fileName)-> + editorController.addDoc project_id, folder._id, fileName, docLines, sl_req_id, (err)-> + callback() + + processFile: (project_id, file_id, fsPath, path, sl_req_id, callback)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + finish = (err)-> + logger.log sl_req_id: sl_req_id, project_id:project_id, file_id:file_id, path:path, "completed processing file update from tpds" + callback(err) + logger.log sl_req_id: sl_req_id, project_id:project_id, file_id:file_id, path:path, "processing file update from tpds" + setupNewEntity project_id, path, (err, folder, fileName) => + if file_id? + editorController.replaceFile project_id, file_id, fsPath, finish + else + editorController.addFile project_id, folder._id, fileName, fsPath, finish + + writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)-> + if !file_id? + file_id = GuidManager.newGuid() + dumpPath = "#{Settings.path.dumpFolder}/#{project_id}_#{file_id}" + + writeStream = fs.createWriteStream(dumpPath) + stream.pipe(writeStream) + + stream.on 'error', (err)-> + logger.err err:err, project_id:project_id, file_id:file_id, dumpPath:dumpPath, + "something went wrong with incoming tpds update stream" + writeStream.on 'error', (err)-> + logger.err err:err, project_id:project_id, file_id:file_id, dumpPath:dumpPath, + "something went wrong with writing tpds update to disk" + + stream.on 'end', -> + logger.log project_id:project_id, file_id:file_id, dumpPath:dumpPath, "incoming tpds update stream ended" + writeStream.on "finish", -> + logger.log project_id:project_id, file_id:file_id, dumpPath:dumpPath, "tpds update write stream finished" + callback null, dumpPath + + if stream.emitBufferedData? + stream.emitBufferedData() + stream.resume() + + +readFileIntoTextArray = (path, callback)-> + fs.readFile path, "utf8", (error, content = "") -> + if error? + logger.err path:path, "error reading file into text array" + return callback(err) + lines = content.split("\n") + callback error, lines + + +setupNewEntity = (project_id, path, callback)-> + lastIndexOfSlash = path.lastIndexOf("/") + fileName = path[lastIndexOfSlash+1 .. -1] + folderPath = path[0 .. lastIndexOfSlash] + editorController.mkdirp project_id, folderPath, (err, newFolders, lastFolder)-> + callback err, lastFolder, fileName diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee new file mode 100644 index 0000000000..7b5b52da99 --- /dev/null +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -0,0 +1,23 @@ +child = require "child_process" +logger = require "logger-sharelatex" +metrics = require "../../infrastructure/Metrics" + +module.exports = ArchiveManager = + extractZipArchive: (source, destination, callback = (err) ->) -> + timer = new metrics.Timer("unzipDirectory") + logger.log source: source, destination: destination, "unzipping file" + + unzip = child.spawn("unzip", [source, "-d", destination]) + + error = null + unzip.stderr.on "data", (chunk) -> + error ||= "" + error += chunk + + unzip.on "exit", () -> + timer.done() + if error? + error = new Error(error) + logger.error err:error, source: source, destination: destination, "error unzipping file" + callback(error) + diff --git a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee new file mode 100644 index 0000000000..561653f645 --- /dev/null +++ b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee @@ -0,0 +1,65 @@ +async = require "async" +fs = require "fs" +_ = require "underscore" +FileTypeManager = require "./FileTypeManager" +EditorController = require "../Editor/EditorController" +ProjectLocator = require "../Project/ProjectLocator" + +module.exports = FileSystemImportManager = + addDoc: (project_id, folder_id, name, path, replace, callback = (error, doc)-> )-> + fs.readFile path, "utf8", (error, content = "") -> + return callback(error) if error? + content = content.replace(/\r/g, "") + lines = content.split("\n") + EditorController.addDoc project_id, folder_id, name, lines, callback + + addFile: (project_id, folder_id, name, path, replace, callback = (error, file)-> )-> + if replace + ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> + return callback(error) if error? + return callback(new Error("Couldn't find folder")) if !folder? + existingFile = null + for fileRef in folder.fileRefs + if fileRef.name == name + existingFile = fileRef + break + if existingFile? + EditorController.replaceFile project_id, existingFile._id, path, callback + else + EditorController.addFile project_id, folder_id, name, path, callback + else + EditorController.addFile project_id, folder_id, name, path, callback + + addFolder: (project_id, folder_id, name, path, replace, callback = (error)-> ) -> + EditorController.addFolder project_id, folder_id, name, (error, new_folder) => + return callback(error) if error? + @addFolderContents project_id, new_folder._id, path, replace, (error) -> + return callback(error) if error? + callback null, new_folder + + addFolderContents: (project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) -> + fs.readdir folderPath, (error, entries = []) => + return callback(error) if error? + jobs = _.map entries, (entry) => + (callback) => + FileTypeManager.shouldIgnore entry, (error, ignore) => + return callback(error) if error? + if !ignore + @addEntity project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback + else + callback() + async.parallelLimit jobs, 5, callback + + addEntity: (project_id, folder_id, name, path, replace, callback = (error, entity)-> ) -> + FileTypeManager.isDirectory path, (error, isDirectory) => + return callback(error) if error? + if isDirectory + @addFolder project_id, folder_id, name, path, replace, callback + else + FileTypeManager.isBinary name, (error, isBinary) => + return callback(error) if error? + if isBinary + @addFile project_id, folder_id, name, path, replace, callback + else + @addDoc project_id, folder_id, name, path, replace, callback + diff --git a/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee b/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee new file mode 100644 index 0000000000..54096d29f8 --- /dev/null +++ b/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee @@ -0,0 +1,49 @@ +fs = require "fs" +Path = require("path") + +module.exports = FileTypeManager = + TEXT_EXTENSIONS : [ + "tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md" + ] + + IGNORE_EXTENSIONS : [ + "dvi", "aux", "log", "ps", "toc", "out", "pdfsync" + # Index and glossary files + "nlo", "ind", "glo", "gls", "glg" + # Bibtex + "bbl", "blg" + # Misc/bad + "doc", "docx", "gz" + ] + + IGNORE_FILENAMES : [ + "__MACOSX" + ] + + isDirectory: (path, callback = (error, result) ->) -> + fs.stat path, (error, stats) -> + callback(error, stats.isDirectory()) + + isBinary: (path, callback = (error, result) ->) -> + parts = path.split(".") + extension = parts.slice(-1)[0] + if extension? + extension = extension.toLowerCase() + callback null, @TEXT_EXTENSIONS.indexOf(extension) == -1 or parts.length <= 1 + + shouldIgnore: (path, callback = (error, result) ->) -> + name = Path.basename(path) + extension = name.split(".").slice(-1)[0] + if extension? + extension = extension.toLowerCase() + ignore = false + if name[0] == "." + ignore = true + if @IGNORE_EXTENSIONS.indexOf(extension) != -1 + ignore = true + if @IGNORE_FILENAMES.indexOf(name) != -1 + ignore = true + callback null, ignore + + + diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee new file mode 100644 index 0000000000..5820ae7ac7 --- /dev/null +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee @@ -0,0 +1,49 @@ +logger = require "logger-sharelatex" +metrics = require "../../infrastructure/Metrics" +fs = require "fs" +Path = require "path" +FileSystemImportManager = require "./FileSystemImportManager" +ProjectUploadManager = require "./ProjectUploadManager" + +module.exports = ProjectUploadController = + uploadProject: (req, res, next) -> + timer = new metrics.Timer("project-upload") + user_id = req.session.user._id + {name, path} = req.files.qqfile + name = Path.basename(name, ".zip") + ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) -> + fs.unlink path, -> + timer.done() + if error? + logger.error + err: error, file_path: path, file_name: name, + "error uploading project" + res.send success: false + else + logger.log + project: project._id, file_path: path, file_name: name, + "uploaded project" + res.send success: true, project_id: project._id + + uploadFile: (req, res, next) -> + timer = new metrics.Timer("file-upload") + {name, path} = req.files.qqfile + project_id = req.params.Project_id + folder_id = req.query.folder_id + FileSystemImportManager.addEntity project_id, folder_id, name, path, true, (error, entity) -> + fs.unlink path, -> + timer.done() + if error? + logger.error + err: error, project_id: project_id, file_path: path, + file_name: name, folder_id: folder_id, + "error uploading file" + res.send success: false + else + logger.log + project_id: project_id, file_path: path, file_name: name, folder_id: folder_id + "uploaded file" + res.send success: true, entity_id: entity?._id + + + diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee new file mode 100644 index 0000000000..b5d108d21d --- /dev/null +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee @@ -0,0 +1,28 @@ +path = require "path" +rimraf = require "rimraf" +ArchiveManager = require "./ArchiveManager" +FileSystemImportManager = require "./FileSystemImportManager" +ProjectCreationHandler = require "../Project/ProjectCreationHandler" +ProjectRootDocManager = require "../Project/ProjectRootDocManager" + +module.exports = ProjectUploadHandler = + createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) -> + ProjectCreationHandler.createBlankProject owner_id, name, (error, project) => + return callback(error) if error? + @insertZipArchiveIntoFolder project._id, project.rootFolder[0]._id, zipPath, (error) -> + return callback(error) if error? + ProjectRootDocManager.setRootDocAutomatically project._id, (error) -> + return callback(error) if error? + callback(error, project) + + insertZipArchiveIntoFolder: (project_id, folder_id, path, callback = (error) ->) -> + destination = @_getDestinationDirectory path + ArchiveManager.extractZipArchive path, destination, (error) -> + return callback(error) if error? + FileSystemImportManager.addFolderContents project_id, folder_id, destination, false, (error) -> + return callback(error) if error? + rimraf(destination, callback) + + _getDestinationDirectory: (source) -> + return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}") + diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee new file mode 100644 index 0000000000..f77cb4bfc5 --- /dev/null +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -0,0 +1,13 @@ +SecurityManager = require('../../managers/SecurityManager') +AuthenticationController = require('../Authentication/AuthenticationController') +ProjectUploadController = require "./ProjectUploadController" + +module.exports = + apply: (app) -> + app.post '/project/new/upload', + AuthenticationController.requireLogin(), + ProjectUploadController.uploadProject + app.post '/Project/:Project_id/upload', + SecurityManager.requestCanModifyProject, + ProjectUploadController.uploadFile + diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee new file mode 100644 index 0000000000..aa8db57c0e --- /dev/null +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -0,0 +1,35 @@ +UserGetter = require "./UserGetter" +logger = require("logger-sharelatex") + +module.exports = UserController = + getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) -> + # this is funcky as hell, we don't use the current session to get the user + # we use the auth token, actually destroying session from the chat api request + req.session.destroy() + logger.log user: req.user, "reciving request for getting logged in users personal info" + return next(new Error("User is not logged in")) if !req.user? + UserController.sendFormattedPersonalInfo(req.user, res, next) + + getPersonalInfo: (req, res, next = (error) ->) -> + UserGetter.getUser req.params.user_id, { _id: true, first_name: true, last_name: true, email: true }, (error, user) -> + logger.log user: req.params.user_id, "reciving request for getting users personal info" + return next(error) if error? + return res.send(404) if !user? + UserController.sendFormattedPersonalInfo(user, res, next) + req.session.destroy() + + + sendFormattedPersonalInfo: (user, res, next = (error) ->) -> + UserController._formatPersonalInfo user, (error, info) -> + return next(error) if error? + res.send JSON.stringify(info) + + _formatPersonalInfo: (user, callback = (error, info) ->) -> + callback null, { + id: user._id.toString() + first_name: user.first_name + last_name: user.last_name + email: user.email + signUpDate: user.signUpDate + } + diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee new file mode 100644 index 0000000000..944597f78f --- /dev/null +++ b/services/web/app/coffee/Features/User/UserCreator.coffee @@ -0,0 +1,19 @@ +User = require("../../models/User").User +UserLocator = require("./UserLocator") + +module.exports = + + getUserOrCreateHoldingAccount: (email, callback = (err, user)->)-> + self = @ + UserLocator.findByEmail email, (err, user)-> + if user? + callback(err, user) + else + self.createNewUser email:email, holdingAccount:true, callback + + createNewUser: (opts, callback)-> + user = new User() + user.email = opts.email + user.holdingAccount = opts.holdingAccount + user.save (err)-> + callback(err, user) diff --git a/services/web/app/coffee/Features/User/UserDeleter.coffee b/services/web/app/coffee/Features/User/UserDeleter.coffee new file mode 100644 index 0000000000..57126af4a5 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserDeleter.coffee @@ -0,0 +1,22 @@ +User = require("../../models/User").User +NewsletterManager = require "../../managers/NewsletterManager" +ProjectDeleter = require("../Project/ProjectDeleter") +logger = require("logger-sharelatex") + +module.exports = + + deleteUser: (user_id, callback = ()->)-> + if !user_id? + logger.err "user_id is null when trying to delete user" + return callback("no user_id") + User.findById user_id, (err, user)-> + logger.log user:user, "deleting user" + if err? + return callback(err) + NewsletterManager.unsubscribe user, (err)-> + if err? + return callback(err) + ProjectDeleter.deleteUsersProjects user._id, (err)-> + if err? + return callback(err) + user.remove callback diff --git a/services/web/app/coffee/Features/User/UserGetter.coffee b/services/web/app/coffee/Features/User/UserGetter.coffee new file mode 100644 index 0000000000..bf88c45f38 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserGetter.coffee @@ -0,0 +1,15 @@ +mongojs = require("../../infrastructure/mongojs") +db = mongojs.db +ObjectId = mongojs.ObjectId + +module.exports = UserGetter = + getUser: (query, projection, callback = (error, user) ->) -> + if arguments.length == 2 + callback = projection + projection = {} + if typeof query == "string" + query = _id: ObjectId(query) + else if query instanceof ObjectId + query = _id: query + + db.users.findOne query, projection, callback diff --git a/services/web/app/coffee/Features/User/UserLocator.coffee b/services/web/app/coffee/Features/User/UserLocator.coffee new file mode 100644 index 0000000000..9b5ed9b0bc --- /dev/null +++ b/services/web/app/coffee/Features/User/UserLocator.coffee @@ -0,0 +1,13 @@ +mongojs = require("../../infrastructure/mongojs") +db = mongojs.db +ObjectId = mongojs.ObjectId + +module.exports = + + findByEmail: (email, callback)-> + email = email.trim() + db.users.findOne email:email, (err, user)-> + callback(err, user) + + findById: (_id, callback)-> + db.users.findOne _id:ObjectId(_id+""), callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee new file mode 100644 index 0000000000..b39a239341 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -0,0 +1,30 @@ +sanitize = require('validator').sanitize + +module.exports = + validateEmail : (email) -> + re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return re.test(email) + + hasZeroLengths : (props) -> + hasZeroLength = false + props.forEach (prop) -> + if prop.length == 0 + hasZeroLength = true + return hasZeroLength + + validateRegisterRequest : (req, callback)-> + email = sanitize(req.body.email).xss().trim().toLowerCase() + password = req.body.password + username = email.match(/^[^@]*/) + if username? + first_name = username[0] + else + first_name = "" + last_name = "" + + if @hasZeroLengths([password, email]) + callback('please fill in all the fields', null) + else if !@validateEmail(email) + callback('not valid email', null) + else + callback(null, {first_name:first_name, last_name:last_name, email:email, password:password}) diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee new file mode 100644 index 0000000000..f6c281ea81 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -0,0 +1,12 @@ +mongojs = require("../../infrastructure/mongojs") +db = mongojs.db +ObjectId = mongojs.ObjectId + +module.exports = UserUpdater = + updateUser: (query, update, callback = (error) ->) -> + if typeof query == "string" + query = _id: ObjectId(query) + else if query instanceof ObjectId + query = _id: query + + db.users.update query, update, callback diff --git a/services/web/app/coffee/Features/Versioning/AutomaticSnapshotManager.coffee b/services/web/app/coffee/Features/Versioning/AutomaticSnapshotManager.coffee new file mode 100644 index 0000000000..59df268969 --- /dev/null +++ b/services/web/app/coffee/Features/Versioning/AutomaticSnapshotManager.coffee @@ -0,0 +1,50 @@ +Keys = require("./RedisKeys") +Settings = require "settings-sharelatex" +redis = require('redis') +rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host) +rclient.auth(Settings.redis.web.password) +VersioningApiHandler = require('../../Features/Versioning/VersioningApiHandler') +async = require('async') +metrics = require('../../infrastructure/Metrics') +logger = require('logger-sharelatex') + + +module.exports = AutomaticSnapshotManager = + markProjectAsUpdated: (project_id, callback = (error) ->) -> + rclient.set Keys.buildLastUpdatedKey(project_id), Date.now(), (error) -> + return callback(error) if error? + rclient.sadd Keys.projectsToSnapshotKey, project_id, (error) -> + return callback(error) if error? + callback() + + unmarkProjectAsUpdated: (project_id, callback = (err)->)-> + rclient.del Keys.buildLastUpdatedKey(project_id), Date.now(), (error) -> + return callback(error) if error? + rclient.srem Keys.projectsToSnapshotKey, project_id, (error) -> + return callback(error) if error? + callback() + + takeAutomaticSnapshots: (callback = (error) ->) -> + rclient.smembers Keys.projectsToSnapshotKey, (error, project_ids) => + logger.log project_ids:project_ids, "taking automatic snapshots" + metrics.gauge "versioning.projectsToSnapshot", project_ids.length + return callback(error) if error? + methods = [] + for project_id in project_ids + do (project_id) => + methods.push((callback) => @takeSnapshotIfRequired(project_id, callback)) + async.series methods, callback + + takeSnapshotIfRequired: (project_id, callback = (error) ->) -> + rclient.get Keys.buildLastUpdatedKey(project_id), (error, lastUpdated) -> + return callback(error) if error? + if lastUpdated? and lastUpdated < Date.now() - Settings.automaticSnapshots.waitTimeAfterLastEdit + VersioningApiHandler.takeSnapshot(project_id, "Automatic snapshot", callback) + else + rclient.get Keys.buildLastSnapshotKey(project_id), (error, lastSnapshot) -> + return callback(error) if error? + if !lastSnapshot? or lastSnapshot < Date.now() - Settings.automaticSnapshots.maxTimeBetweenSnapshots + VersioningApiHandler.takeSnapshot(project_id, "Automatic snapshot", callback) + else + callback() + diff --git a/services/web/app/coffee/Features/Versioning/RedisKeys.coffee b/services/web/app/coffee/Features/Versioning/RedisKeys.coffee new file mode 100644 index 0000000000..9f34d86b6f --- /dev/null +++ b/services/web/app/coffee/Features/Versioning/RedisKeys.coffee @@ -0,0 +1,5 @@ +module.exports = + buildLastUpdatedKey: (project_id) -> "project_last_updated:#{project_id}" + buildLastSnapshotKey: (project_id) -> "project_last_snapshot:#{project_id}" + projectsToSnapshotKey: "projects_to_snapshot" + usersToPollTpdsForUpdates: "users_with_active_projects" diff --git a/services/web/app/coffee/Features/Versioning/VersioningApiController.coffee b/services/web/app/coffee/Features/Versioning/VersioningApiController.coffee new file mode 100644 index 0000000000..480cb4fba0 --- /dev/null +++ b/services/web/app/coffee/Features/Versioning/VersioningApiController.coffee @@ -0,0 +1,31 @@ +versioningApiHandler = require './VersioningApiHandler' +metrics = require('../../infrastructure/Metrics') + +module.exports = + enableVersioning: (project_id, callback)-> + metrics.inc "versioning.enableVersioning" + versioningApiHandler.enableVersioning project_id, callback + + listVersions : (req, res) -> + metrics.inc "versioning.listVersions" + versioningApiHandler.proxyToVersioningApi(req, res) + + getVersion : (req, res) -> + metrics.inc "versioning.getVersion" + versioningApiHandler.proxyToVersioningApi(req, res) + + getVersionFile : (req, res) -> + metrics.inc "versioning.getVersionFile" + versioningApiHandler.proxyToVersioningApi(req, res) + + takeSnapshot: (req, res, next) -> + metrics.inc "versioning.takeSnapshot" + if req.body? and req.body.message? and req.body.message.length > 0 + message = req.body.message + else + message = "Manual snapshot" + versioningApiHandler.takeSnapshot req.params.Project_id, message, (error) -> + if error? + next(error) + else + res.send(200, "{}") diff --git a/services/web/app/coffee/Features/Versioning/VersioningApiHandler.coffee b/services/web/app/coffee/Features/Versioning/VersioningApiHandler.coffee new file mode 100644 index 0000000000..5d390a6b14 --- /dev/null +++ b/services/web/app/coffee/Features/Versioning/VersioningApiHandler.coffee @@ -0,0 +1,75 @@ +settings = require('settings-sharelatex') +logger = require('logger-sharelatex') +Project = require('../../models/Project').Project +request = require('request') +DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') +redis = require('redis') +rclient = redis.createClient(settings.redis.web.port, settings.redis.web.host) +rclient.auth(settings.redis.web.password) +Keys = require("./RedisKeys") +ProjectEntityHandler = require('../../Features/Project/ProjectEntityHandler') +metrics = require('../../infrastructure/Metrics') +keys = require('../../infrastructure/Keys') +queue = require('fairy').connect(settings.redis.fairy).queue(keys.queue.web_to_tpds_http_requests) +slReqIdHelper = require('soa-req-id') + +headers = + Authorization : "Basic " + new Buffer("#{settings.apis.versioning.username}:#{settings.apis.versioning.password}").toString("base64") + +module.exports = + + enableVersioning: (project_or_id, callback = (err)->)-> + Project.getProject project_or_id, 'existsInVersioningApi', (error, project)=> + return callback error if error? + return callback new Error("project_id:#{project_id} does not exist") if !project? + project_id = project._id + if project.existsInVersioningApi + logger.log project_id: project_id, "versioning already enabled" + return callback() + logger.log project_id: project_id, "enabling versioning in versioning API" + @createProject project_id, (error) -> + return callback error if error? + logger.log project_id: project_id, "enabling versioning in Mongo" + project.existsInVersioningApi = true + update = existsInVersioningApi : true + conditions = _id:project_id + Project.update conditions, update, {}, -> + ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (err) -> + callback(err) + + proxyToVersioningApi : (req, res) -> + metrics.inc "versioning.proxy" + options = + url : settings.apis.versioning.url + req.url + headers : headers + logger.log url: req.url, "proxying to versioning api" + getReq = request.get(options) + getReq.pipe(res) + getReq.on "error", (error) -> + logger.error err: error, "versioning API error" + res.send 500 + + createProject : (project_id, callback) -> + url = "#{settings.apis.versioning.url}/project/#{project_id}" + options = {method:"post", url:url, headers:headers, title:"createVersioningProject"} + queue.enqueue project_id, "standardHttpRequest", options, callback + + takeSnapshot: (project_id, message, sl_req_id, callback = (error) ->)-> + {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) + logger.log project_id: project_id, sl_req_id: sl_req_id, "taking snapshot of project" + + # This isn't critical so we can do it async + rclient.set Keys.buildLastSnapshotKey(project_id), Date.now(), () -> + rclient.srem Keys.projectsToSnapshotKey, project_id, () -> + + DocumentUpdaterHandler.flushProjectToMongo project_id, sl_req_id, (err) -> + return callback(err) if err? + url = "#{settings.apis.versioning.url}/project/#{project_id}/version" + json = version:{message:message} + options = {method:"post", json:json, url:url, headers:headers, title:"takeVersioningSnapshot"} + queue.enqueue project_id, "standardHttpRequest", options, -> + logger.log options:options, project_id, "take snapshot enqueued" + callback() + + + diff --git a/services/web/app/coffee/controllers/AdminController.coffee b/services/web/app/coffee/controllers/AdminController.coffee new file mode 100755 index 0000000000..8ae4794943 --- /dev/null +++ b/services/web/app/coffee/controllers/AdminController.coffee @@ -0,0 +1,128 @@ +logger = require('logger-sharelatex') +_ = require('underscore') +User = require('../models/User').User +Quote = require('../models/Quote').Quote +Project = require('../models/Project').Project +DocumentUpdaterHandler = require('../Features/DocumentUpdater/DocumentUpdaterHandler') +Settings = require('settings-sharelatex') +util = require('util') +redis = require('redis') +rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host) +rclient.auth(Settings.redis.web.password) +RecurlyWrapper = require('../Features/Subscription/RecurlyWrapper') +SubscriptionHandler = require('../Features/Subscription/SubscriptionHandler') +projectEntityHandler = require('../Features/Project/ProjectEntityHandler') +TpdsPollingBackgroundTasks = require("../Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks") +EditorRealTimeController = require("../Features/Editor/EditorRealTimeController") + +module.exports = AdminController = + + index : (req, res)=> + http = require('http') + openSockets = {} + for url, agents of require('http').globalAgent.sockets + openSockets["http://#{url}"] = (agent._httpMessage.path for agent in agents) + for url, agents of require('https').globalAgent.sockets + openSockets["https://#{url}"] = (agent._httpMessage.path for agent in agents) + memory = process.memoryUsage() + io = require("../infrastructure/Server").io + allUsers = io.sockets.clients() + users = [] + allUsers.forEach (user)-> + u = {} + user.get "email", (err, email)-> + u.email = email + user.get "first_name", (err, first_name)-> + u.first_name = first_name + user.get "last_name", (err, last_name)-> + u.last_name = last_name + user.get "project_id", (err, project_id)-> + u.project_id = project_id + user.get "user_id", (err, user_id)-> + u.user_id = user_id + user.get "signup_date", (err, signup_date)-> + u.signup_date = signup_date + user.get "login_count", (err, login_count)-> + u.login_count = login_count + user.get "connected_time", (err, connected_time)-> + now = new Date() + connected_mins = (((now - new Date(connected_time))/1000)/60).toFixed(2) + u.connected_mins = connected_mins + users.push u + + d = new Date() + today = d.getDate()+":"+(d.getMonth()+1)+":"+d.getFullYear()+":" + yesterday = (d.getDate()-1)+":"+(d.getMonth()+1)+":"+d.getFullYear()+":" + + multi = rclient.multi() + multi.get today+"docsets" + multi.get yesterday+"docsets" + multi.exec (err, replys)-> + redisstats = + today: + docsets: replys[0] + compiles: replys[1] + yesterday: + docsets: replys[2] + compiles: replys[3] + DocumentUpdaterHandler.getNumberOfDocsInMemory (err, numberOfInMemoryDocs)=> + User.count (err, totalUsers)-> + Project.count (err, totalProjects)-> + res.render 'admin', + title: 'System Admin' + currentConnectedUsers:allUsers.length + users: users + numberOfAceDocs : numberOfInMemoryDocs + totalUsers: totalUsers + totalProjects: totalProjects + openSockets: openSockets + redisstats: redisstats + + dissconectAllUsers: (req, res)=> + logger.warn "disconecting everyone" + EditorRealTimeController.emitToAll 'forceDisconnect', "Sorry, we are performing a quick update to the editor and need to close it down. Please refresh the page to continue." + res.send(200) + + closeEditor : (req, res)-> + logger.warn "closing editor" + Settings.editorIsOpen = req.body.isOpen + res.send(200) + + writeAllToMongo : (req, res)-> + logger.log "writing all docs to mongo" + Settings.mongo.writeAll = true + DocumentUpdaterHandler.flushAllDocsToMongo ()-> + logger.log "all docs have been saved to mongo" + res.send() + + addQuote : (req, res)-> + quote = new Quote + author: req.body.author + quote: req.body.quote + quote.save (err)-> + res.send 200 + + syncUserToSubscription: (req, res)-> + {user_id, subscription_id} = req.body + RecurlyWrapper.getSubscription subscription_id, {}, (err, subscription)-> + User.findById user_id, (err, user)-> + SubscriptionHandler.syncSubscriptionToUser subscription, user._id, (err)-> + logger.log user_id:user_id, subscription_id:subscription_id, "linked account to subscription" + res.send() + + flushProjectToTpds: (req, res)-> + projectEntityHandler.flushProjectToThirdPartyDataStore req.body.project_id, (err)-> + res.send 200 + + pollUsersWithDropbox: (req, res)-> + TpdsPollingBackgroundTasks.pollUsersWithDropbox -> + res.send 200 + + updateProjectCompiler: (req, res, next = (error) ->)-> + Project.findOne _id: req.body.project_id, (error, project) -> + return next(error) if error? + project.useClsi2 = (req.body.new == "new") + logger.log project_id: req.body.project_id, useClsi2: project.useClsi2, "updating project compiler" + project.save (error) -> + return next(error) if error? + res.send(200) \ No newline at end of file diff --git a/services/web/app/coffee/controllers/HomeController.coffee b/services/web/app/coffee/controllers/HomeController.coffee new file mode 100755 index 0000000000..ca93692a16 --- /dev/null +++ b/services/web/app/coffee/controllers/HomeController.coffee @@ -0,0 +1,53 @@ +logger = require('logger-sharelatex') +_ = require('underscore') +User = require('./UserController') +Quotes = require('../models/Quote').Quote + + +module.exports = + index : (req,res)-> + if req.session.user + if req.query.scribtex_path? + res.redirect "/project?scribtex_path=#{req.query.scribtex_path}" + else + res.redirect '/project' + else + res.render 'homepage/home', + title: 'ShareLaTeX.com' + + comments : (req, res)-> + res.render 'homepage/comments.jade', + title: 'User Comments' + + resources : (req, res)-> + res.render 'resources.jade', + title: 'LaTeX Resources' + + tos : (req, res) -> + res.render 'about/tos', + title: "Terms of Service" + + privacy : (req, res) -> + res.render 'about/privacy', + title: "Privacy Policy" + + about : (req, res) -> + res.render 'about/about', + title: "About us" + + notFound: (req, res)-> + res.statusCode = 404 + res.render 'general/404', + title: "Page Not Found" + + security : (req, res) -> + res.render 'about/security', + title: "Security" + + attribution: (req, res) -> + res.render 'about/attribution', + title: "Attribution" + + planned_maintenance: (req, res) -> + res.render 'about/planned_maintenance', + title: "Planned Maintenance" diff --git a/services/web/app/coffee/controllers/InfoController.coffee b/services/web/app/coffee/controllers/InfoController.coffee new file mode 100755 index 0000000000..fd01504a77 --- /dev/null +++ b/services/web/app/coffee/controllers/InfoController.coffee @@ -0,0 +1,12 @@ +module.exports= + themes : (req, res)=> + res.render "info/themes", + title: 'Themes' + + dropbox: (req, res)-> + res.render "info/dropbox", + title: 'Dropbox with LaTeX' + + advisor: (req, res)-> + res.render "info/advisor", + title: 'Advisor Program' \ No newline at end of file diff --git a/services/web/app/coffee/controllers/ProjectController.coffee b/services/web/app/coffee/controllers/ProjectController.coffee new file mode 100755 index 0000000000..c3f80a2bdd --- /dev/null +++ b/services/web/app/coffee/controllers/ProjectController.coffee @@ -0,0 +1,218 @@ +User = require('../models/User').User +Project = require('../models/Project').Project +sanitize = require('validator').sanitize +path = require "path" +logger = require('logger-sharelatex') +_ = require('underscore') +fs = require('fs') +ProjectHandler = require '../handlers/ProjectHandler' +SecurityManager = require '../managers/SecurityManager' +GuidManager = require '../managers/GuidManager' +Settings = require('settings-sharelatex') +projectCreationHandler = require '../Features/Project/ProjectCreationHandler' +projectLocator = require '../Features/Project/ProjectLocator' +projectDuplicator = require('../Features/Project/ProjectDuplicator') +ProjectZipStreamManager = require '../Features/Downloads/ProjectZipStreamManager' +metrics = require('../infrastructure/Metrics') +TagsHandler = require('../Features/Tags/TagsHandler') +SubscriptionLocator = require("../Features/Subscription/SubscriptionLocator") +SubscriptionFormatters = require("../Features/Subscription/SubscriptionFormatters") +FileStoreHandler = require("../Features/FileStore/FileStoreHandler") + +module.exports = class ProjectController + constructor: (@collaberationManager)-> + ProjectHandler = new ProjectHandler() + + list: (req, res, next)-> + timer = new metrics.Timer("project-list") + user_id = req.session.user._id + startTime = new Date() + User.findById user_id, (error, user) -> + logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - User.findById" + startTime = new Date() + # TODO: Remove this one month after the ability to start free trials was removed + SubscriptionLocator.getUsersSubscription user._id, (err, subscription)-> + logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - Subscription.getUsersSubscription" + startTime = new Date() + return next(error) if error? + # TODO: Remove this one month after the ability to start free trials was removed + if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? + freeTrial = + expired: !!subscription.freeTrial.downgraded + expiresAt: SubscriptionFormatters.formatDate(subscription.freeTrial.expiresAt) + TagsHandler.getAllTags user_id, (err, tags, tagsGroupedByProject)-> + logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - TagsHandler.getAllTags" + startTime = new Date() + Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel', (projects, collabertions, readOnlyProjects)-> + logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - Project.findAllUsersProjects" + startTime = new Date() + for project in projects + project.accessLevel = "owner" + for project in collabertions + project.accessLevel = "readWrite" + for project in readOnlyProjects + project.accessLevel = "readOnly" + projects = projects.concat(collabertions).concat(readOnlyProjects) + projects = projects.map (project)-> + project.tags = tagsGroupedByProject[project._id] || [] + return project + tags = _.sortBy tags, (tag)-> + -tag.project_ids.length + logger.log projects:projects, collabertions:collabertions, readOnlyProjects:readOnlyProjects, user_id:user_id, "rendering project list" + sortedProjects = _.sortBy projects, (project)-> + return - project.lastUpdated + res.render 'project/list', + title:'Your Projects' + priority_title: true + projects: sortedProjects + freeTrial: freeTrial + tags:tags + projectTabActive: true + logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - Finished" + timer.done() + + apiNewProject: (req, res)-> + user = req.session.user + projectName = sanitize(req.body.projectName).xss() + template = sanitize(req.body.template).xss() + logger.log user: user, type: template, name: projectName, "creating project" + if template == 'example' + projectCreationHandler.createExampleProject user._id, projectName, (err, project)-> + if err? + logger.error err: err, project: project, user: user, name: projectName, type: "example", "error creating project" + res.send 500 + else + logger.log project: project, user: user, name: projectName, type: "example", "created project" + res.send {project_id:project._id} + else + projectCreationHandler.createBasicProject user._id, projectName, (err, project)-> + if err? + logger.error err: err, project: project, user: user, name: projectName, type: "basic", "error creating project" + res.send 500 + else + logger.log project: project, user: user, name: projectName, type: "basic", "created project" + res.send {project_id:project._id} + + loadEditor: (req, res)-> + timer = new metrics.Timer("load-editor") + if !Settings.editorIsOpen + res.render("general/closed", {title:"updating site"}) + else + if req.session.user? + user_id = req.session.user._id + else + user_id = 'openUser' + project_id = req.params.Project_id + Project.findPopulatedById project_id, (err, project)-> + User.findById user_id, (err, user)-> + if user_id == 'openUser' + anonymous = true + user = + id : user_id + ace: + mode:'none' + theme:'textmate' + fontSize: '12' + autoComplete: true + spellCheckLanguage: "" + pdfViewer: "" + subscription: + freeTrial: + allowed: true + featureSwitches: + dropbox: false + longPolling: false + else + anonymous = false + SubscriptionLocator.getUsersSubscription user._id, (err, subscription)-> + SecurityManager.userCanAccessProject user, project, (canAccess, privlageLevel)-> + allowedFreeTrial = true + if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? + allowedFreeTrial = !!subscription.freeTrial.allowed + if canAccess + timer.done() + res.render 'project/editor', + title: project.name + priority_title: true + bodyClasses: ["editor"] + project : project + owner : project.owner_ref + userObject : JSON.stringify({ + id : user.id + email : user.email + first_name : user.first_name + last_name : user.last_name + referal_id : user.referal_id + subscription : + freeTrial: {allowed: allowedFreeTrial} + }) + userSettingsObject: JSON.stringify({ + mode : user.ace.mode + theme : user.ace.theme + project_id : project._id + fontSize : user.ace.fontSize + autoComplete: user.ace.autoComplete + spellCheckLanguage: user.ace.spellCheckLanguage + pdfViewer : user.ace.pdfViewer + docPositions: {} + longPolling: user.featureSwitches.longPolling + }) + sharelatexObject : JSON.stringify({ + siteUrl: Settings.siteUrl, + jsPath: res.locals.jsPath + }) + privlageLevel: privlageLevel + userCanSeeDropbox: user.featureSwitches.dropbox and project.owner_ref._id+"" == user._id+"" + loadPdfjs: (user.ace.pdfViewer == "pdfjs") + chatUrl: Settings.apis.chat.url + anonymous: anonymous + languages: Settings.languages, + + startBufferingRequest: (req, res, next) -> + req.bufferedChunks = [] + req.endEmitted = false + bufferChunk = (chunk) -> req.bufferedChunks.push(chunk) + req.on "data", bufferChunk + endCallback = () -> req.endEmitted = true + req.on "end", endCallback + req.emitBufferedData = () -> + logger.log chunks: @bufferedChunks.length, emittedEnd: @endEmitted, "emitting buffer chunks" + @removeListener "data", bufferChunk + while @bufferedChunks.length > 0 + @emit "data", @bufferedChunks.shift() + @removeListener "end", endCallback + @emit "end" if @endEmitted + next() + + downloadImageFile : (req, res)-> + project_id = req.params.Project_id + file_id = req.params.File_id + queryString = req.query + logger.log project_id: project_id, file_id: file_id, queryString:queryString, "file download" + res.setHeader("Content-Disposition", "attachment") + FileStoreHandler.getFileStream project_id, file_id, queryString, (err, stream)-> + stream.pipe res + + cloneProject: (req, res)-> + metrics.inc "cloned-project" + project_id = req.params.Project_id + projectName = req.body.projectName + logger.log project_id:project_id, projectName:projectName, "cloning project" + if !req.session.user? + return res.send redir:"/register" + projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)-> + if err? + logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project" + return next(err) + res.send(project_id:project._id) + + deleteProject: (req, res)-> + project_id = req.params.Project_id + logger.log project_id:project_id, "deleting project" + ProjectHandler.deleteProject project_id, (err)-> + if err? + res.send 500 + else + res.send 200 + + diff --git a/services/web/app/coffee/controllers/UserController.coffee b/services/web/app/coffee/controllers/UserController.coffee new file mode 100644 index 0000000000..92e8fe0d2d --- /dev/null +++ b/services/web/app/coffee/controllers/UserController.coffee @@ -0,0 +1,237 @@ +User = require('../models/User').User +sanitize = require('validator').sanitize +fs = require('fs') +_ = require('underscore') +emailer = require('../managers/EmailManager') +logger = require('logger-sharelatex') +Security = require('../managers/SecurityManager') +Settings = require('settings-sharelatex') +newsLetterManager = require('../managers/NewsletterManager') +dropboxHandler = require('../Features/Dropbox/DropboxHandler') +userRegistrationHandler = require('../Features/User/UserRegistrationHandler') +metrics = require('../infrastructure/Metrics') +AnalyticsManager = require('../Features/Analytics/AnalyticsManager') +ReferalAllocator = require('../Features/Referal/ReferalAllocator') +AuthenticationManager = require("../Features/Authentication/AuthenticationManager") +AuthenticationController = require("../Features/Authentication/AuthenticationController") +SubscriptionLocator = require("../Features/Subscription/SubscriptionLocator") +UserDeleter = require("../Features/User/UserDeleter") +Url = require("url") + +module.exports = + + registerForm : (req, res)-> + + sharedProjectData = + project_name:req.query.project_name + user_first_name:req.query.user_first_name + + newTemplateData = {} + if req.session.templateData? + newTemplateData.templateName = req.session.templateData.templateName + + res.render 'user/register', + title: 'Register' + redir: req.query.redir + sharedProjectData: sharedProjectData + newTemplateData: newTemplateData + new_email:req.query.new_email || "" + + + loginForm : (req, res)-> + res.render 'user/login', + title: 'Login', + redir: req.query.redir + + apiRegister : (req, res, next = (error) ->)-> + logger.log email: req.body.email, "attempted register" + redir = Url.parse(req.body.redir or "/project").path + userRegistrationHandler.validateRegisterRequest req, (err, data)-> + if err? + logger.log validation_error: err, "user validation error" + metrics.inc "user.register.validation-error" + res.send message: + text:err + type:'error' + else + User.findOne {email:data.email}, (err, foundUser)-> + if foundUser? && foundUser.holdingAccount == false + AuthenticationController.login req, res + logger.log email: data.email, "email already registered" + metrics.inc "user.register.already-registered" + return AuthenticationController.login req, res + else if foundUser? && foundUser.holdingAccount == true #someone put them in as a collaberator + user = foundUser + user.holdingAccount == false + else + user = new User email: data.email + d = new Date() + user.first_name = data.first_name + user.last_name = data.last_name + user.signUpDate = new Date() + metrics.inc "user.register.success" + user.save (err)-> + req.session.user = user + req.session.justRegistered = true + logger.log user: user, "registered" + AuthenticationManager.setUserPassword user._id, data.password, (error) -> + return next(error) if error? + res.send + redir:redir + id:user._id.toString() + first_name: user.first_name + last_name: user.last_name + email: user.email + created: Date.now() + #things that can be fired and forgot. + newsLetterManager.subscribe user + ReferalAllocator.allocate req.session.referal_id, user._id, req.session.referal_source, req.session.referal_medium + + requestPasswordReset : (req, res)-> + res.render 'user/passwordReset', + title: 'Password Reset', + + doRequestPasswordReset : (req, res, next = (error) ->)-> + email = sanitize(req.body.email).xss() + email = sanitize(email).trim() + email = email.toLowerCase() + logger.log email: email, "password reset requested" + User.findOne {'email':email}, (err, user)-> + if(user?) + randomPassword = generateRandomString 12 + AuthenticationManager.setUserPassword user._id, randomPassword, (error) -> + return next(error) if error? + emailOptions = + receiver : user.email + subject : "Password Reset - ShareLatex.com" + heading : "Password Reset" + message : " Your password has been reset, the new password is

#{randomPassword} +

please login click here + " + emailer.sendEmail emailOptions + metrics.inc "user.password-reset" + res.send message: + text:'An email with your new password has been sent to you' + type:'success' + else + res.send message: + text:'This email address has not been registered with us' + type:'failure' + logger.info email: email, "no user found with email" + + logout : (req, res)-> + metrics.inc "user.logout" + if req.session? && req.session.user? + logger.log user: req.session.user, "logging out" + req.session.destroy (err)-> + if err + logger.err err: err, 'error destorying session' + res.redirect '/login' + + settings : (req, res)-> + logger.log user: req.session.user, "loading settings page" + User.findById req.session.user._id, (err, user)-> + dropboxHandler.getUserRegistrationStatus user._id, (err, status)-> + userIsRegisteredWithDropbox = !err? and status.registered + res.render 'user/settings', + title:'Your settings', + userCanSeeDropbox: user.featureSwitches.dropbox + userHasDropboxFeature: user.features.dropbox + userIsRegisteredWithDropbox: userIsRegisteredWithDropbox + user: user, + themes: THEME_LIST, + editors: ['default','vim','emacs'], + fontSizes: ['10','11','12','13','14','16','20','24'] + languages: Settings.languages, + accountSettingsTabActive: true + + unsubscribe: (req, res)-> + User.findById req.session.user._id, (err, user)-> + newsLetterManager.unsubscribe user, -> + res.send() + + apiUpdate : (req, res)-> + logger.log user: req.session.user, "updating account settings" + metrics.inc "user.settings-update" + User.findById req.session.user._id, (err, user)-> + if(user) + user.first_name = sanitize(req.body.first_name).xss().trim() + user.last_name = sanitize(req.body.last_name).xss().trim() + user.ace.mode = sanitize(req.body.mode).xss().trim() + user.ace.theme = sanitize(req.body.theme).xss().trim() + user.ace.fontSize = sanitize(req.body.fontSize).xss().trim() + user.ace.autoComplete = req.body.autoComplete == "true" + user.ace.spellCheckLanguage = req.body.spellCheckLanguage + user.ace.pdfViewer = req.body.pdfViewer + user.save() + res.send {} + + changePassword : (req, res, next = (error) ->)-> + metrics.inc "user.password-change" + oldPass = req.body.currentPassword + AuthenticationManager.authenticate _id: req.session.user._id, oldPass, (err, user)-> + if(user) + logger.log user: req.session.user, "changing password" + newPassword1 = req.body.newPassword1 + newPassword2 = req.body.newPassword2 + if newPassword1 != newPassword2 + logger.log user: user, "passwords do not match" + res.send + message: + type:'error' + text:'Your passwords do not match' + else + logger.log user: user, "password changed" + AuthenticationManager.setUserPassword user._id, newPassword1, (error) -> + return next(error) if error? + res.send + message: + type:'success' + text:'Your password has been changed' + else + logger.log user: user, "current password wrong" + res.send + message: + type:'error' + text:'Your old password is wrong' + + redirectUserToDropboxAuth: (req, res)-> + user_id = req.session.user._id + dropboxHandler.getDropboxRegisterUrl user_id, (err, url)-> + logger.log url:url, "redirecting user for dropbox auth" + res.redirect url + + completeDropboxRegistration: (req, res)-> + user_id = req.session.user._id + dropboxHandler.completeRegistration user_id, (err, success)-> + res.redirect('/user/settings#dropboxSettings') + + unlinkDropbox: (req, res)-> + user_id = req.session.user._id + dropboxHandler.unlinkAccount user_id, (err, success)-> + res.redirect('/user/settings#dropboxSettings') + + deleteUser: (req, res)-> + user_id = req.session.user._id + UserDeleter.deleteUser user_id, (err)-> + if !err? + req.session.destroy() + res.send(200) + + +generateRandomString = (len)-> + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz" + randomString = '' + count = 0 + while count++ < len + rnum = Math.floor(Math.random() * chars.length) + randomString += chars.substring(rnum,rnum+1) + return randomString + +THEME_LIST = [] +do generateThemeList = () -> + files = fs.readdirSync __dirname + '/../../../public/js/ace/theme' + for file in files + if file.slice(-2) == "js" + cleanName = file.slice(0,-3) + THEME_LIST.push name: cleanName diff --git a/services/web/app/coffee/errors.coffee b/services/web/app/coffee/errors.coffee new file mode 100644 index 0000000000..4a29822efc --- /dev/null +++ b/services/web/app/coffee/errors.coffee @@ -0,0 +1,10 @@ +NotFoundError = (message) -> + error = new Error(message) + error.name = "NotFoundError" + error.__proto__ = NotFoundError.prototype + return error +NotFoundError.prototype.__proto__ = Error.prototype + +module.exports = Errors = + NotFoundError: NotFoundError + diff --git a/services/web/app/coffee/handlers/ProjectHandler.coffee b/services/web/app/coffee/handlers/ProjectHandler.coffee new file mode 100755 index 0000000000..c24685c70d --- /dev/null +++ b/services/web/app/coffee/handlers/ProjectHandler.coffee @@ -0,0 +1,164 @@ +Project = require('../models/Project').Project +Folder = require('../models/Folder').Folder +Doc = require('../models/Doc').Doc +File = require('../models/File').File +User = require('../models/User').User +logger = require('logger-sharelatex') +_ = require('underscore') +Settings = require('settings-sharelatex') +emailer = require('../managers/EmailManager') +tpdsUpdateSender = require '../Features/ThirdPartyDataStore/TpdsUpdateSender' +projectCreationHandler = require '../Features/Project/ProjectCreationHandler' +projectEntityHandler = require '../Features/Project/ProjectEntityHandler' +ProjectEditorHandler = require '../Features/Project/ProjectEditorHandler' +FileStoreHandler = require "../Features/FileStore/FileStoreHandler" +projectLocator = require '../Features/Project/ProjectLocator' +mimelib = require("mimelib") +async = require('async') +tagsHandler = require('../Features/Tags/TagsHandler') + +module.exports = class ProjectHandler + getProject: (project_id, callback)-> + logger.log project_id: project_id, "getting project" + Project.findById project_id, (err, project)-> + callback err, ProjectEditorHandler.buildProjectModelView(project, includeUsers: false) + + confirmFolder = (project_id, folder_id, callback)-> + logger.log folder: folder_id, project_id: project_id, "confirming existence of folder" + if folder_id+'' == 'undefined' + Project.findById project_id, (err, project)-> + callback(project.rootFolder[0]._id) + else if folder_id != null + callback folder_id + else + Project.findById project_id, (err, project)-> + callback(project.rootFolder[0]._id) + + renameEntity: (project_id, entity_id, entityType, newName, callback)-> + logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType)) + if !entityType? + logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id + return callback("No entityType set") + entityType = entityType.toLowerCase() + Project.findById project_id, (err, project)=> + projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path, folder)=> + if err? + return callback err + conditons = {_id:project_id} + update = "$set":{} + namePath = path.mongo+".name" + update["$set"][namePath] = newName + endPath = path.fileSystem.replace(entity.name, newName) + tpdsUpdateSender.moveEntity({project_id:project_id, startPath:path.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev}) + Project.update conditons, update, {}, (err)-> + if callback? + callback err + + renameProject: (project_id, window_id, newName, callback)-> + logger.log project_id: project_id, "renaming project" + conditons = {_id:project_id} + Project.findOne conditons, "name", (err, project)-> + oldProjectName = project.name + Project.update conditons, {name: newName}, {},(err, project)=> + tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName} + if callback? + callback err + + deleteProject: (project_id, callback = (error) ->)-> + logger.log project_id:project_id, "deleting project" + Project.findById project_id, (err, project)=> + if project? + require('../Features/DocumentUpdater/DocumentUpdaterHandler').flushProjectToMongoAndDelete project_id, (error) -> + return callback(error) if error? + Project.applyToAllFilesRecursivly project.rootFolder[0], (file)=> + FileStoreHandler.deleteFile project_id, file._id, -> + Project.remove {_id:project_id}, (err)-> + if callback? + callback(err) + require('../Features/Versioning/AutomaticSnapshotManager').unmarkProjectAsUpdated project_id, -> + tagsHandler.removeProjectFromAllTags project.owner_ref, project_id,-> + project.collaberator_refs.forEach (collaberator_ref)-> + tagsHandler.removeProjectFromAllTags collaberator_ref, project_id, -> + project.readOnly_refs.forEach (readOnly_ref)-> + tagsHandler.removeProjectFromAllTags readOnly_ref, project_id,-> + else + if callback? + callback(err) + + setPublicAccessLevel : (project_id, newAccessLevel, callback)-> + logger.log project_id: project_id, level: newAccessLevel, "set public access level" + if project_id? && newAccessLevel? + if _.include ['readOnly', 'readAndWrite', 'private'], newAccessLevel + Project.update {_id:project_id},{publicAccesLevel:newAccessLevel},{}, (err)-> + if callback? + callback() + + addUserToProject: (project_id, email, privlages, callback)-> + if email != '' + doAdd = (user)=> + Project.findOne(_id: project_id ) + .select("name owner_ref") + .populate('owner_ref') + .exec (err, project)-> + emailOptions = + receiver : email + replyTo : project.owner_ref.email + subject : "#{project.owner_ref.first_name} #{project.owner_ref.last_name} wants to share '#{project.name}' with you" + heading : "#{project.name} #{project.owner_ref.last_name} wants to share '#{project.name}' with you" + message : " + " + template_name:"shared_project_email_template" + view_data: + project: + name: project.name + url: "#{Settings.siteUrl}/project/#{project._id}?" + [ + "project_name=#{project.name}" + "user_first_name=#{project.owner_ref.first_name}" + "new_email=#{email}" + "r=#{project.owner_ref.referal_id}" # Referal + "rs=ci" # referral source = collaborator invite + ].join("&") + owner: + first_name: project.owner_ref.first_name + email: project.owner_ref.email + sharelatex_url: Settings.siteUrl + + emailer.sendEmail emailOptions + if privlages == 'readAndWrite' + level = {"collaberator_refs":user} + logger.log privileges: "readAndWrite", user: user, project: project, "adding user" + else if privlages == 'readOnly' + level = {"readOnly_refs":user} + logger.log privileges: "readOnly", user: user, project: project, "adding user" + Project.update {_id: project_id}, {$push:level},{},(err)-> + projectEntityHandler.flushProjectToThirdPartyDataStore project_id, "", -> + if callback? + callback(user) + + emails = mimelib.parseAddresses(email) + email = emails[0].address + User.findOne {'email':email}, (err, user)-> + if(!user) + user = new User 'email':email, holdingAccount:true + user.save (err)-> + logger.log user: user, 'creating new empty user' + doAdd user + else + doAdd user + + removeUserFromProject: (project_id, user_id, callback)-> + logger.log user_id: user_id, project_id: project_id, "removing user" + conditions = _id:project_id + update = $pull:{} + update["$pull"] = collaberator_refs:user_id, readOnly_refs:user_id + Project.update conditions, update, {}, (err)-> + if err? + logger.err err: err, "problem removing user from project collaberators" + if callback? + callback() + + changeUsersPrivlageLevel: (project_id, user_id, newPrivalageLevel)-> + @removeUserFromProject project_id, user_id, ()=> + User.findById user_id, (err, user)=> + if user + @addUserToProject project_id, user.email, newPrivalageLevel diff --git a/services/web/app/coffee/infrastructure/BackgroundTasks.coffee b/services/web/app/coffee/infrastructure/BackgroundTasks.coffee new file mode 100644 index 0000000000..6433efc9e8 --- /dev/null +++ b/services/web/app/coffee/infrastructure/BackgroundTasks.coffee @@ -0,0 +1,7 @@ +EditorUpdatesController = require("../Features/Editor/EditorUpdatesController") +EditorRealTimeController = require("../Features/Editor/EditorRealTimeController") + +module.exports = BackgroundTasks = + run: () -> + EditorUpdatesController.listenForUpdatesFromDocumentUpdater() + EditorRealTimeController.listenForEditorEvents() diff --git a/services/web/app/coffee/infrastructure/CrawlerLogger.coffee b/services/web/app/coffee/infrastructure/CrawlerLogger.coffee new file mode 100644 index 0000000000..e6900ec994 --- /dev/null +++ b/services/web/app/coffee/infrastructure/CrawlerLogger.coffee @@ -0,0 +1,11 @@ +metrics = require('./Metrics') +module.exports = + log: (req)-> + if req.headers["user-agent"]? + userAgent = req.headers["user-agent"].toLowerCase() + if userAgent.indexOf("google") != -1 + metrics.inc "crawler.google" + else if userAgent.indexOf("facebook") != -1 + metrics.inc "crawler.facebook" + else if userAgent.indexOf("bing") != -1 + metrics.inc "crawler.bing" diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee new file mode 100644 index 0000000000..654d74b711 --- /dev/null +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -0,0 +1,122 @@ +logger = require 'logger-sharelatex' +fs = require 'fs' +crypto = require 'crypto' +Settings = require('settings-sharelatex') +SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters') +querystring = require('querystring') + +fingerprints = {} +Path = require 'path' +jsPath = + if Settings.useMinifiedJs + "/minjs/" + else + "/js/" + +logger.log "Generating file fingerprints..." +for path in [ + "#{jsPath}libs/require.js", + "#{jsPath}ide.js", + "#{jsPath}main.js", + "#{jsPath}list.js", + "#{jsPath}libs/pdf.js", + "#{jsPath}libs/pdf.worker.js", + "/stylesheets/mainStyle.css" +] + filePath = Path.join __dirname, "../../../", "public#{path}" + content = fs.readFileSync filePath + hash = crypto.createHash("md5").update(content).digest("hex") + logger.log "#{filePath}: #{hash}" + fingerprints[path] = hash + +module.exports = (app)-> + app.use (req, res, next)-> + res.locals.session = req.session + next() + + app.use (req, res, next)-> + res.locals.jsPath = jsPath + next() + + app.use (req, res, next)-> + res.locals.settings = Settings + next() + + app.use (req, res, next)-> + res.locals.getSiteHost = -> + Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2) + next() + + app.use (req, res, next)-> + res.locals.formatPrivlageLevel = (privlageLevel)-> + formatedPrivlages = private:"Private", readOnly:"Read Only", readAndWrite:"Read and Write" + return formatedPrivlages[privlageLevel] || "Private" + next() + + app.use (req, res, next)-> + res.locals.buildReferalUrl = (referal_medium) -> + url = Settings.siteUrl + if req.session? and req.session.user? and req.session.user.referal_id? + url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus + return url + res.locals.getReferalId = -> + if req.session? and req.session.user? and req.session.user.referal_id + return req.session.user.referal_id + res.locals.getReferalTagLine = -> + tagLines = [ + "Roar!" + "Shout about us!" + "Please recommend us" + "Tell the world!" + "Thanks for using ShareLaTeX" + ] + return tagLines[Math.floor(Math.random()*tagLines.length)] + res.locals.getRedirAsQueryString = -> + if req.query.redir? + return "?#{querystring.stringify({redir:req.query.redir})}" + return "" + next() + + app.use (req, res, next) -> + res.locals.csrfToken = req.session._csrf + next() + + app.use (req, res, next)-> + res.locals.fingerprint = (path) -> + if fingerprints[path]? + return fingerprints[path] + else + logger.err "No fingerprint for file: #{path}" + return "" + next() + app.use (req, res, next)-> + res.locals.formatPrice = SubscriptionFormatters.formatPrice + next() + + app.use (req, res, next)-> + if req.session.user? + res.locals.mixpanelId = req.session.user._id + res.locals.user = + email: req.session.user.email + first_name: req.session.user.first_name + last_name: req.session.user.last_name + if req.session.justRegistered + res.locals.justRegistered = true + delete req.session.justRegistered + if req.session.justLoggedIn + res.locals.justLoggedIn = true + delete req.session.justLoggedIn + res.locals.mixpanelToken = Settings.analytics?.mixpanel?.token + res.locals.gaToken = Settings.analytics?.ga?.token + res.locals.heapToken = Settings.analytics?.heap?.token + res.locals.tenderUrl = Settings.tenderUrl + next() + + app.use (req, res, next) -> + if req.query? and req.query.scribtex_path? + res.locals.lookingForScribtex = true + res.locals.scribtexPath = req.query.scribtex_path + next() + + + diff --git a/services/web/app/coffee/infrastructure/Keys.coffee b/services/web/app/coffee/infrastructure/Keys.coffee new file mode 100644 index 0000000000..2966f35aa3 --- /dev/null +++ b/services/web/app/coffee/infrastructure/Keys.coffee @@ -0,0 +1,5 @@ +module.exports = + + queue: + web_to_tpds_http_requests: "web_to_tpds_http_requests" + tpds_to_web_http_requests: "tpds_to_web_http_requests" diff --git a/services/web/app/coffee/infrastructure/LoggerSerializers.coffee b/services/web/app/coffee/infrastructure/LoggerSerializers.coffee new file mode 100644 index 0000000000..f496b8cdad --- /dev/null +++ b/services/web/app/coffee/infrastructure/LoggerSerializers.coffee @@ -0,0 +1,18 @@ +module.exports = + user: (user) -> + if !user._id? + user = {_id : user} + return { + id: user._id + email: user.email + first_name: user.name + last_name: user.name + } + + project: (project) -> + if !project._id? + project = {_id: project} + return { + id: project._id + name: project.name + } diff --git a/services/web/app/coffee/infrastructure/Metrics.coffee b/services/web/app/coffee/infrastructure/Metrics.coffee new file mode 100644 index 0000000000..7a972551fd --- /dev/null +++ b/services/web/app/coffee/infrastructure/Metrics.coffee @@ -0,0 +1,24 @@ +StatsD = require('lynx') +settings = require('settings-sharelatex') +statsd = new StatsD('localhost', 8125, {on_error:->}) + +buildKey = (key)-> "web.#{process.env.NODE_ENV}.#{key}" + +module.exports = + set : (key, value, sampleRate = 1)-> + statsd.set buildKey(key), value, sampleRate + + inc : (key, sampleRate = 1)-> + statsd.increment buildKey(key), sampleRate + + Timer : class + constructor :(key, sampleRate = 1)-> + this.start = new Date() + this.key = buildKey(key) + done:-> + timeSpan = new Date - this.start + statsd.timing(this.key, timeSpan, this.sampleRate) + + gauge : (key, value, sampleRate = 1)-> + statsd.gauge key, value, sampleRate + diff --git a/services/web/app/coffee/infrastructure/Monitor.coffee b/services/web/app/coffee/infrastructure/Monitor.coffee new file mode 100644 index 0000000000..9f219ae3f7 --- /dev/null +++ b/services/web/app/coffee/infrastructure/Monitor.coffee @@ -0,0 +1,5 @@ +require("./Monitor/MongoDB").monitor() + +exports.logger = require("./Monitor/HTTP").logger + + diff --git a/services/web/app/coffee/infrastructure/Monitor/HTTP.coffee b/services/web/app/coffee/infrastructure/Monitor/HTTP.coffee new file mode 100644 index 0000000000..0ad1df4ac9 --- /dev/null +++ b/services/web/app/coffee/infrastructure/Monitor/HTTP.coffee @@ -0,0 +1,21 @@ +logger = require "logger-sharelatex" + +module.exports.logger = (req, res, next) -> + startTime = new Date() + end = res.end + res.end = () -> + end.apply(this, arguments) + logger.log + req: + url: req.originalUrl || req.url + method: req.method + referrer: req.headers['referer'] || req.headers['referrer'] + "remote-addr": req.ip || req.socket?.socket?.remoteAddress || req.socket?.remoteAddress + "user-agent": req.headers["user-agent"] + "content-length": req.headers["content-length"] + res: + "content-length": res._headers?["content-length"] + "response-time": new Date() - startTime + "http request" + next() + diff --git a/services/web/app/coffee/infrastructure/Monitor/MongoDB.coffee b/services/web/app/coffee/infrastructure/Monitor/MongoDB.coffee new file mode 100644 index 0000000000..101639eb95 --- /dev/null +++ b/services/web/app/coffee/infrastructure/Monitor/MongoDB.coffee @@ -0,0 +1,57 @@ +Connection = require("mongoose/node_modules/mongodb/lib/mongodb/connection/connection").Connection +MongoReply = require("mongoose/node_modules/mongodb/lib/mongodb/responses/mongo_reply").MongoReply +Metrics = require("../Metrics") +logger = require "logger-sharelatex" +_ = require("underscore") + +connectionMonitor = + newConnection: (id, db) -> + Metrics.inc "mongo-requests" + @connections[id] = + timer: new Metrics.Timer("mongo-request-times") + db: db + start: new Date() + setTimeout (=> @connectionDone(id)), 60000 + + connectionDone: (id) -> + + queryIsNoise = (query)-> + isNoise = false + if query? && _.isObject(query) + keys = _.keys(query) + if keys[0] == "ismaster" or keys[0] == "ping" + isNoise = true + return isNoise + + logItOut = (db)-> + logger.log + request_id: db.requestId, + query: db.query, + collection: db.collectionName, + "response-time": new Date() - start + "mongo request" + + + + if @connections[id]? + @connections[id].timer.done() + db = @connections[id].db + start = @connections[id].start + if !queryIsNoise(db.query) + logItOut(db) + delete @connections[id] + + connections: {} + +monkeyPatchMongo = () -> + write = Connection::write + Connection::write = (db) -> + write.apply(this, arguments) + connectionMonitor.newConnection db.requestId, db + + parseHeader = MongoReply::parseHeader + MongoReply::parseHeader = () -> + parseHeader.apply this, arguments + connectionMonitor.connectionDone this.responseTo + +module.exports.monitor = monkeyPatchMongo diff --git a/services/web/app/coffee/infrastructure/RandomLogging.coffee b/services/web/app/coffee/infrastructure/RandomLogging.coffee new file mode 100644 index 0000000000..c73463ea62 --- /dev/null +++ b/services/web/app/coffee/infrastructure/RandomLogging.coffee @@ -0,0 +1,6 @@ +_ = require('underscore') +metrics = require('./Metrics') + +do trackOpenSockets = -> + metrics.gauge("http.open-sockets", _.size(require('http').globalAgent.sockets.length), 0.5) + setTimeout(trackOpenSockets, 1000) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee new file mode 100644 index 0000000000..2c0866c051 --- /dev/null +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -0,0 +1,102 @@ +express = require('express') +Settings = require('settings-sharelatex') +logger = require 'logger-sharelatex' +metrics = require('./Metrics') +crawlerLogger = require('./CrawlerLogger') +expressLocals = require('./ExpressLocals') +socketIoConfig = require('./SocketIoConfig') +soareqid = require('soa-req-id') +Router = require('../router') +metrics.inc("startup") +redis = require('redis') +RedisStore = require('connect-redis')(express) +SessionSockets = require('session.socket.io') +sessionStore = new RedisStore(host:Settings.redis.web.host, port:Settings.redis.web.port, pass:Settings.redis.web.password) +cookieParser = express.cookieParser(Settings.security.sessionSecret) +oneDayInMilliseconds = 86400000 +ReferalConnect = require('../Features/Referal/ReferalConnect') + +Monitor = require "./Monitor" + +Settings.editorIsOpen ||= true + +if Settings.cacheStaticAssets + staticCacheAge = (oneDayInMilliseconds * 365) +else + staticCacheAge = 0 + +app = express() + +cookieKey = "sharelatex.sid" +cookieSessionLength = 5 * oneDayInMilliseconds + +csrf = express.csrf() +ignoreCsrfRoutes = [] +app.ignoreCsrf = (method, route) -> + ignoreCsrfRoutes.push new express.Route(method, route) + +app.configure ()-> + app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) + app.set 'views', __dirname + '/../../views' + app.set 'view engine', 'jade' + app.use express.bodyParser(uploadDir: __dirname + "/../../../data/uploads") + app.use cookieParser + app.use express.session + proxy: true + cookie: + maxAge: cookieSessionLength + secure: Settings.secureCookie + store: sessionStore + key: cookieKey + + # Measure expiry from last request, not last login + app.use (req, res, next) -> + req.session.expires = Date.now() + cookieSessionLength + next() + + app.use (req, res, next) -> + for route in ignoreCsrfRoutes + if route.method == req.method?.toLowerCase() and route.match(req.path) + return next() + csrf(req, res, next) + + app.use ReferalConnect.use + app.use express.methodOverride() + +expressLocals(app) + +app.configure 'production', -> + logger.info "Production Enviroment" + app.enable('view cache') + +app.use Monitor.logger + +app.use (req, res, next)-> + metrics.inc "http-request" + crawlerLogger.log(req) + next() + +app.use (req, res, next) -> + if !Settings.editorIsOpen + res.status(503) + res.render("general/closed", {title:"Maintenance"}) + else + next() + +app.get "/status", (req, res)-> + res.send("web sharelatex is alive") + req.session.destroy() + +logger.info ("creating HTTP server").yellow +server = require('http').createServer(app) + +io = require('socket.io').listen(server) + +sessionSockets = new SessionSockets(io, sessionStore, cookieParser, cookieKey) +router = new Router(app, io, sessionSockets) +socketIoConfig.configure(io) + +module.exports = + io: io + app: app + server: server diff --git a/services/web/app/coffee/infrastructure/SocketIoConfig.coffee b/services/web/app/coffee/infrastructure/SocketIoConfig.coffee new file mode 100644 index 0000000000..d21dec90f4 --- /dev/null +++ b/services/web/app/coffee/infrastructure/SocketIoConfig.coffee @@ -0,0 +1,20 @@ +SocketIoRedisStore = require('socket.io/lib/stores/redis') + +module.exports = + configure: (io)-> + io.configure -> + io.enable('browser client minification') + io.enable('browser client etag') + + # Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch" + # See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with + io.set('match origin protocol', true) + + # gzip uses a Node 0.8.x method of calling the gzip program which + # doesn't work with 0.6.x + #io.enable('browser client gzip') + io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']) + io.set('log level', 1) + + io.configure 'production', -> + io.set('log level', 1) diff --git a/services/web/app/coffee/infrastructure/mongojs.coffee b/services/web/app/coffee/infrastructure/mongojs.coffee new file mode 100644 index 0000000000..d078911878 --- /dev/null +++ b/services/web/app/coffee/infrastructure/mongojs.coffee @@ -0,0 +1,6 @@ +Settings = require "settings-sharelatex" +mongojs = require "mongojs" +db = mongojs.connect(Settings.mongo.url, ["projects", "users"]) +module.exports = + db: db + ObjectId: mongojs.ObjectId diff --git a/services/web/app/coffee/managers/CollaberationManager.coffee b/services/web/app/coffee/managers/CollaberationManager.coffee new file mode 100644 index 0000000000..ed3aadf87c --- /dev/null +++ b/services/web/app/coffee/managers/CollaberationManager.coffee @@ -0,0 +1,62 @@ +#this file is being slowly refactored out + +logger = require('logger-sharelatex') +sanitize = require('validator').sanitize +projectHandler = require('../handlers/ProjectHandler') +projectHandler = new projectHandler() +SecurityManager = require('./SecurityManager') +_ = require('underscore') +projectEditorHandler = require('../Features/Project/ProjectEditorHandler') +projectEntityHandler = require('../Features/Project/ProjectEntityHandler') +versioningApiHandler = require('../Features/Versioning/VersioningApiHandler') +metrics = require('../infrastructure/Metrics') +EditorRealTimeController = require('../Features/Editor/EditorRealTimeController') + +module.exports = class CollaberationManager + constructor: (@io)-> + + deleteProject: (project_id, callback)-> + metrics.inc "editor.delete-project" + logger.log project_id:project_id, "recived message to delete project" + projectHandler.deleteProject project_id, callback + + renameEntity: (project_id, entity_id, entityType, newName, callback)-> + newName = sanitize(newName).xss() + metrics.inc "editor.rename-entity" + logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project" + projectHandler.renameEntity project_id, entity_id, entityType, newName, => + if newName.length > 0 + EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName + callback?() + + moveEntity: (project_id, entity_id, folder_id, entityType, callback)-> + metrics.inc "editor.move-entity" + projectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, => + EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id + callback?() + + renameProject: (project_id, window_id, newName, callback)-> + newName = sanitize(newName).xss() + projectHandler.renameProject project_id, window_id, newName, => + newName = sanitize(newName).xss() + EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', window_id, newName + callback?() + + setPublicAccessLevel : (project_id, newAccessLevel, callback)-> + projectHandler.setPublicAccessLevel project_id, newAccessLevel, => + EditorRealTimeController.emitToRoom project_id, 'publicAccessLevelUpdated', newAccessLevel + callback?() + + distributMessage: (project_id, client, message)-> + message = sanitize(message).xss() + metrics.inc "editor.instant-message" + client.get "first_name", (err, first_name)=> + EditorRealTimeController.emitToRoom project_id, 'reciveNewMessage', first_name, message + + setRootDoc: (project_id, newRootDocID, callback)-> + projectEntityHandler.setRootDoc project_id, newRootDocID, () => + EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID + callback?() + + takeVersionSnapShot : (project_id, message, callback)-> + versioningApiHandler.takeVersionSnapshot project_id, message, callback diff --git a/services/web/app/coffee/managers/EmailManager.coffee b/services/web/app/coffee/managers/EmailManager.coffee new file mode 100644 index 0000000000..32cc4bce64 --- /dev/null +++ b/services/web/app/coffee/managers/EmailManager.coffee @@ -0,0 +1,45 @@ +logger = require('logger-sharelatex') +Settings = require('settings-sharelatex') +Path = require "path" +_ = require('underscore') +metrics = require('../infrastructure/Metrics') +fs = require("fs") + +if Settings.ses?.key? and Settings.ses?.key != "" and Settings.ses?.secret? and Settings.ses?.secret != "" + ses = require('node-ses') + client = ses.createClient({ key: Settings.ses.key, secret: Settings.ses.secret }); +else + logger.warn "AWS SES credentials are not configured. No emails will be sent." + client = + sendemail: (options, callback = (err, data, res) ->) -> + logger.log options: options, "would send email if SES credentials enabled" + callback() + +module.exports = + sendEmail : (options, callback = (error) ->)-> + logger.log options:options, "sending email" + metrics.inc "email" + template = options.template_name || "emailTemplate" + fs.readFile Path.resolve(__dirname + "/../../templates/email/#{template}.html"), (err, htmlTemplate)-> + logger.error err: err, "error sending email" if err? + return callback(err) if err? + compiledTemplate = _.template htmlTemplate.toString() + htmlMessage = compiledTemplate + previewMessage : options.subject + heading : options.heading + message: options.message + view_data:options.view_data + options = + to: options.receiver + from: "ShareLaTeX " + subject: options.subject + message: htmlMessage + replyTo: options.replyTo || "team@sharelatex.com" + client.sendemail options, (err, data, res)-> + return callback(err) if err? + if err? + logger.log "error sending message" + else + logger.log "Message sent to #{options.to}" + callback() + diff --git a/services/web/app/coffee/managers/GuidManager.coffee b/services/web/app/coffee/managers/GuidManager.coffee new file mode 100644 index 0000000000..c283670282 --- /dev/null +++ b/services/web/app/coffee/managers/GuidManager.coffee @@ -0,0 +1,6 @@ +module.exports = + newGuid : ()-> + S4 = ()-> + return (((1+Math.random())*0x10000)|0).toString(16).substring(1) + return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()) + diff --git a/services/web/app/coffee/managers/NewsletterManager.coffee b/services/web/app/coffee/managers/NewsletterManager.coffee new file mode 100644 index 0000000000..9bac0e7766 --- /dev/null +++ b/services/web/app/coffee/managers/NewsletterManager.coffee @@ -0,0 +1,37 @@ +async = require('async') +Request = require('request') +logger = require 'logger-sharelatex' +Settings = require 'settings-sharelatex' + +module.exports = + subscribe: (user, callback)-> + if !Settings.markdownmail? + logger.warn "No newsletter provider configured so not subscribing user" + return callback() + logger.log user:user, email:user.email, "trying to subscribe user to the mailing list" + options = buildOptions(user, true) + Request.post options, (err, response, body)-> + logger.log body:body, user:user, "finished attempting to subscribe the user to the news letter" + if callback? + callback err + + unsubscribe: (user, callback)-> + if !Settings.markdownmail? + logger.warn "No newsletter provider configured so not unsubscribing user" + return callback() + logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" + options = buildOptions(user, false) + Request.post options, (err, response, body)-> + logger.log err:err, body:body, email:user.email, "compled newsletter unsubscribe attempt" + callback() + +buildOptions = (user, is_subscribed)-> + options = + json: + secret_token: Settings.markdownmail.secret + name: "#{user.first_name} #{user.last_name}" + email: user.email + subscriber_list_id: Settings.markdownmail.list_id + is_subscribed: is_subscribed + url: "https://www.markdownmail.io/lists/subscribe" + return options \ No newline at end of file diff --git a/services/web/app/coffee/managers/SecurityManager.coffee b/services/web/app/coffee/managers/SecurityManager.coffee new file mode 100644 index 0000000000..38d7bdabef --- /dev/null +++ b/services/web/app/coffee/managers/SecurityManager.coffee @@ -0,0 +1,174 @@ +logger = require('logger-sharelatex') +crypto = require 'crypto' +Assert = require 'assert' +Settings = require 'settings-sharelatex' +User = require('../models/User').User +Project = require('../models/Project').Project +HomeController = require("../controllers/HomeController") +AuthenticationController = require("../Features/Authentication/AuthenticationController") +_ = require('underscore') +metrics = require('../infrastructure/Metrics') +querystring = require('querystring') + +module.exports = + restricted : (req, res, next)-> + if req.session.user? + res.render 'user/restricted', + title:'Restricted' + else + logger.log "user not logged in and trying to access #{req.url}, being redirected to login" + res.redirect '/register' + + getCurrentUser: (req, callback) -> + if req.session.user? + User.findById req.session.user._id, callback + else + callback null, null + + requestCanAccessProject : (req, res, next)-> + doRequest = (req, res, next) -> + getRequestUserAndProject req, res, {allow_auth_token: options?.allow_auth_token}, (err, user, project)-> + if !project? + return HomeController.notFound(req, res, next) + userCanAccessProject user, project, (canAccess, permissionLevel)-> + if canAccess + next() + else if user? + logger.log "user_id: #{user._id} email: #{user.email} trying to access restricted page #{req.path}" + res.redirect('/restricted') + else + logger.log "user not logged in and trying to access #{req.url}, being redirected to login" + req.query.redir = req._parsedUrl.pathname + url = "/register?#{querystring.stringify(req.query)}" + res.redirect url + email = "not logged in user" + if arguments.length > 1 + options = + allow_auth_token: false + doRequest.apply(this, arguments) + else + options = req + return doRequest + + requestCanModifyProject : (req, res, next)-> + getRequestUserAndProject req, res, {}, (err, user, project)=> + userCanModifyProject user, project, (canModify)-> + if canModify + next() + else + logger.log "user_id: #{user._id} email: #{user.email} can not modify project redirecting to restricted page" + res.redirect('/restricted') + + userCanModifyProject : userCanModifyProject = (user, project, callback)-> + if !user? or !project? + callback false + else if userIsOwner user, project + callback true + else if userIsCollaberator user, project + callback true + else if project.publicAccesLevel == "readAndWrite" + callback true + else if user.isAdmin + callback true + else + callback false + + + requestIsOwner : (req, res, next)-> + getRequestUserAndProject req, res, {}, (err, user, project)-> + if userIsOwner user, project || user.isAdmin + next() + else + logger.log user_id: user?._id, email: user?.email, "user is not owner of project redirecting to restricted page" + res.redirect('/restricted') + + requestIsAdmin : isAdmin = (req, res, next)-> + logger.log "checking if user is admin" + user = req.session.user + if(user? && user.isAdmin) + logger.log user: user, "User is admin" + next() + else + res.redirect('/restricted') + logger.log user:user, "is not admin redirecting to restricted page" + + userCanAccessProject : userCanAccessProject = (user, project, callback)=> + if !user? + user = {_id:'anonymous-user'} + if !project? + callback false + logger.log user:user, project:project, "Checking if can access" + if userIsOwner user, project + callback true, "owner" + else if userIsCollaberator user, project + callback true, "readAndWrite" + else if userIsReadOnly user, project + callback true, "readOnly" + else if user.isAdmin + logger.log user:user, project:project, "user is admin and can access project" + callback true, "owner" + else if project.publicAccesLevel == "readAndWrite" + logger.log user:user, project:project, "project is a public read and write project" + callback true, "readAndWrite" + else if project.publicAccesLevel == "readOnly" + logger.log user:user, project:project, "project is a public read only project" + callback true, "readOnly" + else + metrics.inc "security.denied" + logger.log user:user, project:project, "Security denied - user can not enter project" + callback false + + userIsOwner : userIsOwner = (user, project)-> + if !user? + return false + else + userId = user._id+'' + ownerRef = getProjectIdFromRef(project.owner_ref) + if userId == ownerRef + true + else + false + + userIsCollaberator : userIsCollaberator = (user, project)-> + if !user? + return false + else + userId = user._id+'' + result = false + _.each project.collaberator_refs, (colabRef)-> + colabRef = getProjectIdFromRef(colabRef) + if colabRef == userId + result = true + return result + + userIsReadOnly : userIsReadOnly = (user, project)-> + if !user? + return false + else + userId = user._id+'' + result = false + _.each project.readOnly_refs, (readOnlyRef)-> + readOnlyRef = getProjectIdFromRef(readOnlyRef) + + if readOnlyRef == userId + result = true + return result + +getRequestUserAndProject = (req, res, options, callback)-> + project_id = req.params.Project_id + Project.findById project_id, 'name owner_ref readOnly_refs collaberator_refs publicAccesLevel', (err, project)=> + if err? + logger.err err:err, "error getting project for security check" + return callback err + AuthenticationController.getLoggedInUser req, options, (err, user)=> + if err? + logger.err err:err, "error getting last logged in user for security check" + callback err, user, project + +getProjectIdFromRef = (ref)-> + if ref._id? + return ref._id+'' + else + return ref+'' + + diff --git a/services/web/app/coffee/models/Doc.coffee b/services/web/app/coffee/models/Doc.coffee new file mode 100644 index 0000000000..3756a88f75 --- /dev/null +++ b/services/web/app/coffee/models/Doc.coffee @@ -0,0 +1,15 @@ +mongoose = require 'mongoose' +Settings = require 'settings-sharelatex' + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +DocSchema = new Schema + name : {type:String, default:'new doc'} + lines : [{}] + rev : {type:Number, default:0} + + +mongoose.model 'Doc', DocSchema +exports.Doc = mongoose.model 'Doc' +exports.DocSchema = DocSchema diff --git a/services/web/app/coffee/models/File.coffee b/services/web/app/coffee/models/File.coffee new file mode 100644 index 0000000000..3ca32b8f8f --- /dev/null +++ b/services/web/app/coffee/models/File.coffee @@ -0,0 +1,14 @@ +mongoose = require 'mongoose' +Settings = require 'settings-sharelatex' + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +FileSchema = new Schema + name : type:String, default:'' + created : type:Date, default: () -> new Date() + rev : {type:Number, default:0} + +mongoose.model 'File', FileSchema +exports.File = mongoose.model 'File' +exports.FileSchema = FileSchema diff --git a/services/web/app/coffee/models/Folder.coffee b/services/web/app/coffee/models/Folder.coffee new file mode 100644 index 0000000000..4c2bf04b64 --- /dev/null +++ b/services/web/app/coffee/models/Folder.coffee @@ -0,0 +1,20 @@ +mongoose = require('mongoose') +Settings = require 'settings-sharelatex' +DocSchema = require('./Doc').DocSchema +FileSchema = require('./File').FileSchema + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +FolderSchema = new Schema + name : {type:String, default:'new folder'} + +FolderSchema.add + docs : [DocSchema] + fileRefs : [FileSchema] + folders : [FolderSchema] + + +mongoose.model('Folder', FolderSchema) +exports.Folder = mongoose.model('Folder') +exports.FolderSchema = FolderSchema diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee new file mode 100644 index 0000000000..a009dc2a45 --- /dev/null +++ b/services/web/app/coffee/models/Project.coffee @@ -0,0 +1,123 @@ +mongoose = require('mongoose') +Settings = require 'settings-sharelatex' +_ = require('underscore') +FolderSchema = require('./Folder.js').FolderSchema +logger = require('logger-sharelatex') +sanitize = require('validator').sanitize +concreteObjectId = require('mongoose').Types.ObjectId +Errors = require "../errors" + + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +ProjectSchema = new Schema + name : {type:String, default:'new project'} + lastUpdated : {type:Date, default: () -> new Date()} + owner_ref : {type:ObjectId, ref:'User'} + collaberator_refs : [ type:ObjectId, ref:'User' ] + readOnly_refs : [ type:ObjectId, ref:'User' ] + rootDoc_id : {type: ObjectId} + rootFolder : [FolderSchema] + publicAccesLevel : {type: String, default: 'private'} + compiler : {type:String, default:'pdflatex'} + spellCheckLanguage : {type:String, default:'en'} + existsInVersioningApi : {type: Boolean, default: false} + deletedByExternalDataSource : {type: Boolean, default: false} + useClsi2 : {type:Boolean, default: true} + description : {type:String, default:''} + +ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> + if project_or_id._id? + callback null, project_or_id + else + try + concreteObjectId(project_or_id.toString()) + catch e + return callback(new Errors.NotFoundError(e.message)) + this.findById project_or_id, fields, callback + +ProjectSchema.statics.findPopulatedById = (project_id, callback)-> + this.find(_id: project_id ) + .populate('collaberator_refs') + .populate('readOnly_refs') + .populate('owner_ref') + .exec (err, projects)-> + if err? + logger.err err:err, project_id:project_id, "something went wrong looking for project findPopulatedById" + callback(err) + else if !projects? || projects.length == 0 + logger.err project_id:project_id, "something went wrong looking for project findPopulatedById, no project could be found" + callback "not found" + else + callback(null, projects[0]) + +ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)-> + this.find {owner_ref:user_id}, requiredFields, (err, projects)=> + this.find {collaberator_refs:user_id}, requiredFields, (err, collabertions)=> + this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=> + callback(projects, collabertions, readOnlyProjects) + +sanitizeTypeOfElement = (elementType)-> + lastChar = elementType.slice -1 + if lastChar != "s" + elementType +="s" + if elementType == "files" + elementType = "fileRefs" + return elementType + +ProjectSchema.statics.putElement = (project_id, folder_id, element, type, callback)-> + type = sanitizeTypeOfElement type + this.findById project_id, (err, project)=> + if err? + callback(err) + if !folder_id? + folder_id = project.rootFolder[0]._id + require('../Features/Project/ProjectLocator').findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=> + newPath = + fileSystem: "#{path.fileSystem}/#{element.name}" + mongo: path.mongo # TODO: This is not correct + if err? + callback(err) + logger.log project_id: project_id, element_id: element._id, type: type, folder_id: folder_id, + "adding element to project" + id = element._id+'' + element._id = concreteObjectId(id) + conditions = _id:project_id + mongopath = "#{path.mongo}.#{type}" + update = "$push":{} + update["$push"][mongopath] = element + this.update conditions, update, {}, (err)-> + if(err) + logger.err err: err, project: project, 'error saving in putElement project' + if callback? + callback(err, {path:newPath}) + +getIndexOf = (searchEntity, id)-> + length = searchEntity.length + count = 0 + while(count < length) + if searchEntity[count]._id+"" == id+"" + return count + count++ + + + +applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (folder, fun)-> + _.each folder.fileRefs, (file)-> + fun(file) + _.each folder.folders, (folder)-> + applyToAllFilesRecursivly(folder, fun) + + +ProjectSchema.methods.getSafeProjectName = -> + safeProjectName = this.name.replace(new RegExp("\\W", "g"), '_') + return sanitize(safeProjectName).xss() + +conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) + +Project = conn.model('Project', ProjectSchema) + +mongoose.model 'Project', ProjectSchema +exports.Project = Project +exports.ProjectSchema = ProjectSchema diff --git a/services/web/app/coffee/models/Quote.coffee b/services/web/app/coffee/models/Quote.coffee new file mode 100644 index 0000000000..9d44d97ab3 --- /dev/null +++ b/services/web/app/coffee/models/Quote.coffee @@ -0,0 +1,13 @@ +mongoose = require 'mongoose' +Settings = require 'settings-sharelatex' + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +QuoteSchema = new Schema + author : {type:String, default:'new quote'} + quote : {type:String} + +mongoose.model 'Quote', QuoteSchema +exports.Quote = mongoose.model 'Quote' +exports.QuoteSchema = QuoteSchema diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee new file mode 100644 index 0000000000..bf0ed085e5 --- /dev/null +++ b/services/web/app/coffee/models/Subscription.coffee @@ -0,0 +1,33 @@ +mongoose = require 'mongoose' +Settings = require 'settings-sharelatex' + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +SubscriptionSchema = new Schema + admin_id : {type:ObjectId, ref:'User', index: {unique: true, dropDups: true}} + member_ids : [ type:ObjectId, ref:'User' ] + recurlySubscription_id : String + planCode : {type: String} + groupPlan : {type: Boolean, default: false} + membersLimit: {type:Number, default:0} + freeTrial: + expiresAt: Date + downgraded: Boolean + planCode: String + allowed: {type: Boolean, default: true} + + +SubscriptionSchema.statics.findAndModify = (query, update, callback)-> + self = @ + this.update query, update, -> + self.findOne query, callback + + +conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) + +Subscription = conn.model('Subscription', SubscriptionSchema) + +mongoose.model 'Subscription', SubscriptionSchema +exports.Subscription = Subscription +exports.SubscriptionSchema = SubscriptionSchema \ No newline at end of file diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee new file mode 100644 index 0000000000..2d8e980954 --- /dev/null +++ b/services/web/app/coffee/models/User.coffee @@ -0,0 +1,84 @@ +Project = require('./Project').Project +Settings = require 'settings-sharelatex' +_ = require('underscore') +mongoose = require('mongoose') +uuid = require('node-uuid') +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +UserSchema = new Schema + email : {type : String, default : ''} + first_name : {type : String, default : ''} + last_name : {type : String, default : ''} + hashedPassword : String + isAdmin : {type : Boolean, default : false} + confirmed : {type : Boolean, default : false} + signUpDate : {type : Date, default: () -> new Date() } + lastLoggedIn : {type : Date} + loginCount : {type : Number, default: 0} + holdingAccount : {type : Boolean, default: false} + ace : { + mode : {type : String, default: 'none'} + theme : {type : String, default: 'textmate'} + fontSize : {type : Number, default:'12'} + autoComplete: {type : Boolean, default: true} + spellCheckLanguage : {type : String, default: "en"} + pdfViewer : {type : String, default: "pdfjs"} + } + features : { + collaborators: {type:Number, default:1} + versioning: {type:Boolean, default:false} + dropbox: {type:Boolean, default:false} + } + featureSwitches : { + dropbox: {type:Boolean, default:true}, + longPolling: {type:Boolean, default:false} + } + referal_id : {type:String, default:() -> uuid.v4().split("-")[0]} + refered_users: [ type:ObjectId, ref:'User' ] + refered_user_count: { type:Number, default: 0 } + subscription: + recurlyToken : String + freeTrialExpiresAt: Date + freeTrialDowngraded: Boolean + freeTrialPlanCode: String + # This is poorly named. It does not directly correspond + # to whether the user has has a free trial, but rather + # whether they should be allowed one in the future. + # For example, a user signing up directly for a paid plan + # has this set to true, despite never having had a free trial + hadFreeTrial: {type: Boolean, default: false} + + +UserSchema.statics.getAllIds = (callback)-> + this.find {}, ["first_name"], callback + + +UserSchema.statics.findReadOnlyProjects = (user_id, callback)-> + @find({'projects.readOnly_refs':user_id}).populate('projects.readOnly_refs').run (err, users)-> + projects = [] + _.each users, (user)-> + _.each user.projects, (project)-> + _.each project.readOnly_refs, (subUser)-> + if(subUser._id == user_id) + projects.push(project) + callback(projects) + +UserSchema.statics.findCollaborationProjects = (user_id, callback)-> + @find({'projects.collaberator_refs':user_id}).populate('projects.collaberator_refs').run (err, users)-> + projects = [] + _.each users, (user)-> + _.each user.projects, (project)-> + _.each project.collaberator_refs, (subUser)-> + if(subUser._id == user_id) + projects.push(project) + callback(projects) + + + +conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10) + +User = conn.model('User', UserSchema) + +model = mongoose.model 'User', UserSchema +exports.User = User diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee new file mode 100644 index 0000000000..100902cdb8 --- /dev/null +++ b/services/web/app/coffee/router.coffee @@ -0,0 +1,359 @@ +UserController = require('./controllers/UserController') +AdminController = require('./controllers/AdminController') +HomeController = require('./controllers/HomeController') +ProjectController = require("./controllers/ProjectController") +ProjectApiController = require("./Features/Project/ProjectApiController") +InfoController = require('./controllers/InfoController') +SpellingController = require('./Features/Spelling/SpellingController') +CollaberationManager = require('./managers/CollaberationManager') +SecutiryManager = require('./managers/SecurityManager') +AuthorizationManager = require('./Features/Security/AuthorizationManager') +versioningController = require("./Features/Versioning/VersioningApiController") +EditorController = require("./Features/Editor/EditorController") +EditorUpdatesController = require("./Features/Editor/EditorUpdatesController") +Settings = require('settings-sharelatex') +TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') +ProjectHandler = require('./handlers/ProjectHandler') +dropboxHandler = require('./Features/Dropbox/DropboxHandler') +SubscriptionRouter = require './Features/Subscription/SubscriptionRouter' +UploadsRouter = require './Features/Uploads/UploadsRouter' +metrics = require('./infrastructure/Metrics') +ReferalController = require('./Features/Referal/ReferalController') +ReferalMiddleware = require('./Features/Referal/ReferalMiddleware') +TemplatesController = require('./Features/Templates/TemplatesController') +TemplatesMiddlewear = require('./Features/Templates/TemplatesMiddlewear') +AuthenticationController = require('./Features/Authentication/AuthenticationController') +TagsController = require("./Features/Tags/TagsController") +CollaboratorsController = require('./Features/Collaborators/CollaboratorsController') +PersonalInfoController = require('./Features/User/UserController') +DocumentController = require('./Features/Documents/DocumentController') +CompileManager = require("./Features/Compile/CompileManager") +CompileController = require("./Features/Compile/CompileController") +HealthCheckController = require("./Features/HealthCheck/HealthCheckController") +ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" +logger = require("logger-sharelatex") + +httpAuth = require('express').basicAuth (user, pass)-> + isValid = Settings.httpAuthUsers[user] == pass + if !isValid + logger.err user:user, pass:pass, "invalid login details" + return isValid + +module.exports = class Router + constructor: (app, io, socketSessions)-> + app.use(app.router) + + collaberationManager = new CollaberationManager(io) + + Project = new ProjectController(collaberationManager) + projectHandler = new ProjectHandler() + + app.get '/', HomeController.index + + app.get '/login', UserController.loginForm + app.post '/login', AuthenticationController.login + app.get '/logout', UserController.logout + app.get '/restricted', SecutiryManager.restricted + + app.get '/resources', HomeController.resources + app.get '/comments', HomeController.comments + app.get '/tos', HomeController.tos + app.get '/about', HomeController.about + app.get '/attribution', HomeController.attribution + app.get '/security', HomeController.security + app.get '/privacy_policy', HomeController.privacy + app.get '/planned_maintenance', HomeController.planned_maintenance + app.get '/themes', InfoController.themes + app.get '/advisor', InfoController.advisor + app.get '/dropbox', InfoController.dropbox + + app.get '/register', UserController.registerForm + app.post '/register', UserController.apiRegister + + SubscriptionRouter.apply(app) + UploadsRouter.apply(app) + + if Settings.enableSubscriptions + app.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus + + app.get '/user/settings', AuthenticationController.requireLogin(), UserController.settings + app.post '/user/settings', AuthenticationController.requireLogin(), UserController.apiUpdate + app.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword + app.get '/user/passwordreset', UserController.requestPasswordReset + app.post '/user/passwordReset', UserController.doRequestPasswordReset + app.del '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe + app.del '/user', AuthenticationController.requireLogin(), UserController.deleteUser + + app.get '/dropbox/beginAuth', UserController.redirectUserToDropboxAuth + app.get '/dropbox/completeRegistration', UserController.completeDropboxRegistration + app.get '/dropbox/unlink', UserController.unlinkDropbox + + app.get '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken + app.get '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), PersonalInfoController.getLoggedInUsersPersonalInfo + app.get '/user/:user_id/personal_info', httpAuth, PersonalInfoController.getPersonalInfo + + app.get '/project', AuthenticationController.requireLogin(), Project.list + app.post '/project/new', AuthenticationController.requireLogin(), Project.apiNewProject + app.get '/project/new/template', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.createProjectFromZipTemplate + + app.get '/Project/:Project_id', SecutiryManager.requestCanAccessProject, Project.loadEditor + app.get '/Project/:Project_id/file/:File_id', SecutiryManager.requestCanAccessProject, Project.downloadImageFile + + # This is left for legacy reasons and can be removed once all editors have had a chance to refresh: + app.get '/Project/:Project_id/download/pdf', SecutiryManager.requestCanAccessProject, CompileController.downloadPdf + + app.get '/Project/:Project_id/output/output.pdf', SecutiryManager.requestCanAccessProject, CompileController.downloadPdf + app.get /^\/project\/([^\/]*)\/output\/(.*)$/, + ((req, res, next) -> + params = + "Project_id": req.params[0] + "file": req.params[1] + req.params = params + next() + ), SecutiryManager.requestCanAccessProject, CompileController.getFileFromClsi + + app.del '/Project/:Project_id', SecutiryManager.requestIsOwner, Project.deleteProject + app.post '/Project/:Project_id/clone', SecutiryManager.requestCanAccessProject, Project.cloneProject + + app.post '/Project/:Project_id/snapshot', SecutiryManager.requestCanModifyProject, versioningController.takeSnapshot + app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions + app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion + app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions + app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion + + app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject + app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators + + app.get '/Project/:Project_id/download/zip', SecutiryManager.requestCanAccessProject, ProjectDownloadsController.downloadProject + + + app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags + app.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate + + app.get '/project/:project_id/details', httpAuth, ProjectApiController.getProjectDetails + + app.get '/internal/project/:Project_id/zip', httpAuth, ProjectDownloadsController.downloadProject + app.get '/internal/project/:project_id/compile/pdf', httpAuth, CompileController.compileAndDownloadPdf + + + app.get '/project/:Project_id/doc/:doc_id', httpAuth, DocumentController.getDocument + app.post '/project/:Project_id/doc/:doc_id', httpAuth, DocumentController.setDocument + app.ignoreCsrf('post', '/project/:Project_id/doc/:doc_id') + + app.post '/user/:user_id/update/*', httpAuth, Project.startBufferingRequest, TpdsController.mergeUpdate + app.del '/user/:user_id/update/*', httpAuth, TpdsController.deleteUpdate + app.ignoreCsrf('post', '/user/:user_id/update/*') + app.ignoreCsrf('delete', '/user/:user_id/update/*') + + app.get '/enableversioning/:Project_id', (req, res)-> + versioningController.enableVersioning req.params.Project_id, -> res.send() + + app.get /^\/project\/([^\/]*)\/version\/([^\/]*)\/file\/(.*)$/, + ((req, res, next) -> + params = + "Project_id": req.params[0] + "Version_id": req.params[1] + "File_id": req.params[2] + req.params = params + next() + ), + SecutiryManager.requestCanAccessProject, versioningController.getVersionFile + + app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + + #Admin Stuff + app.get '/admin', SecutiryManager.requestIsAdmin, AdminController.index + app.post '/admin/closeEditor', SecutiryManager.requestIsAdmin, AdminController.closeEditor + app.post '/admin/dissconectAllUsers', SecutiryManager.requestIsAdmin, AdminController.dissconectAllUsers + app.post '/admin/writeAllDocsToMongo', SecutiryManager.requestIsAdmin, AdminController.writeAllToMongo + app.post '/admin/addquote', SecutiryManager.requestIsAdmin, AdminController.addQuote + app.post '/admin/syncUserToSubscription', SecutiryManager.requestIsAdmin, AdminController.syncUserToSubscription + app.post '/admin/flushProjectToTpds', SecutiryManager.requestIsAdmin, AdminController.flushProjectToTpds + app.post '/admin/pollUsersWithDropbox', SecutiryManager.requestIsAdmin, AdminController.pollUsersWithDropbox + app.post '/admin/updateProjectCompiler', SecutiryManager.requestIsAdmin, AdminController.updateProjectCompiler + + app.get '/perfTest', (req,res)-> + res.send("hello") + req.session.destroy() + + app.get '/status', (req,res)-> + res.send("websharelatex is up") + req.session.destroy() + + app.get '/health_check', HealthCheckController.check + + app.get "/status/compiler/:Project_id", SecutiryManager.requestCanAccessProject, (req, res) -> + success = false + CompileManager.compile req.params.Project_id, "test-compile", {}, () -> + success = true + res.writeHead 200 + res.end "Compiler returned in less than 10 seconds" + setTimeout (() -> + if !success + res.writeHead 500 + res.end "Compiler timed out" + ), 10000 + req.session.destroy() + + app.get '/test', (req, res) -> + res.render "tests", + privlageLevel: "owner" + project: + name: "test" + date: Date.now() + layout: false + userCanSeeDropbox: true + languages: [] + + app.get '/oops-express', (req, res, next) -> next(new Error("Test error")) + app.get '/oops-internal', (req, res, next) -> throw new Error("Test error") + app.get '/oops-mongo', (req, res, next) -> + require("./models/Project").Project.findOne {}, () -> + throw new Error("Test error") + + app.get '*', HomeController.notFound + + + socketSessions.on 'connection', (err, client, session)-> + metrics.inc('socket-io.connection') + # This is not ideal - we should come up with a better way of handling + # anonymous users, but various logging lines rely on user._id + if !session or !session.user? + user = {_id: "anonymous-user"} + else + user = session.user + + client.on 'joinProject', (data, callback) -> + EditorController.joinProject(client, user, data.project_id, callback) + + client.on 'disconnect', () -> + metrics.inc ('socket-io.disconnect') + EditorController.leaveProject client, user + + client.on 'reportError', (error, callback) -> + EditorController.reportError client, error, callback + + client.on 'sendUpdate', (doc_id, windowName, change)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorUpdatesController.applyAceUpdate(client, project_id, doc_id, windowName, change) + + client.on 'applyOtUpdate', (doc_id, update) -> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorUpdatesController.applyOtUpdate(client, project_id, doc_id, update) + + client.on 'clientTracking.updatePosition', (cursorData) -> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + EditorController.updateClientPosition(client, cursorData) + + client.on 'addUserToProject', (email, newPrivalageLevel, callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + EditorController.addUserToProject project_id, email, newPrivalageLevel, callback + + client.on 'removeUserFromProject', (user_id, callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + EditorController.removeUserFromProject(project_id, user_id, callback) + + client.on 'setSpellCheckLanguage', (compiler, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.setSpellCheckLanguage project_id, compiler, callback + + client.on 'setCompiler', (compiler, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.setCompiler project_id, compiler, callback + + client.on 'leaveDoc', (doc_id, callback)-> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + EditorController.leaveDoc(client, project_id, doc_id, callback) + + client.on 'joinDoc', (args...)-> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + EditorController.joinDoc(client, project_id, args...) + + client.on 'addDoc', (folder_id, docName, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.addDoc(project_id, folder_id, docName, [""], callback) + + client.on 'addFolder', (folder_id, folderName, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.addFolder(project_id, folder_id, folderName, callback) + + client.on 'deleteEntity', (entity_id, entityType, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.deleteEntity(project_id, entity_id, entityType, callback) + + client.on 'renameEntity', (entity_id, entityType, newName, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + collaberationManager.renameEntity(project_id, entity_id, entityType, newName, callback) + + client.on 'moveEntity', (entity_id, folder_id, entityType, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + collaberationManager.moveEntity(project_id, entity_id, folder_id, entityType, callback) + + client.on 'setProjectName', (window_id, newName, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + collaberationManager.renameProject(project_id, window_id, newName, callback) + + client.on 'getProject',(callback)-> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + projectHandler.getProject(project_id, callback) + + client.on 'setRootDoc', (newRootDocID, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + collaberationManager.setRootDoc(project_id, newRootDocID, callback) + + client.on 'deleteProject', (callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + collaberationManager.deleteProject(project_id, callback) + + client.on 'setPublicAccessLevel', (newAccessLevel, callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + collaberationManager.setPublicAccessLevel(project_id, newAccessLevel, callback) + + client.on 'pdfProject', (opts, callback)-> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + CompileManager.compile(project_id, user._id, opts, callback) + + # This is deprecated and can be removed once all editors have had a chance to refresh + client.on 'getRawLogs', (callback)-> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + CompileManager.getLogLines project_id, callback + + client.on 'distributMessage', (message)-> + AuthorizationManager.ensureClientCanViewProject client, (error, project_id) => + collaberationManager.distributMessage project_id, client, message + + client.on 'changeUsersPrivlageLevel', (user_id, newPrivalageLevel)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + projectHandler.changeUsersPrivlageLevel project_id, user_id, newPrivalageLevel + + client.on 'enableversioningController', (callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + versioningController.enableVersioning project_id, callback + + client.on 'getRootDocumentsList', (callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.getListOfDocPaths project_id, callback + + client.on 'forceResyncOfDropbox', (callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + EditorController.forceResyncOfDropbox project_id, callback + + client.on 'getUserDropboxLinkStatus', (owner_id, callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + dropboxHandler.getUserRegistrationStatus owner_id, callback + + client.on 'publishProjectAsTemplate', (user_id, callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + TemplatesController.publishProject user_id, project_id, callback + + client.on 'unPublishProjectAsTemplate', (user_id, callback)-> + AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) => + TemplatesController.unPublishProject user_id, project_id, callback + + client.on 'updateProjectDescription', (description, callback)-> + AuthorizationManager.ensureClientCanEditProject client, (error, project_id) => + EditorController.updateProjectDescription project_id, description, callback + + client.on "getLastTimePollHappned", (callback)-> + EditorController.getLastTimePollHappned(callback) diff --git a/services/web/app/templates/email/emailTemplate.html b/services/web/app/templates/email/emailTemplate.html new file mode 100755 index 0000000000..ca9a51c9c0 --- /dev/null +++ b/services/web/app/templates/email/emailTemplate.html @@ -0,0 +1,408 @@ + + + + + + + + + + +

+ + + + +
+ + + + + +
+ + + + + + + + + +
+
+ <%= previewMessage %> +
+
+ +
+ + +
+ + + + + + + + + + + +
+ + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + +
+
+

<%= heading %>

+

<%= message %>

+

Thank you

+

ShareLatex.com

+
+
+ + +
+ +
+ + + + + +
+ + + + + + +
+ +
+ + +
+ +
+
+
+
+ + diff --git a/services/web/app/templates/email/shared_project_email_template.html b/services/web/app/templates/email/shared_project_email_template.html new file mode 100755 index 0000000000..5f86a19c1f --- /dev/null +++ b/services/web/app/templates/email/shared_project_email_template.html @@ -0,0 +1,414 @@ + + + + + + +
+ + + + +
+ + + + + +
+ + + + + + + + + +
+
+ <%- previewMessage %> +
+
+
+ + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ +
+ + + + + +
+ + + + + + +
+
+

 

+ + + <%-view_data.owner.first_name%> (<%-view_data.owner.email%>) wants to share a LaTeX Project called '<%-view_data.project.name%>' with you. + +

 

+
+ +
+

 

+

- ShareLaTeX Team

+
+
+ + +
+ +
+ + + + + +
+ + + + + + +
+
+  Twitter + | + Facebook +
+
+ + +
+ +
+
+
+
+ + diff --git a/services/web/app/templates/project_files/main.tex b/services/web/app/templates/project_files/main.tex new file mode 100644 index 0000000000..c35723530c --- /dev/null +++ b/services/web/app/templates/project_files/main.tex @@ -0,0 +1,31 @@ +\documentclass{article} +\usepackage[utf8]{inputenc} + +\title{<%= project_name %>} +\author{<%= user.first_name %> <%= user.last_name %>} +\date{<%= month %> <%= year %>} + +\usepackage{natbib} +\usepackage{graphicx} + +\begin{document} + +\maketitle + +\section{Introduction} +There is a theory which states that if ever anyone discovers exactly what the Universe is for and why it is here, it will instantly disappear and be replaced by something even more bizarre and inexplicable. +There is another theory which states that this has already happened. + +\begin{figure}[h!] +\centering +\includegraphics[scale=1.7]{universe.jpg} +\caption{The Universe} +\label{fig:univerise} +\end{figure} + +\section{Conclusion} +``I always thought something was fundamentally wrong with the universe'' \citep{adams1995hitchhiker} + +\bibliographystyle{plain} +\bibliography{references} +\end{document} diff --git a/services/web/app/templates/project_files/mainbasic.tex b/services/web/app/templates/project_files/mainbasic.tex new file mode 100644 index 0000000000..12198ef625 --- /dev/null +++ b/services/web/app/templates/project_files/mainbasic.tex @@ -0,0 +1,14 @@ +\documentclass{article} +\usepackage[utf8]{inputenc} + +\title{<%= project_name %>} +\author{<%= user.first_name %> <%= user.last_name %>} +\date{<%= month %> <%= year %>} + +\begin{document} + +\maketitle + +\section{Introduction} + +\end{document} diff --git a/services/web/app/templates/project_files/references.bib b/services/web/app/templates/project_files/references.bib new file mode 100644 index 0000000000..1758b10f6f --- /dev/null +++ b/services/web/app/templates/project_files/references.bib @@ -0,0 +1,8 @@ +@book{adams1995hitchhiker, + title={The Hitchhiker's Guide to the Galaxy}, + author={Adams, D.}, + isbn={9781417642595}, + url={http://books.google.com/books?id=W-xMPgAACAAJ}, + year={1995}, + publisher={San Val} +} diff --git a/services/web/app/templates/project_files/universe.jpg b/services/web/app/templates/project_files/universe.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ed19e7d6779b47d8c63f6fa5a21954dcfb6cac00 GIT binary patch literal 28172 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 F|JeoOP;meN literal 0 HcmV?d00001 diff --git a/services/web/app/views/about/about.jade b/services/web/app/views/about/about.jade new file mode 100644 index 0000000000..fa4c6d8a4d --- /dev/null +++ b/services/web/app/views/about/about.jade @@ -0,0 +1,39 @@ +extends ../layout + +block content + .container + .row + .span8.offset2.span-box + .page-header + h1 About us + h3 Meet the team behind your favourite online LaTeX editor + p.team-profile + img(src='/img/about/henry_oswald.jpg') + strong Henry Oswald + | built an experimental LaTeX editor in 2011 which later became ShareLaTeX. He is a trained software engineer who lives in London. + | Henry has been responsible for building up a reliable platform for ShareLaTeX that allows instant real-time collaboration. + | Henry is a strong advocate of Test Driven Development and makes sure we keep the ShareLaTeX code clean and easy to maintain. + p.team-profile + img(src='/img/about/james_allen.jpg') + strong James Allen + | started working with Henry early in 2012 and finished his PhD in theoretical physics early in 2013. James began working on + | one of the first online LaTeX editors, ScribTeX, in 2008 and he has played a large role in developing the technologies and + | concepts that made ScribTeX and now ShareLaTeX possible. James is also slightly too obsessed with typography and learning the internals of how LaTeX works. + p(style="clear: both") + + h3 Motivation + p Our first priority with ShareLaTeX is to build a tool which makes life easier for all the LaTeX users our there. + | The "thank you"s and success stories are a strong motivator for us, and we hope to always be able to offer a fully-functional + | LaTeX editor which anyone can use for free. + p We also believe that charging money for tools like ShareLaTeX is important since it helps to guarantee the future of the site and + | the safety of your work, as well as allowing us to focus on it full time to develop new features. We prefer to provide a valuable + | service at a fair price rather than having to try run the site from adverts, or worse. As our customers, we are answerable to you and only you. + + h3 Technologies + p + | We use a lot of exciting technologies to run ShareLaTeX. We have mutliple APIs behind the scenes that we've written in Node.js, but the one exception is our + a(href='https://github.com/scribtex/clsi') open sourced compiler API. + | This is written in Ruby on Rails. We generally write in + a(href='http://coffeescript.org/') coffee-script + | as we find it helps to speed up development and make our code more readable. Your data is stored in MongoDB, Redis and Amazon S3. We practice Test Driven Development (TDD) to help produce robust and clean code which is easy to refactor. + include ../general/small-footer diff --git a/services/web/app/views/about/attribution.jade b/services/web/app/views/about/attribution.jade new file mode 100644 index 0000000000..a513b335fc --- /dev/null +++ b/services/web/app/views/about/attribution.jade @@ -0,0 +1,31 @@ +extends ../layout + +block content + .container + .row + .span6.offset3.span-box + .page-header + h1 Attribution + p + | We've only been able to create ShareLaTeX thanks to the many amazing free and + | open source technologies that exist. Here are some that we have been using and would + | like to say thank you to: + ul + li + a(href="http://nodejs.org/") Node.js + | and the massive set of modules that are available. + li + a(href="https://github.com/jviereck/pdfListView") pdfListView. + | A wrapper for PDF.js that powers our built in PDF viewer. + li + a(href="http://www.iconshock.com/") Icons from Iconshock. + li + a(href="http://twitter.github.com/bootstrap/") Bootstrap + | for letting us survive without being designers. + + include ../general/small-footer + + + + + diff --git a/services/web/app/views/about/planned_maintenance.jade b/services/web/app/views/about/planned_maintenance.jade new file mode 100644 index 0000000000..5fb0b1edde --- /dev/null +++ b/services/web/app/views/about/planned_maintenance.jade @@ -0,0 +1,16 @@ +extends ../layout + +block content + .container + .row + .span6.offset3.span-box + .page-header + h1 Planned Maintenance + p There is currently no planned maintenance + + include ../general/small-footer + + + + + diff --git a/services/web/app/views/about/privacy.jade b/services/web/app/views/about/privacy.jade new file mode 100644 index 0000000000..6a396217a7 --- /dev/null +++ b/services/web/app/views/about/privacy.jade @@ -0,0 +1,26 @@ +extends ../layout + +block content + .container + .row + .span8.offset2.span-box + .page-header + h1 Privacy Policy + h3 Information gathering + p When you register for ShareLaTeX we collect information such as your name and email address. ShareLaTeX uses this information to be able to provide our service, for identification and authorization of users, and to be able to contact you. + p The information we collect is not shared with other organisations except as detailed below for the provisioning and improvement of our service. The data we collect will never be sold to third parties for commercial purposes. + + h3 Cookies + p ShareLaTeX uses a cookie, which is a small amount of data stored by your web browser on your computer. The cookie stores your current session and allows you to stay logged in. Cookies are required to use the ShareLaTeX service. + + h3 Data storage + p ShareLaTeX uses third parties to host our services and store your data. You retain all rights to the data you upload to ShareLaTeX. + + h3 Credit cards and billing details + p We use a third party vendor, + a(href="recurly.com") Recurly + | , to store and process credit card transactions. Your email address, credit card details and billing address are passed on to Recurly and are not stored with ShareLaTeX. + + h3 Third party tracking + p We use Google Analytics, Mixpanel and HeapAnalytics to track users' interactions with ShareLaTeX. This data is used for the improvement of our service. + include ../general/small-footer diff --git a/services/web/app/views/about/security.jade b/services/web/app/views/about/security.jade new file mode 100644 index 0000000000..0c0cd105f9 --- /dev/null +++ b/services/web/app/views/about/security.jade @@ -0,0 +1,93 @@ +extends ../layout + +block content + .container + .row + .span6.offset3.span-box + .page-header + h1 Security + p + | Keeping your data safe is one of our top priorities. + | We work hard to make sure that ShareLaTeX is as secure as we can make it, + | and your input and feedback on our security is always appreciated. + h3 Responsible disclosure + p + | Please send reports of any urgent or sensitive security issues to + a(href="mailto:team@sharelatex.com") team@sharelatex.com. + | Use our + a(href="/sharelatex-security.pub") public key + | to encrypt your message and please provide us with a secure way to + | contact you. + + p + | Note that the URLs at /learn, /help + | and ctan.sharelatex.com are not under our direct control, + | and vulnerabilities should be reported to either MediaWiki or TenderApp + | respectively. + + p + | We are very grateful for all the responsibly reported security vulnerabilities, + | however listing on the hall of fame is reserved for people who report + | vulnerabilities that were previously unknown and we regard as serious. + + h3 Acknowledgements + p + | We'd like to thank the following people who have responsibly disclosed + | vulnerabilities to us and helped improved the security of ShareLaTeX: + + ul + li + a(href="https://twitter.com/Abdulahhusam", rel="nofollow") Abdullah Hussam Gazi + li + a(href="http://adamziaja.com", rel="nofollow") Adam Ziaja + li + a(href="http://alihassanpenetrationtester.blogspot.com/", rel="nofollow") Ali Hasan Ghauri + li + a(href="https://twitter.com/EhArvindSingh") Arvind Singh Shekhawat + li + a(href="https://www.facebook.com/Dakshxss", rel="nofollow") Daksh Patel + li + a(href="https://twitter.com/dibsyhex", rel="nofollow") Dibyendu Sikdar + li + a(href="https://www.facebook.com/jaymark.pestano", rel="nofollow") Jaymark Pestaño + li + a(href="https://twitter.com/korapsyon", rel="nofollow") Jerold Camacho + li + a(href="https://twitter.com/kamilsevi", rel="nofollow") Kamil Sevi + li + a(href="https://twitter.com/kingkaustubhp", rel="nofollow") Kaustubh Padwad + li 'KoF2002' & 'Sr33h4r!(XSS no0B)' + li + a(href="http://twitter.com/umenmactech", rel="nofollow") Manish Bhattacharya + li + a(href="http://twitter.com/Manjesh24", rel="nofollow") Manjesh S + li + a(href="https://www.facebook.com/Shahmeer.1994", rel="nofollow") Muhammad Shahmeer + li + a(href="http://nbsriharsha.blogspot.in", rel="nofollow") N B Sri Harsha + li + a(href="http://www.linkedin.com/in/osandamalith", rel="nofollow") Osanda Malith Jayathissa + li + a(href="https://twitter.com/prasadk14", rel="nofollow") Prasad Kancharla + li + a(href="https://www.facebook.com/c0m4dr3404", rel="nofollow") Praveen Nair (Kerala Cyber Squad - India) + li + a(href="https://twitter.com/iAmPr3m", rel="nofollow") Prem Kumar + li + a(href="https://www.facebook.com/HardNocksHittnHard", rel="nofollow") Sherin Panikar (Kerala Cyber Squad - India) + li + a(href="https://twitter.com/Simon90_Italy", rel="nofollow") Simone Memoli + li + a(href="https://twitter.com/tareksiddiki", rel="nofollow") Tarek Siddiki + li + a(href="https://twitter.com/venugopalt", rel="nofollow") Venugopal Thotakura + li + a(href="http://softproweb.blogspot.com/", rel="nofollow") Waqeeh Ul Hasan + li + a(href="https://twitter.com/zerodayguys", rel="nofollow") Zeroday Guys (Rakesh Singh & V.Harish Kumar) + li + a(href="https://twitter.com/_prashantnegi", rel="nofollow") Prashant Negi + span and + a(href="https://twitter.com/AjaySinghNegi", rel="nofollow") Singh Negi + + include ../general/small-footer diff --git a/services/web/app/views/about/tos.jade b/services/web/app/views/about/tos.jade new file mode 100644 index 0000000000..fa525ac68a --- /dev/null +++ b/services/web/app/views/about/tos.jade @@ -0,0 +1,34 @@ +extends ../layout + +block content + .container + .row + .span8.offset2.span-box + .page-header + h1 Terms of Service + p + | Thank you for taking the time to read our terms of service. + | We've tried to keep it simple, but you must agree to these terms in order to use ShareLaTeX. + h3 You must provide a valid email address + p + | We expect you to register with a valid email address. + | This will be our only way to communicate with you. + | Changes to these terms of service and any other notifications about updates + | to the service will be sent to the email address you provide. + | We will not be held responsible for any information not being received. + h3 You own your content, not us + p You retain all ownership, copyright and intellectual property rights to any content uploaded to ShareLaTeX. Your content will only be shared with other users of your choosing and we will never share your content with third parties without your consent. The staff of ShareLaTeX have access to your content, but we make an effort to only access it when absolutely necessary. + h3 You're not allowed to abuse the service + p ShareLaTeX is provided assuming users will act in good faith. However, we retain the right to remove any account or content that we feel is abusing the service. In the rare event of this happening, it will most likely be for one of the following reasons: using the service for illegal reasons; uploading illegal, unauthorised or objectionable content; consuming an excessive amount of computing resources. + h3 We're not infallible + p ShareLaTeX is provided on an 'as is' basis, without future promise of availability. ShareLaTeX is not liable for any damages caused by the loss or inability to access your data. + p [Founder's note: We try hard to provide a service which is reliable, secure and regularly backed up. While the above paragraph sounds grim, we need it there to cover ourselves incase something does go wrong. We certainly aren't planning to need to fall back on it. This assurance aside, it's always good practice to back up your own data.] + h3 Cancellation and refunds policy + p ShareLaTeX's services are paid for monthly or yearly in advance and are non-refundable. There will be no refunds or credits for partial months/years of service or downgrades. Subscription downgrades and cancellations will come into effect when your next payment is due. Upgrades to your plan will be billed immediately at the new rate for the remainder of the billing period. You may cancel your account at any time from your account subscription settings. + h3 Paypal Reference Transactions + p PayPal Reference Transactions are used to allow ShareLaTeX to make subsequent transactions on a monthly or annual basis until you cancel your account. + h3 Pricing changes + p We will notify you of any changes to our pricing plans that affect you at least 30 days before your next payment is due. We will notify you via the email address you provided when subscribing for a paid plan. + h3 You must be human + p To protect ourselves from accidental or malicious abuse, automated scripts and programs are not permitted to register an account or access the service. + include ../general/small-footer diff --git a/services/web/app/views/admin.jade b/services/web/app/views/admin.jade new file mode 100644 index 0000000000..0c035d4d31 --- /dev/null +++ b/services/web/app/views/admin.jade @@ -0,0 +1,133 @@ +extends layout + +block content + .container + .row.box + .page-header + h1 Admin Panel + .row + + .tabbable + ul.nav.nav-tabs + li.active + a(href='#userListSection', data-toggle="tab") Users + li + a(href='#closeEditorSection', data-toggle="tab") Close Editor + li + a(href='#quotesManagementSection', data-toggle="tab") Quotes Management + li + a(href='#subscriptionManagement', data-toggle="tab") Subscription Management + li + a(href='#tpdsManagementSection', data-toggle="tab") Tpds Management + li + a(href='#compilerManagementSection', data-toggle="tab") Compiler Management + + .tab-content.form-horizontal + .tab-pane#userListSection.form.form-horizontal.active + .clearfix + ul#stats + li current connected users : #{currentConnectedUsers} + li number of docs in memory : #{numberOfAceDocs} + li total registred users : #{totalUsers} + li total projects : #{totalProjects} + li yesterday: + ul + li docsets : #{redisstats.yesterday.docsets} + li today: + ul + li docsets : #{redisstats.today.docsets} + + .clearfix + span Open sockets + ul + -each agents, url in openSockets + li #{url} - total : #{agents.length} + ul + -each agent in agents + li #{agent} + + table.table-striped.table.table-striped#connected-users + thead + tr + th email + th first name + th last name + th login count + th signup date + th user id + th project id + th connected mins + tbody + -each user in users + tr + td #{user.email} + td #{user.first_name} + td #{user.last_name} + td #{user.login_count} + td #{user.signup_date} + td #{user.user_id} + td #{user.project_id} + td #{user.connected_mins} + + .tab-pane#closeEditorSection.form.form-horizontal.active + form + .clearfix + button.btn#closeEditor(data-csrf=csrfToken) Close Editor + .clearfix + button.btn#disconnectAll(data-csrf=csrfToken) Force Discount Now Users + + .tab-pane#quotesManagementSection.form.form-horizontal.active + form.validate(enctype='multipart/form-data', method='post',action='/admin/addquote') + input(name="_csrf", type="hidden", value=csrfToken) + .clearfix + label(for='xlInput') author + .input + input.span4#email.email.required(type='text', name='author', placeholder='author') + .clearfix + label(for='xlInput') quote + .input + input.span4#password.required(type='text', name='quote', placeholder='quote') + .actions + button.btn-primary.btn.btn-large#login(type='submit') add + + .tab-pane#subscriptionManagement + form.validate(enctype='multipart/form-data', method='post',action='/admin/syncUserToSubscription') + input(name="_csrf", type="hidden", value=csrfToken) + .clearfix + label(for='xlInput') subscription_id + .input + input.span4#email.required(type='text', name='subscription_id', placeholder='subscription_id') + .clearfix + label(for='xlInput') user_id + .input + input.span4#password.required(type='text', name='user_id', placeholder='user_id') + .actions + button.btn-primary.btn.btn-large#login(type='submit') Link + + .tab-pane#tpdsManagementSection + h3 flush project to tpds + form(enctype='multipart/form-data', method='post',action='/admin/flushProjectToTpds') + input(name="_csrf", type="hidden", value=csrfToken) + .clearfix + label(for='xlInput') project_id + .input + input.span4#email.required(type='text', name='project_id', placeholder='project_id') + .actions + button.btn-primary.btn.btn-large#login(type='submit') Flush + button#pollTpds.btn(src='/admin/pollUsersWithDropbox', data-csrf=csrfToken) Poll users with dropbox + + + .tab-pane#compilerManagementSection + h3 Update project compiler + form(enctype='multipart/form-data', method='post',action='/admin/updateProjectCompiler') + input(name="_csrf", type="hidden", value=csrfToken) + .clearfix + label(for='xlInput') project_id + .input + input.span4.required(type='text', name='project_id', placeholder='project_id') + .actions + input(type='submit', name="new", value="new").btn Use new + input(type='submit', name="old", value="old").btn Use old + + - locals.supressDefaultJs = true + script(data-main='js/admin.js', src='js/libs/require.js') diff --git a/services/web/app/views/changelog.jade b/services/web/app/views/changelog.jade new file mode 100644 index 0000000000..92fb1d6cab --- /dev/null +++ b/services/web/app/views/changelog.jade @@ -0,0 +1,138 @@ +extends layout + +block content + .container + .row + .span12.span-box + .page-header + h1 Change Log + small Some of the things that have changed on the site + #changeLogContent + h5 April + ul + li Added Templates + h5 March + ul + li Improved search and replace + li Improved Zip uploads + li Improved Zip downloads + li Added user bonus page + li Added new help pages and knowledge base + li Started Dropbox beta + li Added 'About Us' page + h5 February + ul + li Built in spell checker + li Auto completion of commands + li Saved message in bottom left doesn't flicker anymore + li Files and folders are order alphabetically in the left hand pane + li The root document can now be nested inside a folder + li Duplicate project can now be done from inside the project settings + h5 January + ul + li Split view and adjustable size panels added. + h5 December + ul + li Site can now compile with LaTeX, XeLaTeX and pdfLaTeX + li Made history/version control functionality + li When returning to document the editor scrolls to where you previously where + li Added recompile button + h5 November + ul + li Added premium accounts + li Updated backend collaberation system to allow multiple instances of the website to be run at the same time for more robustness + h5 May - October + ul + li Lots of work on increasing stability so we can serve our growing user base (thank you!) + li Background work to make it possible to record a complete history of changes to your documents + h5 18th May + ul + li changed compiler to be more robust + li updated ace editor code with new vim binding + li added things to the resources page + h5 13th May + ul + li Added site to chrome web store + li cleaned up home page + li added mailchimp sign up + li added comments page + li added link to public projects + h5 11th May + ul + li Added flat view of public projects + h5 9th May + ul + li Fixed bug with deleting projects + h5 5th May + ul + li Lost of changes behind the scenes to improve robustness + h5 21st April + ul + li Increased logging length to 48 hours + li Trying to access a project while not logged in redirects to login page rather than restricted + h5 20th April + ul + li Stability update to the compiling + h5 16th April + ul + li When a compile fails user is shown the logs by default + h5 11th April + ul + li Fixed lost connection bug + li Fixed bug so large projects will compile (25,000+ words) + h5 31st March + ul + li Added public and read only projects + li Added blog post of LNUG talk + li Added project upload + li Improved safety of site removing edge case of when site crashes data could be potentially lost + li Added blank project option when creating new project + h5 26th March + ul + li Update to faq + li All js files are now minified including editor + h5 21st March + ul + li Fixed bug with double encoding of PDFs in headers + li improved reliability of site + h5 17th March + ul + li Minified JavaScript everywhere but editor + h4 16th March + ul + li Added themes preview + li Added Detexify to resources page + h4 14th March + ul + li Added Resources list + li Improved application monitoring + h4 13th March 2012 + ul + li Improved resilience reducing number of restarts required. + li added UTF-8 to default project template + li Fixed projects not always deleting + h4 11th March 2012 + ul + li Fixed deeply nested documents not being fount + h4 8th March 2012 + ul + li improved error handling on projects that don't compile + li added more FAQ answers. + h4 5th March 2012 + ul + li fixed bug where error logs were not shown, infinite spinning compile + li allow users to see raw logs + li add change log + li added quantcast + h4 4th March 2012 + ul + li fixed google plus button + h4 3th March 2012 + ul + li fixed dropping connections + li fixed problem with missing data in zipped projects + h4 2nd March 2012 + ul + li updated 404 page with correct css + + diff --git a/services/web/app/views/general/404.jade b/services/web/app/views/general/404.jade new file mode 100644 index 0000000000..b826ea865a --- /dev/null +++ b/services/web/app/views/general/404.jade @@ -0,0 +1,9 @@ +extends ../layout + +block content + .container + .row + .span6.offset3.span-box + .page-header + h2 Sorry, we can't find the page you are looking for. + .row diff --git a/services/web/app/views/general/closed.jade b/services/web/app/views/general/closed.jade new file mode 100644 index 0000000000..ce1c3aaf15 --- /dev/null +++ b/services/web/app/views/general/closed.jade @@ -0,0 +1,13 @@ +extends ../layout + +block content + .container + .row + .span4.offset4.span-box + .page-header + h1 Maintenance + p + | Sorry, ShareLaTeX is briefly down for maintenance. + | We should be back within minutes, but if not, or you have + | an urgent request, please contact us at + | team@sharelatex.com diff --git a/services/web/app/views/general/genericMessage.jade b/services/web/app/views/general/genericMessage.jade new file mode 100644 index 0000000000..af476040c0 --- /dev/null +++ b/services/web/app/views/general/genericMessage.jade @@ -0,0 +1,9 @@ +extends ../layout + +block content + .container + .row + .box.span4.offset4 + .page-header + h1= messageTitle + p!= messageBody diff --git a/services/web/app/views/general/long-form-features.jade b/services/web/app/views/general/long-form-features.jade new file mode 100644 index 0000000000..638c19275f --- /dev/null +++ b/services/web/app/views/general/long-form-features.jade @@ -0,0 +1,45 @@ +.long-form-features + .row + .span7 + h2 A LaTeX Editor for smooth collaboration + p Keep your LaTeX collaborators up to date by letting everyone access and edit the same LaTeX document. + p.subdued + | The days of making sure everyone has access to the latest version are over; the latest version is always available online. + | You can even work on the document at the same time as your collaborators with our real-time editor, + | and our built in chat will help you communicate while you're editing. + .span5 + img(src="/img/screenshots/editorCollaborating.png") + + .row + .span5 + img(src="/img/screenshots/editorCode.png") + .span7 + h2 A LaTeX Editor that is easy to use + p ShareLaTeX is the easiest LaTeX editor to get started if you’ve never used LaTeX before. + p.subdued + | There's no installation needed and our example projects are ready to be compiled with one click. + | You or your collaborators no longer need to be experts in LaTeX to be able to edit your documents effectively. + | Our editor also highlights any errors in the LaTeX log to make debugging as simple as possible. + + .row + .span7 + h2 Work on LaTeX from anywhere + p You can access our LaTeX editor and compile your LaTeX documents from any computer with an internet connection. + p.subdued + | You don’t need to worry about getting LaTeX set up on each computer you use, and compatibility issues are gone. + | There are no lock-ins and you can download your projects to work offline any time you like. + | You can also upload existing documents to get your work onto ShareLaTeX quickly. + .span5 + img(src="/img/screenshots/editorPDF.png") + + .row + .span5 + img(src="/img/dropbox/dropbox_banner_tall.png") + .span7 + h2 Easily sync with your offline files + p Work offline using dropbox + p.subdued + | Dropbox sync keeps all of your LaTeX projects synced to your dropbox folder. You can make changes inside your dropbox then see + | them sync back to ShareLaTeX. Collaborate with users who like to work offline with their own tools by sharing the Dropbox + | folder with them. + diff --git a/services/web/app/views/general/partial/registerForm.jade b/services/web/app/views/general/partial/registerForm.jade new file mode 100644 index 0000000000..51df0552a3 --- /dev/null +++ b/services/web/app/views/general/partial/registerForm.jade @@ -0,0 +1,13 @@ +form#registerFormShort(style="display: none;", method="post") + input(name='_csrf', type='hidden', value=csrfToken) + .fieldset + .clearfix + .input + input.large#email.span4.email.required(type='email', placeholder='Email Address', name='email') + .clearfix + .input + input.large#password.span4.required(type='password', placeholder='Password', name='password') + .actions + button.btn.btn-success.btn-huge#registerButton(type='submit') Register + + diff --git a/services/web/app/views/general/sidebar.jade b/services/web/app/views/general/sidebar.jade new file mode 100644 index 0000000000..1750ce1abf --- /dev/null +++ b/services/web/app/views/general/sidebar.jade @@ -0,0 +1,24 @@ +.sidebar-navigation(style="width: 180px; top: 40px") + ul + li(class = typeof projectTabActive != "undefined" ? "active" : "") + a(href="/project").tab-link.project-list-tab + .content Projects + li(class = typeof accountSettingsTabActive != "undefined" ? "active" : "") + a(href="/user/settings").tab-link.account-settings-tab + .content Account Settings + - if (settings.enableSubscriptions) + li(class = typeof subscriptionTabActive != "undefined" ? "active" : "") + a(href="/user/subscription").tab-link.subscription-tab + .content Subscription + + - if (settings.enableSubscriptions) + .bonus-advert.ab-bonus-sidebar + .speech-bubble + h3 + a(href="/user/bonus").plain-link Get free stuff! + p + a(href="/user/bonus").plain-link Recommend ShareLaTeX to your friends and we'll upgrade your account for free. + p + a(href="/user/bonus").btn.btn-success Get free stuff + img(src="/img/logo/lion-64.png").lion + diff --git a/services/web/app/views/general/small-footer.jade b/services/web/app/views/general/small-footer.jade new file mode 100644 index 0000000000..4ae3ea67c6 --- /dev/null +++ b/services/web/app/views/general/small-footer.jade @@ -0,0 +1,32 @@ +.row-fluid + .span12 + .small-footer + ul + li © ShareLaTeX + li + a(href="/tos") Terms of Service + li + a(href="/privacy_policy") Privacy Policy + li + a(href="/user/subscription/plans") Plans & Pricing + li + a(href="/university") University Licenses + li + a(href="/dropbox") Dropbox + ul + li + a(href="/security") Security + li + a.js-tender-widget(href='#') Contact us + li + a(href="/attribution") Thanks + li + a(href="/blog") Blog + li + a(href="http://www.twitter.com/sharelatex") Twitter + li + a(href="http://www.facebook.com/pages/ShareLaTeX/301671376556660") Facebook + li + a(href="https://plus.google.com/115074691861228882827", rel="publisher") Google+ + ul + li LaTeX Editor - ShareLaTeX diff --git a/services/web/app/views/general/social-footer.jade b/services/web/app/views/general/social-footer.jade new file mode 100644 index 0000000000..c648045fad --- /dev/null +++ b/services/web/app/views/general/social-footer.jade @@ -0,0 +1,30 @@ +.container + .row + .span12 + .social-footer + #fb-root + script(type="text/javascript") + (function(d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) return; + js = d.createElement(s); js.id = id; + js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1"; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); + .social + ul + li.facebook + .fb-like(data-href="https://www.sharelatex.com", data-send="true", data-width="450", data-show-faces="false", data-font="arial", data-action="recommend") + ul + li.google + .g-plusone(data-annotation="inline", data-width="300") + script(type="text/javascript") + (function() { + var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true; + po.src = 'https://apis.google.com/js/plusone.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); + })(); + li.twitter + a(href="https://twitter.com/sharelatex", class="twitter-follow-button", data-show-count="false") Follow @sharelatex + script(type="text/javascript") + !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs'); diff --git a/services/web/app/views/homepage/comments.jade b/services/web/app/views/homepage/comments.jade new file mode 100644 index 0000000000..a73b194f84 --- /dev/null +++ b/services/web/app/views/homepage/comments.jade @@ -0,0 +1,46 @@ +include header + +mixin quote(quote, author) + blockquote + p #{quote} + small #{author} + +extends ../layout + +block content + .container.box + .page-header + h2 What a few of the thousands of people using sharelatex are saying + .row + .span4 + + .span4 + + .span4 + + .row + .span4 + + .span4 + + .span4 + + .row + .span4 + + .span4 + + .span4 + + .row + .span4 + + .span4 + + .span4 + + + + + - locals.supressDefaultJs = true + script(data-main=jsPath+'main.js', src='js/libs/require.js', baseurl=jsPath) diff --git a/services/web/app/views/homepage/header.jade b/services/web/app/views/homepage/header.jade new file mode 100644 index 0000000000..0195b3090a --- /dev/null +++ b/services/web/app/views/homepage/header.jade @@ -0,0 +1,15 @@ +.container + #logoArea + .span8 + h1.logo.large + a(href='/').plain ShareLaTeX + span.sub.logo Online LaTeX Editor + .span3#homePagePills + ul.nav.nav-pills + li + a(href='/') Home + li + a(href='/blog') Blog + li + a(href='/comments') Comments + diff --git a/services/web/app/views/homepage/home.jade b/services/web/app/views/homepage/home.jade new file mode 100644 index 0000000000..75e8b607d8 --- /dev/null +++ b/services/web/app/views/homepage/home.jade @@ -0,0 +1,70 @@ +link(rel='canonical', href='https://www.sharelatex.com') + +extends ../layout + +block content + .masthead + .container + .row + .title.span8 + h1 Online LaTeX Editor + h2 Quickly start using LaTeX and work together in real-time + div + img(src="/img/screenshot.png") + .register.span4 + h2 Register now + .messageArea + include ../general/partial/registerForm + + .universities + .container + .row + p Used by students and academics at: + #slides + .clearfix + .span3 + img(src="/img/crests/harvard.gif", alt="harvard university logo") + .span3 + img(src="/img/crests/mit.gif", alt="mit university logo") + .span3 + img(src="/img/crests/oxford.gif", alt="oxford university logo") + .span3 + img(src="/img/crests/tokyo.png", alt="tokyo university logo") + .clearfix + .span3 + img(src="/img/crests/cambridge.png", alt="cambridge university logo") + .span3 + img(src="/img/crests/liverpool.jpg", alt="liverpool university logo") + .span3 + img(src="/img/crests/icl.png", alt="icl university logo") + .span3 + img(src="/img/crests/yale.png", alt="yale university logo") + .clearfix + .span3 + img(src="/img/crests/durham.png", alt="durham university logo") + .span3 + img(src="/img/crests/nasa.png", alt="nasa university logo") + .span3 + img(src="/img/crests/toronto.gif", alt="toronto university logo") + .span3 + img(src="/img/crests/stanford.png", alt="stanford university logo") + + + .container + include ../general/long-form-features + + .row + .span12.lower-signup-button + a(href="/user/subscription/plans").btn.btn-success.btn-huge Sign up now! + + include ../general/social-footer + include ../general/small-footer + + .container + .row + .span12(style="text-align:right; margin-bottom:20px") + a(href="https://mixpanel.com/f/partner") + img(src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png",alt="Mobile Analytics") + + + diff --git a/services/web/app/views/homepage/socialMedia.jade b/services/web/app/views/homepage/socialMedia.jade new file mode 100644 index 0000000000..839a4804a0 --- /dev/null +++ b/services/web/app/views/homepage/socialMedia.jade @@ -0,0 +1,7 @@ + +.span2.offset3 + include ../referal/tweet +.span2 + include ../referal/googleplus +.span1 + include ../referal/facebookLike diff --git a/services/web/app/views/info/advisor.jade b/services/web/app/views/info/advisor.jade new file mode 100644 index 0000000000..16e9a921de --- /dev/null +++ b/services/web/app/views/info/advisor.jade @@ -0,0 +1,69 @@ +extends ../layout + +block content + .container + .row + .span8.offset2.span-box + .page-header + h1 Become a ShareLaTeX Advisor + .long-form-features + + div Want to help us change collaboration in academia? We are currently recruiting members to join our advisor program. If you love LaTeX like we do and want to help represent us at your Institution then please get in touch. + + hr + + div + h2 Selected Advisors will: + ul + li Teach LaTeX sessions in your Institution + li Help us refine our the learning resources for the sessions + li Invite your colleges to work on ShareLaTeX + li Give us feedback and new ideas + li Let us know what the needs of your Institution are + hr + div + h2 You will get: + ul + li A premium ShareLaTeX account for free + li Be the first to try out new Beta features + li Get coffee and cake paid by us for your sessions + li Get to connect with other ShareLaTeX Advisors around the world + hr + div + h2 Apply + div To apply to be a ShareLaTeX advisor please fill in this form telling us about yourself. Let us know about what you have done using ShareLaTeX and LaTeX in the past, any opinions you have on LaTeX or collaboration in academia. + div   + + form(name='form2', autocomplete='off', enctype='multipart/form-data', method='post', novalidate='novalidate', action='https://sharelatex.wufoo.com/forms/m7x3z9/#public') + .clearfix + label#title1.desc(for='Field1') Email address + .input + input.span4#Field1.field.text.medium(name='Field1', type='email', value='', maxlength='255', tabindex='1', onkeyup='', required='required') + + .clearfix + label#title2.desc(for='Field2') Full name + .input + input.span4#Field2.field.text.medium(name='Field2', type='text', value='', maxlength='255', tabindex='2', onkeyup='') + .clearfix + label#title3.desc(for='Field3') Institution + .input + div + input.span4#Field3.field.text.medium(name='Field3', type='text', value='', maxlength='255', tabindex='3', onkeyup='') + .clearfix + label#title4.desc(for='Field4') Position + div + .input + input.span4#Field4.field.text.medium(name='Field4', type='text', value='', maxlength='255', tabindex='4', onkeyup='') + .clearfix + label#title5.desc(for='Field5') About you + .input + textarea.span4#Field5.field.textarea.medium(name='Field5', spellcheck='true', rows='10', cols='50', tabindex='5', onkeyup='') + div + input#saveForm.btn.btn-success.submit(name='saveForm', type='submit', value='Submit') + .hide + label(for='comment') Do Not Fill This Out + textarea#comment(name='comment', rows='1', cols='1') + input#idstamp(type='hidden', name='idstamp', value='UtmlhAnuOejSWtH/UuhT7YWdvNi99jxEEveIrxKyVtc=') + + + include ../general/small-footer diff --git a/services/web/app/views/info/dropbox.jade b/services/web/app/views/info/dropbox.jade new file mode 100644 index 0000000000..020ee06241 --- /dev/null +++ b/services/web/app/views/info/dropbox.jade @@ -0,0 +1,94 @@ +extends ../layout + +block content + .container + .row + .span10.offset1.span-box + .page-header + h1 Dropbox and LaTeX + .long-form-features + .row + .span5 + h2 ShareLaTeX Dropbox Sync + p LaTeX and Dropbox has never been easier + p.subdued ShareLaTeX allows you to link your account + | to Dropbox, keeping your LaTeX work synced at all + | times. Using LaTeX in a team is made easier with + | dropbox allowing your team members to use their own + | tools, and work offline if required. With all of + | your LaTeX synced to dropbox you never have to worry + | about keeping backups of your ShareLaTeX work again. + ul + li Work Offline + li Collaborate with more people + li Worry free backups + li Use different LaTeX Editors + li Easy to add new files into your ShareLaTeX project + li Automated backup snapshots + .span5 + img(src="/img/dropbox/dropbox_banner_tall.png", alt="Sync your ShareLaTeX LaTeX using Dropbox").noborder + + center + a.btn.btn-large.btn-success(href='/user/subscription/plans') Sign Up Now for Dropbox Sync + hr + + .long-form-features + .row + .span6 + h2 Work Offline + p.subdued Dropbox Sync allows you to work on your + | LaTeX offline using Dropbox, then come back to the + | updated project in ShareLaTeX, just like you never + | left. Being on the move with no internet connection + | no longer means you can not work on your LaTeX + | project, and Dropbox's great offline capabilities + | allows you to use local editors then let Dropbox and + | ShareLaTeX take care of the rest. + .span4 + img(src="/img/dropbox/dropbox_progress_bar.png", alt="Dropbox changes are checked for every 15 minutes").short_image + + + .row + .span4 + img(src="/img/dropbox/share_dropbox_folder.png", alt="share latex folder with other dropbox users").short_image + .span6 + h2 Collaborate with Anyone + p.subdued Not everyone wants to use ShareLaTeX, and + | sharing your LaTeX files via Dropbox allows you to + | collaborate with people who like to use a different + | editor. Many people have been using LaTeX for + | decades and do not want to move away from the tools + | they know so well, but this no longer has to be a reason + | to stop you working on a paper with them + + .row + .span6 + h2 LaTeX Backups in Dropbox + p.subdued All of your LaTeX files will be safely + | stored in Dropbox so you no longer have to worry + | about having another backup of your paper. + .span4 + img(src="/img/dropbox/dropbox_logo.png", alt="Sync your ShareLaTeX LaTeX using Dropbox") + + .row + .span4 + img(src="/img/dropbox/document_updated_modal.png", alt="Dropbox changes are checked for every 15 minutes") + .span6 + h2 Easy to Insert Files and Folders into your ShareLaTeX Project + p.subdued You can easily add new files into your + | LaTeX project by just moving them into your Dropbox; + | after a sync they will apear in ShareLaTeX. + + .row + .span6 + h2 Automated Snapshots After Each Update + p.subdued Each time a change is detected in Dropbox + | ShareLaTeX takes a project snapshot meaning there is + | no chance of your changes being lost. + .span4 + img(src="/img/dropbox/history_diff.png", alt="snapshots are taken before changes from Dropbox are applied") + + center + a.btn.btn-large.btn-success(href='/user/subscription/plans') Sign Up Now for Dropbox Sync + + include ../general/small-footer diff --git a/services/web/app/views/info/themes.jade b/services/web/app/views/info/themes.jade new file mode 100644 index 0000000000..4361cac1c9 --- /dev/null +++ b/services/web/app/views/info/themes.jade @@ -0,0 +1,92 @@ +extends ../layout + +block content + .container + .row + .box + .page-header + h1 Available Themes + div + h3 Chrome + img(src='/img/themes/origonal/Chrome.jpg') + hr + h3 Monokai + img(src='/img/themes/origonal/Monokai.jpg') + hr + h3 Clouds + img(src='/img/themes/origonal/clouds.jpg') + hr + h3 clouds_midnight + img(src='/img/themes/origonal/clouds_midnight.jpg') + hr + h3 cobalt + img(src='/img/themes/origonal/cobalt.jpg') + hr + h3 crimson_editor + img(src='/img/themes/origonal/crimson_editor.jpg') + hr + h3 dawn + img(src='/img/themes/origonal/dawn.jpg') + hr + h3 dreamweaver + img(src='/img/themes/origonal/dreamweaver.jpg') + hr + h3 eclipse + img(src='/img/themes/origonal/eclipse.jpg') + hr + h3 ide_fingers + img(src='/img/themes/origonal/ide_fingers.jpg') + hr + h3 kr_theme + img(src='/img/themes/origonal/kr_theme.jpg') + hr + h3 merbivore_soft + img(src='/img/themes/origonal/merbivore.jpg') + hr + h3 merbivore_soft + img(src='/img/themes/origonal/merbivore_soft.jpg') + hr + h3 mono_industrial + img(src='/img/themes/origonal/mono_industrial.jpg') + hr + h3 pastles_on_dark + img(src='/img/themes/origonal/pastles_on_dark.jpg') + hr + h3 solarized_dark + img(src='/img/themes/origonal/solarized_dark.jpg') + hr + h3 solarized_light + img(src='/img/themes/origonal/solarized_light.jpg') + hr + h3 tomorrow + img(src='/img/themes/origonal/tomorrow.jpg') + hr + h3 tomorrow_night + img(src='/img/themes/origonal/tomorrow_night.jpg') + hr + h3 tomorrow_night_blue + img(src='/img/themes/origonal/tomorrow_night_blue.jpg') + hr + h3 tomorrow_night_bright + img(src='/img/themes/origonal/tomorrow_night_bright.jpg') + hr + h3 tomorrow_night_eightys + img(src='/img/themes/origonal/tomorrow_night_eightys.jpg') + hr + h3 twilight + img(src='/img/themes/origonal/twilight.jpg') + hr + h3 vibrant_ink + img(src='/img/themes/origonal/vibrant_ink.jpg') + // li.span3 + // a(href='#').thumbnail + // img(src='http://placehold.it/260x180', alt='') + // li.span3 + // a(href='#').thumbnail + // img(src='http://placehold.it/260x180', alt='') + // li.span3 + // a(href='#').thumbnail + // img(src='http://placehold.it/260x180', alt='') + // li.span3 + // a(href='#').thumbnail + // img(src='http://placehold.it/260x180', alt='') diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade new file mode 100644 index 0000000000..bbd213fb40 --- /dev/null +++ b/services/web/app/views/layout.jade @@ -0,0 +1,106 @@ +!!! +html(itemscope, itemtype='http://schema.org/Product') + head + - if (typeof(priority_title) !== "undefined" && priority_title) + title= title + ' - Online LaTeX Editor ShareLaTeX' + - else + title= 'Online LaTeX Editor ShareLaTeX - ' +title + link(rel='stylesheet', href='/stylesheets/mainStyle.css?fingerprint='+fingerprint('/stylesheets/mainStyle.css')) + + meta(itemprop="name" ,content="ShareLaTeX - Real Time Online LaTeX Collaborative Editor in Your Browser") + meta(itemprop="description", content="Online LaTeX editor for collaborative editing, great for Maths or Sciences. You don't need to install LaTeX so it's great for beginners too.") + meta(itemprop="image", content="https://www.sharelatex.com/favicon.ico") + meta(name="description", content="Online LaTeX editor for collaborative editing, great for Maths or Sciences. You don't need to install LaTeX so it's great for beginners too.") + + block scripts + + - if (typeof(gaToken) != "undefined") + script(type='text/javascript') + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '#{gaToken}']); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); + + - if (typeof(mixpanelToken) != "undefined") + script(type="text/javascript") + (function(c,a){window.mixpanel=a;var b,d,h,e;b=c.createElement("script"); + b.type="text/javascript";b.async=!0;b.src=("https:"===c.location.protocol?"https:":"http:")+ + '//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';d=c.getElementsByTagName("script")[0]; + d.parentNode.insertBefore(b,d);a._i=[];a.init=function(b,c,f){function d(a,b){ + var c=b.split(".");2==c.length&&(a=a[c[0]],b=c[1]);a[b]=function(){a.push([b].concat( + Array.prototype.slice.call(arguments,0)))}}var g=a;"undefined"!==typeof f?g=a[f]=[]: + f="mixpanel";g.people=g.people||[];h=['disable','track','track_pageview','track_links', + 'track_forms','register','register_once','unregister','identify','alias','name_tag', + 'set_config','people.set','people.increment','people.track_charge','people.append']; + for(e=0;ehttps://scribtex.sharelatex.com. Please update your bookmarks. + p(style="text-align: center") You can find the page you were looking for here: + p(style="text-align: center") + a(href="https://scribtex.sharelatex.com#{scribtexPath}", style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath} + .modal-footer + button(data-dismiss="modal").btn OK + +include modals + +if !locals.supressDefaultJs + script(type='text/javascript') + require = { + "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'main.js')}" + } + script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js?fingerprint=' + fingerprint(jsPath + 'libs/require.js'), baseurl=jsPath) + +- if (typeof(tenderUrl) != "undefined") + script(src="https://#{tenderUrl}/tender_widget.js" ) + script(type="text/javascript") + Tender = { + hideToggle: true, + widgetToggles: $(".js-tender-widget"), + category: "questions" + }; + +- if (typeof(heapToken) != "undefined" && session && session.user) + script(type="text/javascript") + var heap=heap||[];heap.load=function(a){window._heapid=a;var b=document.createElement("script");b.type="text/javascript",b.async=!0,b.src=("https:"===document.location.protocol?"https:":"http:")+"//cdn.heapanalytics.com/js/heap.js";var c=document.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c);var d=function(a){return function(){heap.push([a].concat(Array.prototype.slice.call(arguments,0)))}},e=["identify","track"];for(var f=0;f 0) + .row-fluid + .span9 + form.search + input#projectFilter(placeholder='Search Projects', autofocus='autofocus').span6.projectSearch.search-query + i.icon-search + i.icon-remove + ul#projectList + mixin projectList(projects) + + .span3 + .tag-list + h2 Tags + #allProjectTagsArea + ul + - each tag in tags + -if (tag.project_ids.length > 0) + li + mixin tag("", tag.name, false) + span.number x #{tag.project_ids.length} + + - else + .row-fluid + .span12 + .sharelatex-intro + .create-project-arrow Create your first project + .welcome + h2 Welcome to ShareLaTeX! + p New to LaTeX? Then have a look at our + a(href="/templates") templates + | or + a(href="/learn") help guides + | . + + + + if freeTrial && freeTrial.expired == false + .row-fluid + .span12 + .alert.alert-info.alert-free-trial + p You are currently using a free trial which expires on #{freeTrial.expiresAt}. + p + a(href="/user/subscription").btn.btn-primary Upgrade now + if freeTrial && freeTrial.expired == true + .row-fluid + .span12 + .alert.alert-danger.alert-free-trial + p Your free trial has expired! Upgrade now to continue using ShareLaTeX uninterrupted. + p + a(href="/user/subscription").btn.btn-danger Upgrade now + + include ../general/small-footer + script(type="text/template")#tagTemplate + mixin tag('{{ project_id }}', '{{ tagName }}', true) + + - locals.supressDefaultJs = true + script( + data-main=jsPath+'list.js?fingerprint='+fingerprint(jsPath + 'list.js'), + baseurl=jsPath, + src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') + ) + diff --git a/services/web/app/views/project/new.jade b/services/web/app/views/project/new.jade new file mode 100644 index 0000000000..834367ffde --- /dev/null +++ b/services/web/app/views/project/new.jade @@ -0,0 +1,19 @@ +.container + .page-header + h1 New File + .row + .span12.columns + form#newFile(enctype='multipart/form-data', method='post') + fieldset + .clearfix + label(for='xlInput') Name + .input + input.xlarge(type='text', name='name') + .clearfix + label(for='xlInput') Upload File + .input + input#file_upload.input-file(type='file', name='image') + + .actions + Button.primary.btn(type='submit') Save + diff --git a/services/web/app/views/project/partials/manage.jade b/services/web/app/views/project/partials/manage.jade new file mode 100644 index 0000000000..a6bca39ffc --- /dev/null +++ b/services/web/app/views/project/partials/manage.jade @@ -0,0 +1,77 @@ +.box + .page-header + h2 Project Settings + .tabbable + ul.nav.nav-tabs + li.active + a(href='#generalProjectSettings', data-toggle="tab") General + li + a(href='#exportSettings', data-toggle="tab") Export & Copy + li + a(href='#deleteProjectTab', data-toggle="tab") Delete Project + - if(userCanSeeDropbox) + li#manageDropboxSettiingsTabLink + a(href='#dropboxProjectSettings', data-toggle='tab') Dropbox + span.label.label-warning beta + + .tab-content.form-horizontal + .tab-pane#generalProjectSettings.form.form-horizontal.active + if privlageLevel == 'owner' || privlageLevel == 'readAndWrite' + .control-group + label(for='xlInput').control-label Project Name + .controls + .input + input.projectName(type='text', value=project.name) + + .control-group + label(for='input').control-label Root Document + .controls + .input + select#rootDocList + + .control-group + label(for='spellCheck').control-label + | Spell check + .controls + select(name="spellCheckLanguage")#spellCheckLanguageSelection + option(value="",selected=(project.spellCheckLanguage == "")) Off + optgroup(label="Language") + for language in languages + option( + value=language.code, + selected=(language.code == project.spellCheckLanguage) + )= language.name + + .control-group#multipleCompilers + label(for='input').control-label Compiler + .controls + .input + select#compilers + option(value='latex') LaTeX + option(value='pdflatex') pdfLaTeX + option(value='xelatex') XeLaTeX + option(value='lualatex') LuaLaTeX + else + span You do not have permission to modify these settings. + + if privlageLevel == 'owner' + .control-group + label(for='select').control-label Public Access + .controls + select#publicAccessLevel + option(value='private') Private + option(value='readOnly') Public - Read Only + option(value='readAndWrite') Public - Read and Write + + .tab-pane#exportSettings + a.btn#DownloadZip Download Project as Zip + div   + a.btn(href='/project/'+project._id+'/clone').cloneProject Clone Project + + .tab-pane#deleteProjectTab + if privlageLevel == 'owner' + button#deleteProject.btn.btn-danger Delete Project + else + span You do not have permission to modify these settings. + + .tab-pane#dropboxProjectSettings diff --git a/services/web/app/views/project/partials/pdf.jade b/services/web/app/views/project/partials/pdf.jade new file mode 100644 index 0000000000..8b8118549a --- /dev/null +++ b/services/web/app/views/project/partials/pdf.jade @@ -0,0 +1,38 @@ +#controls + button#previous + img(src='images/go-up.svg', align='top', height='32') + | Previous + button#next + img(src='images/go-down.svg', align='top', height='32') + | Next + .separator + input#pageNumber(type='number', value='1', size='4', min='1') + span / + span#numPages -- + .separator + button#next(title='Zoom Out') + img(src='images/zoom-out.svg', align='top', height='32') + button#next(title='Zoom In') + img(src='images/zoom-in.svg', align='top', height='32') + .separator + select#scaleSelect + option#customScaleOption(value='custom') + option(value='0.5') 50% + option(value='0.75') 75% + option(value='1') 100% + option(value='1.25') 125% + option(value='1.5', selected='selected') 150% + option(value='2') 200% + option#pageWidthOption(value='page-width') Page Width + option#pageFitOption(value='page-fit') Page Fit + .separator + button#print + img(src='images/document-print.svg', align='top', height='32') + | Print + .separator + input#fileInput(type='file') + .separator + span#info -- +#loading Loading... 0% +#viewer + diff --git a/services/web/app/views/project/table.jade b/services/web/app/views/project/table.jade new file mode 100644 index 0000000000..39151e16df --- /dev/null +++ b/services/web/app/views/project/table.jade @@ -0,0 +1,10 @@ +.container + table.table.table-striped.table-bordered#revisionList(cellpadding='0', cellspacing='0', border='0') + thead + tr + th Date + th Files Changed + tbody + + + diff --git a/services/web/app/views/referal/bonus.jade b/services/web/app/views/referal/bonus.jade new file mode 100644 index 0000000000..e9cb732e19 --- /dev/null +++ b/services/web/app/views/referal/bonus.jade @@ -0,0 +1,138 @@ +extends ../layout + +block content + .container.bonus.box + .row + .span8.offset2 + .page-header + h1 Recommend ShareLaTeX. Get free stuff. + + + .row + .span6.offset3 + h2 Help us spread the word about ShareLaTeX. + + .row + .span4.offset4.bonus-banner + .bonus-top + + .row + .span4.offset4.bonus-banner + .title + a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url=#{encodeURIComponent(buildReferalUrl("t"))}&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet + + .row + .span4.offset4.bonus-banner + .title + a(href='#', onclick='postToFeed(); return false;').facebook Post on Facebook + + .row + .span4.offset4.bonus-banner + .title + a(href="https://plus.google.com/share?url=#{encodeURIComponent(buildReferalUrl("gp"))}", onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus Share us on Google+ + + .row + .span4.offset4.bonus-banner + .title + a(href='mailto:?subject=Online LaTeX editor you may like, &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. #{encodeURIComponent(buildReferalUrl("e"))}', title='Share by Email').email Email us to your friends + + .row + .span4.offset4.bonus-banner + .title + a(href='#link-modal', data-toggle="modal").link Link to us from your website + + .row.ab-bonus + .span6.offset3 + p.thanks When someone starts using ShareLaTeX after your recommendation we'll give you some free stuff to say thanks! Check your progress below. + .row.ab-bonus + .span6.offset3(style="position: relative; height: 30px; margin-top: 20px;") + - for (var i = 0; i <= 10; i++) { + - if (refered_user_count == i) + .number(style="left: #{i}0%").active #{i} + - else + .number(style="left: #{i}0%") #{i} + - } + + .row.ab-bonus + .span6.offset3 + .progress(style="height: 25px") + - if (refered_user_count == 0) + div(style="text-align: center; padding: 4px;") Spread the word and fill this bar up + .bar(style="width: #{refered_user_count}0%") + + .row.ab-bonus + .span6.offset3(style="position: relative; height: 70px;") + .perk(style="left: 10%;", class = refered_user_count >= 1 ? "active" : "") One free collaborator + .perk(style="left: 30%;", class = refered_user_count >= 3 ? "active" : "") Three free collaborators + .perk(style="left: 60%;", class = refered_user_count >= 6 ? "active" : "") Free Dropbox and History + .perk(style="left: 90%;", class = refered_user_count >= 9 ? "active" : "") Free Professional account + + .row.ab-bonus + .span6.offset3 + - if (refered_user_count == 0) + p.thanks You've not introduced anyone to ShareLaTeX yet. Get sharing! + - else if (refered_user_count == 1) + p.thanks You've introduced #{refered_user_count} person to ShareLaTeX. Good job, but can you get some more? + - else + p.thanks You've introduced #{refered_user_count} people to ShareLaTeX. Good job! + + #link-modal.modal.hide + .modal-header + h3 Link to ShareLaTeX + .modal-body + p You can link to ShareLaTeX with the following HTML: + p + textarea(readonly=true) + Online LaTeX Editor ShareLaTeX + p Thanks! + .modal-footer + button.btn(data-dismiss="modal") Close + + include ../general/social-footer + include ../general/small-footer + + script(type='text/javascript', src='//platform.twitter.com/widgets.js') + script(src='https://connect.facebook.net/en_US/all.js') + script(type='text/javascript') + FB.init({appId: "148710621956179", status: true, cookie: true}); + + function postToFeed() { + + // calling the API ... + var obj = { + method: 'feed', + redirect_uri: 'https://www.sharelatex.com', + link: '!{buildReferalUrl("fb")}', + picture: 'https://www.sharelatex.com/img/logo/logosmall.png', + name: 'ShareLaTeX - Online LaTeX Editor', + caption: 'Free Unlimited Projects and Compiles', + description: 'ShareLaTeX is a free online LaTeX Editor. Real time collaboration like Google Docs, with Dropbox, history and auto-complete' + }; + + function callback(response) { + // document.getElementById('msg').innerHTML = "Post ID: " + response['post_id']; + } + + FB.ui(obj, callback); + } + + script(type="text/javascript") + $(function() { + mixpanel.track("Viewed referral page"); + $(".twitter").click(function() { + mixpanel.track("Clicked Bonus Referral Button", { medium: "twitter" }); + }); + $(".email").click(function() { + mixpanel.track("Clicked Bonus Referral Button", { medium: "email" }); + }); + $(".facebook").click(function() { + mixpanel.track("Clicked Bonus Referral Button", { medium: "facebook" }); + }); + $(".google-plus").click(function() { + mixpanel.track("Clicked Bonus Referral Button", { medium: "google_plus" }); + }); + $(".link").click(function() { + mixpanel.track("Clicked Bonus Referral Button", { medium: "direct" }); + }); + }); + diff --git a/services/web/app/views/referal/facebookLike.jade b/services/web/app/views/referal/facebookLike.jade new file mode 100644 index 0000000000..73fe385228 --- /dev/null +++ b/services/web/app/views/referal/facebookLike.jade @@ -0,0 +1,9 @@ +div.fb-like(data-href='https://www.sharelatex.com', ref='#{getReferalId()}', data-send='false', data-layout='box_count', data-width='450', data-show-faces='true', data-font='lucida grande') +script(type='text/javascript') + (function(d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) {return;} + js = d.createElement(s); js.id = id; + js.src = '//connect.facebook.net/en_GB/all.js#xfbml=1'; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); \ No newline at end of file diff --git a/services/web/app/views/referal/facebookWallPost.jade b/services/web/app/views/referal/facebookWallPost.jade new file mode 100644 index 0000000000..a07cf1ccfb --- /dev/null +++ b/services/web/app/views/referal/facebookWallPost.jade @@ -0,0 +1,25 @@ +a(onclick='postToFeed(); return false;').facebook Post on Facebook + +script(src='http://connect.facebook.net/en_US/all.js') +script(type='text/javascript') + FB.init({appId: "148710621956179", status: true, cookie: true}); + + function postToFeed() { + + // calling the API ... + var obj = { + method: 'feed', + redirect_uri: 'https://www.sharelatex.com', + link: '#{buildReferalUrl()}', + picture: 'https://www.sharelatex.com/img/logo/logosmall.png', + name: 'ShareLaTeX - Online LaTeX Editor', + caption: 'Free Unlimited Projects and Compiles', + description: 'ShareLaTeX is a free Online LaTeX Editor. Real time collaboration like Google Docs, with Dropbox, history and Auto Complete' + }; + + //function callback(response) { + // document.getElementById('msg').innerHTML = "Post ID: " + response['post_id']; + //} + + FB.ui(obj, callback); + } diff --git a/services/web/app/views/referal/googleplus.jade b/services/web/app/views/referal/googleplus.jade new file mode 100644 index 0000000000..c7ee30ff37 --- /dev/null +++ b/services/web/app/views/referal/googleplus.jade @@ -0,0 +1,10 @@ + + +script(type='text/javascript') + window.___gcfg = {lang: 'en-GB'}; + + (function() { + var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true; + po.src = 'https://apis.google.com/js/plusone.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); + })(); \ No newline at end of file diff --git a/services/web/app/views/referal/tweet.jade b/services/web/app/views/referal/tweet.jade new file mode 100644 index 0000000000..fb6cc4e15e --- /dev/null +++ b/services/web/app/views/referal/tweet.jade @@ -0,0 +1,2 @@ +a(href='https://twitter.com/share', counturl='https://www.sharelatex.com', data-text="is trying out online latex editor ShareLaTeX #{buildReferalUrl()}", data-count='vertical').twitter-share-button Tweet +script(type='text/javascript', src='//platform.twitter.com/widgets.js') diff --git a/services/web/app/views/referal/tweetShare.jade b/services/web/app/views/referal/tweetShare.jade new file mode 100644 index 0000000000..6dc95d0bf3 --- /dev/null +++ b/services/web/app/views/referal/tweetShare.jade @@ -0,0 +1,2 @@ +a(href='https://twitter.com/share', counturl='https://www.sharelatex.com', data-text="is trying out online latex editor ShareLaTeX #{buildReferalUrl()}", data-count='none').twitter Tweet about us +script(type='text/javascript', src='//platform.twitter.com/widgets.js') diff --git a/services/web/app/views/resources.jade b/services/web/app/views/resources.jade new file mode 100644 index 0000000000..101175736d --- /dev/null +++ b/services/web/app/views/resources.jade @@ -0,0 +1,59 @@ +extends layout + +block content + .container + .row + .span12.span-box + .page-header + h1 LaTeX Resources + small   We are the LaTeX Editor, here are some other LaTeX resources we like + div + ul + li + a(href='http://en.wikibooks.org/wiki/LaTeX') LaTeX Wikibook + span   - Covering 95% of what you need to know about LaTeX in a clear and simple way with great examples you can often copy and paste + li + a(href='http://detexify.kirelabs.org/classify.html') Detexify + span   - A great way of finding LaTeX symbols + li + a(href='http://www.tug.dk/FontCatalogue/seriffonts.html') LaTeX Fonts + span   - A collection of LaTeX fonts + li + a(href='http://mathurl.com/') MathUrl + span   - allows for live equation editing + li + a(href='http://webdemo.visionobjects.com/equation.html') WebEquation + span   - draw the symbol on the screen to see the LaTeX equivalent + li + a(href='http://www.texample.net/') TeXample.net + span   - a great site for tikz reference + li + a(href='http://www.howtotex.com/') howtoTeX.com + span   - a useful collection of templates, tutorials and how-tos + li + a(href='http://truben.no/latex/table/') LaTeX table editor + span   - if you struggle with tables in LaTeX this tool gives you a nice 'excel like' view to help you along + li + a(href="http://www.math.binghamton.edu/erik/beameruserguide.pdf") Beamer user guide + span   - A good guide into beamer + div + h3 Mobile Apps + ul + li + a(href='https://play.google.com/store/apps/details?id=coolcherrytrees.software.detexify&feature=search_result#?t=W251bGwsMSwxLDEsImNvb2xjaGVycnl0cmVlcy5zb2Z0d2FyZS5kZXRleGlmeSJd') Android + li + a(href='https://itunes.apple.com/app/detexify/id328805329?mt=8') Detexify for ios + li + a(href='http://www.windowsphone.com/en-us/store/app/detexify/3e6813bd-04b1-455b-bc14-14dfe904c54b') Detexify for Windows Phone + li + a(href='http://www.windowsphone.com/en-us/store/app/repotex/694085e0-e825-425b-a40d-40bce5cecb3b') Repotex + li + a(href='http://www.windowsphone.com/en-us/store/app/fasttexdraw/3e1b4255-7798-45ec-b679-a43e74e82769') FastTeXDraw for windows phone + + hr + div + | If you know of other resources, please + a.js-tender-widget(href='#') recommend +   them to us. + include ./general/small-footer + diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade new file mode 100644 index 0000000000..454852568b --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard.jade @@ -0,0 +1,83 @@ +extends ../layout + + + +mixin printPlan(plan) + -if (!plan.hideFromUsers) + tr + td + strong #{plan.name} + td + ul + -for benefit in plan.featureDescription + li #{benefit.text}   + if benefit.comingSoon + span.label.label-info coming soon + td + -if (plan.annual) + | $#{plan.price / 100} / year + -else + | $#{plan.price / 100} / month + td + -if (subscription.state == "free-trial") + a(href="/user/subscription/new?planCode=#{plan.planCode}").btn.btn-primary Subscribe to this plan + -else if (plan.planCode == subscription.planCode) + button.btn.disabled Your plan + -else + form(action="/user/subscription/update",method="post") + input(type="hidden", name="_csrf", value=csrfToken) + input(type="hidden",name="plan_code",value="#{plan.planCode}") + input(type="submit",value="Change to this plan").btn.btn-primary + +mixin printPlans(plans) + -each plan in plans + mixin printPlan(plan) + +block content + include ../general/sidebar + + .content-with-navigation-sidebar + .box + .row-fluid + .span12 + .page-header + h1 Your Subscription + p: case subscription.state + when "free-trial" + p You are currently using a free trial which expires on #{subscription.expiresAt}. + p Choose a plan below to subscribe to. + when "active" + p You are currently subscribed to the #{subscription.name} plan. + p The next payment of #{subscription.price} will be collected on #{subscription.nextPaymentDueAt} + form(action="/user/subscription/cancel",method="post") + input(type="hidden", name="_csrf", value=csrfToken) + input(type="submit",value="Cancel your subscription").btn.btn-danger + p: a(href="/user/subscription/billing-details/edit").btn.btn-primary Update your billing details + when "canceled" + p You are currently subscribed to the #{subscription.name} plan. + p Your subscription has been canceled and will terminate on #{subscription.nextPaymentDueAt}. No further payments will be taken. + form(action="/user/subscription/reactivate",method="post") + input(type="hidden", name="_csrf", value=csrfToken) + input(type="submit",value="Reactivate your subscription").btn.btn-success + when "expired" + p Your subscription has expired. + a(href="/user/subscription/plans") Create New Subscription + default + p There is a problem with your subscription. Please contact us for more information. + + -if(subscription.groupPlan) + a(href="/subscription/group").btn.btn-success Manage Group + hr + h2 Change plan + p: table.table + tr + th Name + th Features + th Price + th + mixin printPlans(plans.studentAccounts) + mixin printPlans(plans.individualMonthlyPlans) + mixin printPlans(plans.individualAnnualPlans) + + + diff --git a/services/web/app/views/subscriptions/edit-billing-details.jade b/services/web/app/views/subscriptions/edit-billing-details.jade new file mode 100644 index 0000000000..42f3f0de88 --- /dev/null +++ b/services/web/app/views/subscriptions/edit-billing-details.jade @@ -0,0 +1,22 @@ +extends ../layout + +block content + - locals.supressDefaultJs = true + script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath) + script(src=jsPath+'libs/recurly.min.js') + link(rel='stylesheet', href='/recurly/recurly.css') + + #billingDetailsForm.box Loading billing details form... + + script(type="text/javascript") + Recurly.config(!{recurlyConfig}) + Recurly.buildBillingInfoUpdateForm({ + target : "#billingDetailsForm", + successURL : "#{successURL}?_csrf=#{csrfToken}", + signature : "!{signature}", + accountCode : "#{user.id}" + }); + + include ../general/small-footer + + diff --git a/services/web/app/views/subscriptions/group_admin.jade b/services/web/app/views/subscriptions/group_admin.jade new file mode 100644 index 0000000000..78ce39256b --- /dev/null +++ b/services/web/app/views/subscriptions/group_admin.jade @@ -0,0 +1,45 @@ +extends ../layout + +block content + .container.box + .row + .span12 + .page-header + h2 Group Admin + + div You are allowed up to + strong #{subscription.membersLimit} + | members in this group + + table.table-striped.table.table-striped + thead + tr + th email + th Name + th Registered + + tbody#userList + -each user in users + tr + td #{user.email} + td #{user.first_name} #{user.last_name} + td #{!user.holdingAccount} + td + button.btn.btn-danger(id=user._id) Remove + + + form.well.form-inline#addUserToGroup + div + input(name="_csrf", type="hidden", value=csrfToken) + input(name="email", type="email", placeholder="someone@email.com")#newEmail.email.input-large   + button.btn.btn-primary.addUser Add + div   + div Add multiple emails seperated with commas or space. + + - locals.supressDefaultJs = true + script(data-main='/js/SubscriptionGroupsManager.js', src='/js/libs/require.js') + + + + + diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade new file mode 100644 index 0000000000..53a42c3224 --- /dev/null +++ b/services/web/app/views/subscriptions/new.jade @@ -0,0 +1,19 @@ +extends ../layout + +block content + - locals.supressDefaultJs = true + script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath) + script(src=jsPath+'libs/recurly.min.js') + link(rel='stylesheet', href='/recurly/recurly.css') + + #subscribeForm.box Loading subscription form... + + script(type="text/javascript") + mixpanel.track("Page Viewed", { name: "payment_form", plan: "#{plan_code}" }) + + script(type="text/javascript") + Recurly.config(!{recurlyConfig}) + Recurly.buildSubscriptionForm(!{subscriptionFormOptions}); + + include ../general/small-footer + diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade new file mode 100644 index 0000000000..f3e3e6d419 --- /dev/null +++ b/services/web/app/views/subscriptions/plans.jade @@ -0,0 +1,157 @@ +extends ../layout + +mixin liSection(feature) + | #{feature.text} + -if(feature.comingSoon) + span.label.label-info coming soon + -if(feature.beta) + span.label.label-warning beta + + +mixin plan(plan, cssClass, monthly) + .pricing-table + ul(class=cssClass) + + li.pricing-header-row-1 + .package-title + h2.no-bold #{plan.name} + + li.pricing-header-row-2 + .package-price + if plan.price == 0 + h1.free Free forever + else + h1.no-bold + | $#{plan.price/100} + if monthly + span.cents /month + else + span.cents /year + + - var odd = true + -each feature in plan.featureDescription + - odd = !odd + - if(odd) + li.pricing-content-row-odd + mixin liSection(feature) + - else + li.pricing-content-row-even + mixin liSection(feature) + + li.pricing-footer + - var href = '/user/subscription/new?planCode='+plan.planCode + + - planIsPersonal = plan.planCode.indexOf("personal") != -1 + - userNotLoggedIn = session && !session.user + -if(planIsPersonal) + - href = "/register" + -else if(userNotLoggedIn) + - href = "/register?redir="+href + a.btn.btn-success(href='#{href}').sign_up_now + | Sign Up Now! + + +block content + .container + .row + .span12.span-box + .page-header + h1 Choose your plan + blockquote.quote.pull-right + p + | This is one of the most useful resources I have ever found on the Internet. + br + | Fantastic execution and thoughtful attention to detail make this product shine! + small Benjamin Shepherd, Waterloo University + + .row + .span12 + .offset3 + ul.nav.nav-pills.pricing-pills + li.active + a(href="#", data-target=".monthly-pricing", data-toggle="tab") Monthly + li + a(href="#", data-target=".annual-pricing", data-toggle="tab") Annual + li + a(href="#", data-target=".student-pricing", data-toggle="tab") Half price student plans + + .row + .span12 + .page-header + h2 Individual Plans + + .pricing-steelblue.pricing-row + .tab-content + .tab-pane.active.monthly-pricing + .row + .span12.tagline + p An online LaTeX editor for collaborating on the same LaTeX project and editing together in real-time. You’ll never go out of sync with your collaborators again, or lose track of any changes. + .row + .span4 + mixin plan(plans.personalAccount, "", true) + .span4 + mixin plan(plans.individualMonthlyPlans[0], "big", true) + .span4 + mixin plan(plans.individualMonthlyPlans[1], "", true) + + .tab-pane.annual-pricing + .row + .span12.tagline + p Collaborate on the same LaTeX project and edit together in real-time. You’ll never go out of sync with your collaborators again, or lose track of any changes. + .span4 + mixin plan(plans.personalAccount, "", true) + .span4 + mixin plan(plans.individualAnnualPlans[0], "big", false) + .span4 + mixin plan(plans.individualAnnualPlans[1], "", false) + + .tab-pane.student-pricing + .row + .span8.offset2.tagline + p Getting started and working with LaTeX has never been so easy. Start creating beautiful work now. + .span4 + mixin plan(plans.personalAccount, "", true) + .span4 + mixin plan(plans.studentAccounts[0], "big", true) + .span4 + mixin plan(plans.studentAccounts[1], "", false) + + .row + .span12.ab-guarantee-shown(style="text-align: center;") + h2 30 day money back guarantee, cancel anytime. + + .pricing-steelblue.pricing-row + .tab-content + .tab-pane.active.monthly-pricing + .page-header + h2 Group Plans + .row + .span12.tagline + p Improve the workflow of your research group by unlocking ShareLaTeX's premium features for everyone on your team + .row + .span4 + mixin plan(plans.groupMonthlyPlans[0], "", true) + .span4 + mixin plan(plans.groupMonthlyPlans[1], "big", true) + .span4 + mixin plan(plans.groupMonthlyPlans[2], "", true) + .tab-pane.annual-pricing + .page-header + h2 Group Plans + .row + .span12.tagline + p Improve the workflow of your research group by unlocking ShareLaTeX's premium features for everyone on your team + .row + .span4 + mixin plan(plans.groupAnnualPlans[0], "", false) + .span4 + mixin plan(plans.groupAnnualPlans[1], "big", false) + .span4 + mixin plan(plans.groupAnnualPlans[2], "", false) + .tab-pane.student-pricing + + include ../general/small-footer + link(rel='stylesheet', href='/stylesheets/plans.css?fingerprint='+fingerprint('/stylesheets/mainStyle.css')) + + script + mixpanel.track("Page Viewed", { name: "plans" }) diff --git a/services/web/app/views/subscriptions/successful_subscription.jade b/services/web/app/views/subscriptions/successful_subscription.jade new file mode 100644 index 0000000000..569490a163 --- /dev/null +++ b/services/web/app/views/subscriptions/successful_subscription.jade @@ -0,0 +1,33 @@ +extends ../layout + +block content + link(href='http://fonts.googleapis.com/css?family=Just+Another+Hand', rel='stylesheet', type='text/css') + .container + .row + div   + .span8.offset2.span-box + .page-header + h2 Thanks for subscribing! + .alert.alert-success + p Your card will be charged soon. + p The next payment of #{subscription.price} will be collected on #{subscription.nextPaymentDueAt}, if you do not want to be charged again + a(href="/user/subscription") click here to cancel. + div + p + - if (subscription.groupPlan == true) + a.btn.btn-success.btn-large(href="/subscription/group") Add your first group members now + div.letter-from-founders + p Thank you for subscribing to the #{subscription.name} plan. It's support from people like yourself that allows ShareLaTeX to continue to grow and improve. + + p If there is anything you ever need please feel free to contact us directly at + a(href='mailto:team@sharelatex.com') team@sharelatex.com + | - it goes straight to both our inboxes. + p Regards, + br + | Henry and James + .portraits + img(src="/img/about/henry_oswald.jpg") +   + img(src="/img/about/james_allen.jpg") + div + a.btn.btn-primary(href="/project") < Back to your projects diff --git a/services/web/app/views/templates.jade b/services/web/app/views/templates.jade new file mode 100644 index 0000000000..fe815fd3ca --- /dev/null +++ b/services/web/app/views/templates.jade @@ -0,0 +1,505 @@ +#templates(style='display : none') + + script(type="text/template")#editorLayoutTemplate + div#mainSplitter + aside#sidebar.ui-layout-west + //input#search-field(type='search', placeholder='Filter Files by Name') + // Position:relative is to get scrolling while dragging to work: + // http://stackoverflow.com/questions/1718547/jquery-draggable-scroll-not-working-when-helper-clone-is-used + #sections(style="position: relative;") + #options + span#saving-area + + #content.content.ui-layout-center + #loading.fullEditorArea + #disconnect(style='display: none;').fullEditorArea + #mainAreaMessage Sorry, your browser has lost the connection to our server. Please try refreshing the page. + #projectDeleted(style='display: none;').fullEditorArea + #mainAreaMessage This project has been renamed or deleted. + #folderArea(style='display: none;').fullEditorArea + #imageArea(style='display: none;').fullEditorArea + iframe + + script(type="text/template")#tabTemplate + li(id="{{ id }}-tab-li") + a(href="#", data-toggle="tab", data-target="\\#{{ id }}-tab", class="tab-link {{ id }}-tab") + .content {{ name }} + + script(type="text/template")#tabContentTemplate + div.tab-pane(id="{{ id }}-tab") + + script(type="text/template")#fileTreeTemplate + .file-tree.js-file-tree + + script(type="text/template")#fileTreeActionsTemplate + .actions + .new-entity.dropdown.js-new-entity-menu + a.dropdown-toggle(href="#", data-toggle="dropdown", title="New file, folder or upload") + i.icon-plus + span.text New + ul.dropdown-menu + li + a.js-new-file(href="#") + img(src="/img/doc.png") + | New File + li + a.js-new-folder(href="#") + img(src="/img/folder.png") + | New Folder + li + a.js-upload-file(href="#") + img(src="/img/upload-file.png") + | Upload file(s) + .js-rename-btn.rename-btn + a(href="#", title="Rename") + i.icon-pencil + .js-delete-btn.delete-btn + a(href="#", title="Delete") + i.icon-trash + + script(type="text/template")#rootDocListEntity + option {{name}} + + script(type="text/template")#entityTemplate + .entity-list-item(class="entity-{{ type }}", entity-type="{{ type }}", id="{{ id }}") + .clickable.js-clickable + i(class="sprite-{{ type }}") + span.name {{ name }} + input.rename.js-rename + + script(type="text/template")#folderTemplate + .entity-list-item(class="entity-{{ type }}", entity-type="{{ type }}", id="{{ id }}") + .toggle.js-toggle + img(src="/img/right-arrow.png").js-closed + img(src="/img/down-arrow.png").js-open + .clickable.js-clickable + i(class="sprite-{{ type }}") + span.name {{ name }} + input.rename.js-rename + + script(type="text/template")#entityListTemplate + .contents + .entity-list(id="{{ id }}-file-list") + + script(type="text/template")#newEntityModalTemplate + div + input.inputmodal(placeholder="name") + + script(type="text/template")#messageTemplate + .chatMessage + span.name {{name}} + span : + span.message {{message}} + + script(type="text/template")#spellingMenuTemplate + div.btn-group.spell-check-menu + a.btn.dropdown-toggle(data-toggle="dropdown", href="#") + span.underlined Ab + ul.dropdown-menu.pull-right + li.divider + li + a#learnWord(href="#") Learn word + + script(type="text/template")#spellingMenuEntryTemplate + li.spelling-suggestion + a(href="#") {{word}} + + script(type="text/template")#contextMenuTemplate + ul.dropdown-menu.context-menu + + script(type="text/template")#contextMenuEntryTemplate + li + a(href="#") {{text}} + + script(type="text/template")#genericModalTemplate + .modal + .modal-header + h3 {{ title }} + .modal-body + .message {{{ message }}} + .creditCardFreeTrialModal + .modal-footer + + + script(type="text/template")#creditCardFreeTrialModal + .modal + .modal-header + h3 {{ title }} + .modal-body + #subscribeForm + .modal-footer + + + script(type="text/template")#genericModalTemplateWithButton + .modal + .modal-header + h3 {{ title }} + .modal-body + .message {{ message }} + .modal-footer + button.btn.btn-primary ok + + script(type="text/template")#genericModalButtonTemplate + a(href="#",class="btn {{ class }}") {{ text }} + + + script(type="text/template")#editorPanelTemplate + #editorArea(style='display: none;') + #editorSplitter + #leftEditorPanel.ui-layout-center + #editor + #undoConflictWarning(style="display: none") + | Watch out! We had to undo some of your collaborators changes before we could undo yours. + a(href="#").js-hide Hide + #rightEditorPanel.ui-layout-east + + script(type="text/template")#loadingIndicatorTemplate + .loading + + script(type="text/template")#settingsSideBarLinkTemplate + ul + li.root.project#settings(title='Show Project Settings') + img(src='/img/settings.png') + span Settings + + script(type="text/template")#usersSideBarLinkTemplate + ul + li.root.project#projectCollaberators(title='Show Project Collaborators') + img(src='/img/user.png') + span Collaborators + + script(type="text/template")#pdfSideBarLinkTemplate + ul + li.root.project#pdf(title='Show PDF', alt='pdf') + img(src='/img/pdf.png') + span PDF + + script(type="text/template")#helpLinkTemplate + div + a(href="#", title="LaTeX Help") LaTeX Help + + script(type="text/template")#editorTourTemplate + div + a(href="#", title="Editor Tour") Editor Tour + + script(type="text/template")#pdfPanelTemplate + #pdfArea(style='display: none;').fullEditorArea + #pdfToolBar.btn-toolbar + .btn-group + button#recompilePdf.btn.btn-success(type='button') Recompile + .btn-group#showLogGroup + button#showLog.btn Logs + .btn-group#showPdfGroup + button#showPdf.btn Back to PDF + .btn-group + button#downloadPdf.btn Download + button#downloadLinksButton.btn.dropdown-toggle(data-toggle="dropdown") + span.caret + ul.dropdown-menu#downloadLinks + .btn-group.pull-right(data-toggle="buttons-radio") + button(type="button", title="Flat view")#flatViewButton.btn + i.icon-flatview + button(type="button", title="Split view")#splitViewButton.btn + i.icon-splitview + #pdfAreaContent + .not-compiled-yet-message + | Click here to preview your work as a PDF. + .compiling-message(style='display: none;') Compiling... + #logArea(style='display: none;') + ul + button.btn.btn-info.btn-large#showRawLog Show Raw Logs + #rawLogArea(style='display: none;') + pre + + script(type="text/template")#outputFileLinkTemplate + li + a(href="/project/{{ project_id }}/output/{{ path }}", target="_blank") Download {{ name }} + + script(type="text/template")#pdfjsViewerTemplate + .pdfjs-viewer + .pdfjs-list-view + .btn-group + button.btn.btn-info.js-fit-height + img(src="/img/fit-to-height.png") + button.btn.btn-info.js-fit-width + img(src="/img/fit-to-width.png") + button.btn.btn-info.js-zoom-out + img(src="/img/zoom-out.png") + button.btn.btn-info.js-zoom-in + img(src="/img/zoom-in.png") + .progress.progress-info + .bar + span Loading + + script(type="text/template")#compileSuccessTemplate + li.alert.alert-success + strong No errors. + span Great Job! + + script(type="text/template")#compileErrorTemplate + li.alert.alert-error + strong Compile Error: + span Sorry, something went wrong and the project could not be compiled. This may be due to our compiler being overloaded or an incompatibility with the project. Please try again in a few moments and if the problem continues let us know via the feedback tab at the top. + + script(type="text/template")#compileFailedTemplate + li.alert.alert-error + strong Ooops, your LaTeX code couldn't compile for some reason. Please check these errors for details: + + + script(type="text/template")#compileLogEntryTemplate + li.alert.clickable(class="alert-{{ type }}") + strong {{ title }}: + span {{ message }} + .small {{ content }} + + script(type="text/template")#projectMemberListTemplate + table.table + thead + tr + th Email + th Privileges + th + tbody + {{#showAdminControls}} + form.well.form-inline.addUserForm + input(type="email",placeholder="someone@email.com")#newEmail.email.input-large + select.privileges.input-medium + option(value="readAndWrite") Read and write + option(value="readOnly") Read Only + button.btn.btn-primary.addUser Add New Collaborator + {{/showAdminControls}} + + script(type="text/template")#projectMemberListItemTemplate + tr.projectMember + td.email {{ email }} + td.privileges {{ privileges }} + td + {{#showRemove}} + button(href="#").btn.btn-danger.removeUser Remove + {{/showRemove}} + + script(type="text/template")#socialSharingTemplate + .box + .page-header + h2 Share Publicly + link(href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css",rel="stylesheet") + div.share-button + a.btn.btn-twitter.btn-large(href="#") + i.icon-twitter + |   Share on Twitter + div.share-button + .btn.btn-facebook.btn-large + i.icon-facebook + |   Share on Facebook + div.share-button + .btn.btn-google-plus.btn-large + i.icon-google-plus + |   Share on Google+ + div.share-button + .btn.btn-large.btn-url + i.icon-link + |   Share URL + + + script(type="text/template")#publishProject + -if(session && session.user && session.user.isAdmin) + .box + .page-header + h2 Publish Project + + div + .btn#publishProjectAsTemplate Publish project as template + .btn#unPublishProjectAsTemplate unpublish project as template + .row + textarea.span6#projectDescription {{description}} + + script(type="text/template")#settingsPanelTemplate + .fullEditorArea.projectSettings + include project/partials/manage + + script(type="text/template")#userPanelTemplate + .fullEditorArea.projectSettings + .box + .page-header + h2 Share Privately (via email) + #projectMembersList + #socialSharing + #publishProject + + script(type="text/template")#historyPanelTemplate + #revisionHistoryArea.fullEditorArea + + script(type="text/template")#historySideBarLinkTemplate + ul + li.root.project#showHistory(title='Show project history', alt='history') + img(src='/img/clock.png') + span History + + script(type='text/template')#revisionAreaTemplate + #historyAreaWrapper + #historySideBar + .take-snapshot-wrapper + a#take-snapshot.btn.btn-primary Take Snapshot + #versionListArea + #diffViewArea + #enableVersioningMessage + .message History is not yet enabled for this project. + a.btn.btn-primary.btn-large#enableVersioning Enable history + + + script(type='text/template')#snapshotCommentTemplate + input(type="text", placeholder="Snapshot comment")#snapshotComment + + script(type='text/template')#diffTemplate + .diffView + h2 {{ message }} + .date {{ date }} + + script(type='text/template')#fileDiffTemplate + .fileHeader + {{#diff}} + {{^deleted}} + ul.nav.nav-pills.pull-right + li.active + a(href="#diff-{{id}}", data-toggle="pill").diff Diff + li + a(href="#raw-{{id}}", data-toggle="pill").raw Raw + {{/deleted}} + {{/diff}} + h3(class="{{headerClass}}") {{path}} + {{#moved}} + .fileMoved Moved to {{newPath}} + {{/moved}} + {{#diff}} + .tab-content + .tab-pane.active.tab-diff(id="diff-{{id}}") + {{#sections}} + table.sectionDiff + {{#lines}} + tr(class="line {{type}}") + {{#added}} + td.old_line_number + td.new_line_number {{new_number}} + td.symbol + + {{/added}} + {{#removed}} + td.old_line_number {{old_number}} + td.new_line_number + td.symbol - + {{/removed}} + {{#unchanged}} + td.old_line_number {{old_number}} + td.new_line_number {{new_number}} + td.symbol + {{/unchanged}} + td.content {{content}} + {{/lines}} + .sectionSeparator + {{/sections}} + .tab-pane.tab-raw(id="raw-{{id}}") + pre.rawFileContent Loading... + {{/diff}} + {{#binary}} + {{^deleted}} + .binaryFileDiff + a(href="{{url}}",target="_blank").rawFileLink View this version of the file + {{/deleted}} + {{/binary}} + + script(type='text/template')#versionListItemTemplate + a(href="#") + div(class='version-message') {{message}} + div(class='version-date') {{date}} + + script(type='text/template')#autoCompleteSuggestionTemplate + li + strong {{base}} + | {{completion}} + + script(type='text/template')#versionListTemplate + ul#version-list.nav.nav-pills.nav-stacked + li.loading Loading... + li.empty-message You don't have any versions yet! + + script(type='text/template')#fileViewTemplate + {{#image}} + div + img(src='{{ previewUrl }}') + {{/image}} + {{^image}} + .no-preview No preview available :( + {{/image}} + a(href='{{ downloadUrl }}', target="_blank").download.btn.btn-large Download {{ name }} + + script(type='text/template')#hotKeysLinkTemplate + div + a(href="#", title='Show Hot Keys List') Hot keys + + script(type='text/template')#hotKeysListTemplate + .hotkeys + h3 Common + .hotkeys-column + .hotkey + span.combination.win Ctrl + F + span.combination.mac Cmd + F + span.description Find (and replace) + .hotkey + span.combination.win Ctrl + Enter + span.combination.mac Cmd + Enter + span.description Compile + .hotkey + span.combination.win Ctrl + / + span.combination.mac Cmd + / + span.description Toggle Comment + .hotkeys-column + .hotkey + span.combination.win Ctrl + Z + span.combination.mac Cmd + Z + span.description Undo + .hotkey + span.combination.win Ctrl + Y + span.combination.mac Cmd + Y + span.description Redo + .clear + + h3 Navigation + .hotkeys-column + .hotkey + span.combination.win Ctrl + Home + span.combination.mac Cmd + Home + span.description Beginning of document + .hotkey + span.combination.win Ctrl + End + span.combination.mac Cmd + End + span.description End of document + .hotkeys-column + .hotkey + span.combination.win Ctrl + L + span.combination.mac Cmd + L + span.description Go To Line + .clear + + h3 Editing + .hotkeys-column + .hotkey + span.combination.win Ctrl + D + span.combination.mac Cmd + D + span.description Delete Current Line + .hotkey + span.combination.win Ctrl + A + span.combination.mac Cmd + A + span.description Select All + .hotkey + span.combination.win Tab + span.combination.mac Tab + span.description Indent Selection + .hotkeys-column + .hotkey + span.combination.win Ctrl + U + span.combination.mac Ctrl + U + span.description To Uppercase + .hotkey + span.combination.win Ctrl + Shift + U + span.combination.mac Ctrl + Shift + U + span.description To Lowercase + .clear diff --git a/services/web/app/views/templates/dropbox.jade b/services/web/app/views/templates/dropbox.jade new file mode 100644 index 0000000000..146df5bc43 --- /dev/null +++ b/services/web/app/views/templates/dropbox.jade @@ -0,0 +1,15 @@ +script(type='text/template')#userNotLinkedToDropboxTemplate + .alert.alert-warning.userDropboxStatus + span Your account is not linked to dropbox + |     + a(href='/user/settings#dropboxSettings').btn.btn-warning Update Dropbox Settings + +script(type='text/template')#userLinkedToDropboxTemplate + row + div + strong {{minsTillNextPoll}} minutes + | until dropbox is next checked for changes + div   + .progress.progress-striped.active + .bar(style='width: {{percentageLeftTillNextPoll}}%;') + diff --git a/services/web/app/views/tests.jade b/services/web/app/views/tests.jade new file mode 100644 index 0000000000..99a948a7fe --- /dev/null +++ b/services/web/app/views/tests.jade @@ -0,0 +1,14 @@ +!!!html5 +head + title Mocha Tests + link(rel="stylesheet",href="stylesheets/mocha.css") + script require = { baseUrl : "/js", "urlArgs" : "fingerprint=#{date}", "paths": { "underscore" : "libs/underscore" } } +body + #mocha + #test-area() + include templates + script(type="text/javascript") + window.userSettings = { + project_id: "test-project" + } + script(src="/js/libs/require.js",data-main="tests/unit/run.js") diff --git a/services/web/app/views/user/feedback.jade b/services/web/app/views/user/feedback.jade new file mode 100644 index 0000000000..db984e31bf --- /dev/null +++ b/services/web/app/views/user/feedback.jade @@ -0,0 +1,14 @@ +extends ../layout + +block content + .container + .box + .page-header + h1 Feedback + form(method='post') + .clearfix + label(for='xlInput') If you are having trouble with a project please include the url e.g. www.sharelatex.com/project/50c49b73ef78e1ad0c000006 + .input + textarea.span11(type='text', name='message').feedback + .actions + button.btn.btn-large.btn-primary(type='submit') Send diff --git a/services/web/app/views/user/login.jade b/services/web/app/views/user/login.jade new file mode 100644 index 0000000000..b096dd15ab --- /dev/null +++ b/services/web/app/views/user/login.jade @@ -0,0 +1,24 @@ +extends ../layout + +block content + .container + .row + span4.offset4.span-box + .page-header + h1 Login + .messageArea + form.validate#loginForm(enctype='multipart/form-data', method='post') + input(name='_csrf', type='hidden', value=csrfToken) + input(name='redir', type='hidden', value=redir) + .clearfix + label(for='xlInput') Email + .input + input.span4#email.email.required(type='email', autofocus="autofocus", name='email', placeholder='your@email.com') + .clearfix + label(for='xlInput') Password + .input + input.span4#password.required(type='password', name='password', placeholder='********') + .actions + button.btn-primary.btn.btn-large#login(type='submit') Login + a#passwordReset(href='/user/passwordreset') forgot password? + include ../general/small-footer diff --git a/services/web/app/views/user/passwordReset.jade b/services/web/app/views/user/passwordReset.jade new file mode 100644 index 0000000000..5bc8733a10 --- /dev/null +++ b/services/web/app/views/user/passwordReset.jade @@ -0,0 +1,17 @@ +extends ../layout + +block content + .container + .row + .box.span4.offset4 + .page-header + h1 Password Reset + .messageArea + form.validate#passwordReset(method='post') + input(type="hidden", name="_csrf", value=csrfToken) + .clearfix + label(for='xlInput') Email + .input + input.span4.email.required(type='email', name='email', placeholder='your@email.com') + .actions + button.btn.btn-primary.btn.btn-large(type='submit') Submit diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade new file mode 100644 index 0000000000..6a8a0ff5d8 --- /dev/null +++ b/services/web/app/views/user/register.jade @@ -0,0 +1,42 @@ +extends ../layout + +block content + .container + .row + .registration_message + if sharedProjectData.user_first_name !== undefined + h1 #{sharedProjectData.user_first_name} would like you to view '#{sharedProjectData.project_name}' + div Join ShareLaTeX to view this project + else if newTemplateData.templateName !== undefined + h1 Please register to edit the '#{newTemplateData.templateName}' template + div Already have a ShareLaTeX account? + a(href="/login") Login here + + .row + .span-box.span4.offset4 + .page-header + h1 Register + .messageArea + form#registerFormShort(method="post") + input(name='_csrf', type='hidden', value=csrfToken) + input(name='redir', type='hidden', value=redir) + .clearfix + label(for='xlInput') Email + .input + input#email.span4.email.required(type='email', name='email', value='#{new_email}') + .clearfix + label(for='xlInput') Password + .input + input#password.span4.required(type='password', name='password') + .actions + button#registerButton.btn-primary.btn.btn-xlarge(type='submit') Register + + + + include ../general/small-footer + + script + mixpanel.track("Page Viewed", { name: "register" }) + $('#registerButton').click(function(){ + mixpanel.track("registerpage.registerd") + }) diff --git a/services/web/app/views/user/restricted.jade b/services/web/app/views/user/restricted.jade new file mode 100644 index 0000000000..3ce1b5a0ff --- /dev/null +++ b/services/web/app/views/user/restricted.jade @@ -0,0 +1,10 @@ +extends ../layout + +block content + + .container#loginBox + .row + .box.span3.offset4 + .page-header + h1 Restricted + diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.jade new file mode 100644 index 0000000000..8640ce3a1d --- /dev/null +++ b/services/web/app/views/user/settings.jade @@ -0,0 +1,176 @@ +extends ../layout + +block content + include ../general/sidebar + + .content-with-navigation-sidebar + .box + .row-fluid + .span12 + .page-header + h1 Settings + .messageArea + .tabbable + ul.nav.nav-tabs + li.active + a(href='#personalSettings', data-toggle="tab") Personal + li + a(href='#editorSettings', data-toggle="tab") Editor + li + a(href='#passwordReset', data-toggle="tab") Password + li + a(href='#newsletter', data-toggle="tab") Newsletter + - if(userCanSeeDropbox) + li + a(href='#dropboxSettings', data-toggle="tab") Dropbox + span.label.label-warning beta + li + a(href='#deleteAccount', data-toggle="tab") Delete Account + + form#userSettings.tab-content.form-horizontal + input(type="hidden", name="_csrf", value=csrfToken) + .tab-pane#personalSettings.active + .control-group + label(for='firstName').control-label First Name + .controls + input#firstName(type='text', name='first_name', value=user.first_name) + .control-group + label(for='lastName').control-label Last Name + .controls + input#lastName(type='text', name='last_name', value=user.last_name) + .form-actions + button.btn.btn-primary.large(type='submit') Update + + .tab-pane#dropboxSettings + .alert.alert-info + a(href='/help/kb/dropbox-2') Read how dropbox works + - if(!userHasDropboxFeature) + .alert.alert-info Dropbox sync is a premium feature     + a.btn.btn-info(href='/user/subscription/plans') Upgrade + - else if(userIsRegisteredWithDropbox) + .alert.alert-success Account is linked! + row + a(href='/dropbox/unlink').btn Unlink Dropbox + - else + a.btn.btn-info(href='/dropbox/beginAuth') Link to dropbox + + + .tab-pane#editorSettings + .control-group + label(for='theme').control-label Theme   + a(href='/themes') (preview) + .controls + select(name='theme')#theme + each theme in themes + if(theme.name == user.ace.theme) + option(value=theme.name, selected='selected')= theme.name + else + option(value=theme.name)= theme.name + .control-group + label(for='mode').control-label Key Binding + .controls + select(name='mode')#mode + each mode in editors + if(mode == user.ace.mode) + option(value=mode, selected='selected')= mode + else + option(value=mode)= mode + .control-group + label(for='fontSize').control-label Font Size + .controls + select(name='fontSize')#fontSize + each fontSize in fontSizes + if(fontSize == user.ace.fontSize) + option(value=fontSize, selected='selected')= fontSize + else + option(value=fontSize)= fontSize + + .control-group + label(for='autoComplete').control-label + | Auto complete + .controls + if (user.ace.autoComplete) + label.radio.inline + input(type='radio', name='autoComplete', value='true', checked) + | On + label.radio.inline + input(type='radio', name='autoComplete', value='false') + | Off + else + label.radio.inline + input(type='radio', name='autoComplete', value='true') + | On + label.radio.inline + input(type='radio', name='autoComplete', value='false', checked) + | Off + + .control-group + label(for='spellCheck').control-label + | Default Spell check language + .controls + select(name="spellCheckLanguage") + option(value="",selected=(user.ace.spellCheckLanguage == "")) Off + optgroup(label="Language") + for language in languages + option( + value=language.code, + selected=(language.code == user.ace.spellCheckLanguage) + )= language.name + + .control-group + label(for="pdfViewer").control-label + | PDF Viewer + .controls + if (user.ace.pdfViewer == "native") + label.radio.inline + input(type='radio', name='pdfViewer', value='native', checked) + | Native (Best image quality) + br + label.radio.inline + input(type='radio', name='pdfViewer', value='pdfjs') + | Built in (Pdf page stays the same after recompile) + else + label.radio.inline + input(type='radio', name='pdfViewer', value='native') + | Native (Best image quality) + br + label.radio.inline + input(type='radio', name='pdfViewer', value='pdfjs', checked) + | Built in (Pdf page stays the same after recompile) + + + .form-actions + button.btn.btn-primary.large(type='submit') Update + + .tab-pane#passwordReset + a#changePassword.btn Change Password + + .tab-pane#deleteAccount + a#deleteUserAccount.btn.btn-danger(data-csrf=csrfToken) Delete your account + + .tab-pane#newsletter + p Every few months we send a news letter out summarizing the new features available, if you would prefer to not receive this email then you are free to unsubscribe below at any time + a#unsubscribeFromNewsletter.btn.btn-danger(data-csrf=csrfToken) Unsubscribe + + #changePasswordModal(style='display: none') + .modal + form#changePasswordForm(method="post", action="/user/password/update") + .modal-header + h3 Change Password + .modal-body + input(type="hidden", name="_csrf", value=csrfToken) + .clearfix + label(for='xlinput.inputmodal') Current Password + .input.inputmodal + input.inputmodal.span5#currentPassword(type='password', name='currentPassword', placeholder='*********') + .clearfix + label(for='xlinput.inputmodal') New Password + .input.inputmodal + input.inputmodal.span5#newPassword1(type='password', name='newPassword1', placeholder='************') + .clearfix + label(for='xlinput.inputmodal') New Password + .input.inputmodal + input.inputmodal.span5#newPassword2(type='password', name='newPassword2', placeholder='************') + .modal-footer + button(type="submit").btn.btn-primary#confirmPasswordChange Change + button.btn.cancel Cancel diff --git a/services/web/config/.gitignore b/services/web/config/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/config/settings.development.coffee b/services/web/config/settings.development.coffee new file mode 100644 index 0000000000..8404481f40 --- /dev/null +++ b/services/web/config/settings.development.coffee @@ -0,0 +1,204 @@ +Path = require('path') +http = require('http') +http.globalAgent.maxSockets = 300 + +# Make time interval config easier. +seconds = 1000 +minutes = 60 * seconds + +# These credentials are used for authenticating api requests +# between services that may need to go over public channels +httpAuthUser = "sharelatex" +httpAuthPass = "password" +httpAuthUsers = {} +httpAuthUsers[httpAuthUser] = httpAuthPass + +sessionSecret = "secret-please-change" + +module.exports = + # File storage + # ------------ + # + # ShareLaTeX stores binary files like images in S3. + # Fill in your Amazon S3 credential below. + s3: + key: "" + secret: "" + bucketName : "" + + # Databases + # --------- + mongo: + url : 'mongodb://127.0.0.1/sharelatexTesting' + + redis: + web: + host: "localhost" + port: "6379" + password: "" + + api: + host: "localhost" + port: "6379" + password: "" + + # Service locations + # ----------------- + + # Configure which ports to run each service on. Generally you + # can leave these as they are unless you have some other services + # running which conflict, or want to run the web process on port 80. + internal: + web: + port: webPort = 3000 + documentupdater: + port: docUpdaterPort = 3003 + + # Tell each service where to find the other services. If everything + # is running locally then this is easy, but they exist as separate config + # options incase you want to run some services on remote hosts. + apis: + web: + url: "http://localhost:#{webPort}" + user: httpAuthUser + pass: httpAuthPass + documentupdater: + url : "http://localhost:#{docUpdaterPort}" + thirdPartyDataStore: + url : "http://localhost:3002" + emptyProjectFlushDelayMiliseconds: 5 * seconds + tags: + url :"http://localhost:3012" + spelling: + url : "http://localhost:3005" + versioning: + snapshotwaitms:3000 + url: "http://localhost:4000" + username: httpAuthUser + password: httpAuthPass + recurly: + privateKey: "" + apiKey: "" + subdomain: "" + chat: + url: "http://localhost:3010" + templates: + port: 3007 + blog: + port: 3008 + filestore: + url: "http://localhost:3009" + clsi: + url: "http://localhost:3013" + templates_api: + url: "http://localhost:3014" + + # Where your instance of ShareLaTeX can be found publically. Used in emails + # that are sent out, generated links, etc. + siteUrl : 'http://localhost:3000' + + # Same, but with http auth credentials. + httpAuthSiteUrl: 'http://#{httpAuthUser}:#{httpAuthPass}@localhost:3000' + + # Security + # -------- + security: + sessionSecret: sessionSecret + + httpAuthUsers: httpAuthUsers + + # Default features + # ---------------- + # + # You can select the features that are enabled by default for new + # new users. + plans: plans = [{ + planCode: "personal" + name: "Personal" + price: 0 + features: + collaborators: -1 + dropbox: true + versioning: true + }] + + # Spelling languages + # ------------------ + # + # You must have the corresponding aspell package installed to + # be able to use a language. + languages: [ + {name: "English", code: "en"} + ] + + # Third party services + # -------------------- + # + # ShareLaTeX's regular newsletter is managed by Markdown mail. Add your + # credentials here to integrate with this. + # markdownmail: + # secret: "" + # list_id: "" + # + # Fill in your unique token from various analytics services to enable + # them. + # analytics: + # mixpanel: + # token: "" + # ga: + # token: "" + # heap: + # token: "" + # + # ShareLaTeX's help desk is provided by tenderapp.com + # tenderUrl: "" + # + # ShareLaTeX uses Amazon's SES api to send transactional emails. + # Uncomment these lines and provide your credentials to be able to send emails. + # ses: + # "key":"" + # "secret":"" + + # Production Settings + # ------------------- + + # Should javascript assets be served minified or not. Note that you will + # need to run `grunt compile:minify` within the web-sharelatex directory + # to generate these. + useMinifiedJs: false + + # Should static assets be sent with a header to tell the browser to cache + # them. + cacheStaticAssets: false + + # If you are running ShareLaTeX over https, set this to true to send the + # cookie with a secure flag (recommended). + secureCookie: false + + # Internal configs + # ---------------- + path: + # If we ever need to write something to disk (e.g. incoming requests + # that need processing but may be too big for memory, then write + # them to disk here). + dumpFolder: Path.resolve "data/dumpFolder" + + # Automatic Snapshots + # ------------------- + automaticSnapshots: + # How long should we wait after the user last edited to + # take a snapshot? + waitTimeAfterLastEdit: 5 * minutes + # Even if edits are still taking place, this is maximum + # time to wait before taking another snapshot. + maxTimeBetweenSnapshots: 30 * minutes + + # Smoke test + # ---------- + # Provide log in credentials and a project to be able to run + # some basic smoke tests to check the core functionality. + # + # smokeTest: + # user: "" + # password: "" + # projectId: "" diff --git a/services/web/data/.gitignore b/services/web/data/.gitignore new file mode 100644 index 0000000000..0fa27a178d --- /dev/null +++ b/services/web/data/.gitignore @@ -0,0 +1,4 @@ +gnore everything in this directory +* +# Except this file +!.gitignore diff --git a/services/web/data/dumpFolder/.gitignore b/services/web/data/dumpFolder/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/logs/.gitignore b/services/web/data/logs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/pdfs/.gitignore b/services/web/data/pdfs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/uploads/.gitignore b/services/web/data/uploads/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/zippedProjects/.gitignore b/services/web/data/zippedProjects/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/package.json b/services/web/package.json new file mode 100644 index 0000000000..cae1d2c491 --- /dev/null +++ b/services/web/package.json @@ -0,0 +1,60 @@ +{ + "name": "web-sharelatex", + "version": "0.0.1", + "directories": { + "public": "./public" + }, + "dependencies": { + "heapdump": "0.1.0", + "express": "3.3.4", + "mongoose": "3.6.19", + "jade": "0.28.1", + "validator": "0.4.22", + "underscore": "1.4.4", + "node-fs": "0.1.5", + "rimraf": "2.1.2", + "connect-redis": "1.4.5", + "redis": "0.8.4", + "request": "2.14.0", + "xml2js": "0.2.0", + "dateformat": "1.0.4-1.2.3", + "optimist": "0.3.5", + "async": "0.2.9", + "lynx": "0.0.11", + "session.socket.io": "0.1.4", + "socket.io": "0.9.15", + "mimelib": "~0.2.8", + "bufferedstream": "~1.4.1", + "mixpanel": "0.0.18", + "settings-sharelatex": "git+ssh://git@bitbucket.org:sharelatex/settings-sharelatex.git#master", + "logger-sharelatex": "git+ssh://git@bitbucket.org:sharelatex/logger-sharelatex.git#bunyan", + "soa-req-id": "git+ssh://git@bitbucket.org:sharelatex/soa-req-id.git#master", + "fairy": "0.0.2", + "node-uuid": "~1.4.0", + "mongojs": "0.9.8", + "node-ses": "0.0.3", + "bcrypt": "0.7.5", + "archiver": "~0.5.1", + "ratelimiter": "~1.0.0", + "nodetime": "~0.8.15", + "mocha": "~1.17.1" + }, + "devDependencies": { + "chai": "", + "chai-spies": "", + "sandboxed-module": "0.2.0", + "timekeeper": "", + "sinon": "", + "grunt-concurrent": "~0.4.3", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-coffee": "~0.10.0", + "grunt-nodemon": "~0.2.0", + "grunt-contrib-less": "~0.9.0", + "grunt-mocha-test": "~0.9.0", + "grunt-available-tasks": "~0.4.1", + "grunt-contrib-requirejs": "~0.4.1", + "grunt-execute": "~0.1.5", + "bunyan": "~0.22.1", + "grunt-bunyan": "~0.5.0" + } +} diff --git a/services/web/public/app.build.js b/services/web/public/app.build.js new file mode 100644 index 0000000000..d1ff1c7a8d --- /dev/null +++ b/services/web/public/app.build.js @@ -0,0 +1,41 @@ +({ + appDir: "js", + baseUrl: "./", + dir: "minjs", + inlineText:false, + preserveLicenseComments:false, + + paths : { + "underscore": "libs/underscore", + "jquery": "libs/jquery" + }, + shim: { + "libs/backbone": { + deps: ["libs/underscore"] + }, + "libs/pdfListView/PdfListView": { + deps: ["libs/pdf"] + }, + "libs/pdf": { + deps: ["libs/compatibility"] + } + }, + + skipDirOptimize: true, + + modules: [ + { + name: "main", + exclude: ["jquery"] + }, { + name: "ide", + exclude: ["jquery"] + }, { + name: "home", + exclude: ["jquery"] + }, { + name: "list", + exclude: ["jquery"] + } + ] +}) diff --git a/services/web/public/backbone.js b/services/web/public/backbone.js new file mode 100644 index 0000000000..e956adf0e8 --- /dev/null +++ b/services/web/public/backbone.js @@ -0,0 +1,1429 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(root, factory) { + // Set up Backbone appropriately for the environment. + if (typeof exports !== 'undefined') { + // Node/CommonJS, no need for jQuery in that case. + factory(root, exports, require('underscore')); + } else if (typeof define === 'function' && define.amd) { + // AMD + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + } else { + // Browser globals + root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender)); + } +}(this, function(root, Backbone, _, $) { + + // Initial Setup + // ------------- + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to slice/splice. + var slice = Array.prototype.slice; + var splice = Array.prototype.splice; + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '0.9.2'; + + // Set the JavaScript library that will be used for DOM manipulation and + // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, + // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an + // alternate JavaScript library (or a mock library for testing your views + // outside of a browser). + Backbone.setDomLibrary = function(lib) { + $ = lib; + }; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return Backbone; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // ----------------- + + // Regular expression used to split event strings + var eventSplitter = /\s+/; + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback functions + // to an event; trigger`-ing an event fires all callbacks in succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind one or more space separated events, `events`, to a `callback` + // function. Passing `"all"` will bind the callback to all events fired. + on: function(events, callback, context) { + + var calls, event, node, tail, list; + if (!callback) return this; + events = events.split(eventSplitter); + calls = this._callbacks || (this._callbacks = {}); + + // Create an immutable callback list, allowing traversal during + // modification. The tail is an empty object that will always be used + // as the next node. + while (event = events.shift()) { + list = calls[event]; + node = list ? list.tail : {}; + node.next = tail = {}; + node.context = context; + node.callback = callback; + calls[event] = {tail: tail, next: list ? list.next : node}; + } + + return this; + }, + + // Remove one or many callbacks. If `context` is null, removes all callbacks + // with that function. If `callback` is null, removes all callbacks for the + // event. If `events` is null, removes all bound callbacks for all events. + off: function(events, callback, context) { + var event, calls, node, tail, cb, ctx; + + // No events, or removing *all* events. + if (!(calls = this._callbacks)) return; + if (!(events || callback || context)) { + delete this._callbacks; + return this; + } + + // Loop through the listed events and contexts, splicing them out of the + // linked list of callbacks if appropriate. + events = events ? events.split(eventSplitter) : _.keys(calls); + while (event = events.shift()) { + node = calls[event]; + delete calls[event]; + if (!node || !(callback || context)) continue; + // Create a new list, omitting the indicated callbacks. + tail = node.tail; + while ((node = node.next) !== tail) { + cb = node.callback; + ctx = node.context; + if ((callback && cb !== callback) || (context && ctx !== context)) { + this.on(event, cb, ctx); + } + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) return this; + all = calls.all; + events = events.split(eventSplitter); + rest = slice.call(arguments, 1); + + // For each event, walk through the linked list of callbacks twice, + // first to trigger the event, then to trigger any `"all"` callbacks. + while (event = events.shift()) { + if (node = calls[event]) { + tail = node.tail; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, rest); + } + } + if (node = all) { + tail = node.tail; + args = [event].concat(rest); + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + } + + return this; + } + + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + attributes || (attributes = {}); + if (options && options.parse) attributes = this.parse(attributes); + if (defaults = getValue(this, 'defaults')) { + attributes = _.extend({}, defaults, attributes); + } + if (options && options.collection) this.collection = options.collection; + this.attributes = {}; + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + this.changed = {}; + this._silent = {}; + this._pending = {}; + this.set(attributes, {silent: true}); + // Reset change tracking. + this.changed = {}; + this._silent = {}; + this._pending = {}; + this._previousAttributes = _.clone(this.attributes); + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // A hash of attributes that have silently changed since the last time + // `change` was called. Will become pending attributes on the next call. + _silent: null, + + // A hash of attributes that have changed since the last `'change'` event + // began. + _pending: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.get(attr); + return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"` unless + // you choose to silence it. + set: function(key, value, options) { + var attrs, attr, val; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + + // Extract attributes and options. + options || (options = {}); + if (!attrs) return this; + if (attrs instanceof Model) attrs = attrs.attributes; + if (options.unset) for (attr in attrs) attrs[attr] = void 0; + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + var changes = options.changes = {}; + var now = this.attributes; + var escaped = this._escapedAttributes; + var prev = this._previousAttributes || {}; + + // For each `set` attribute... + for (attr in attrs) { + val = attrs[attr]; + + // If the new and current value differ, record the change. + if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { + delete escaped[attr]; + (options.silent ? this._silent : changes)[attr] = true; + } + + // Update or delete the current value. + options.unset ? delete now[attr] : now[attr] = val; + + // If the new and previous value differ, record the change. If not, + // then remove changes for this attribute. + if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { + this.changed[attr] = val; + if (!options.silent) this._pending[attr] = true; + } else { + delete this.changed[attr]; + delete this._pending[attr]; + } + } + + // Fire the `"change"` events. + if (!options.silent) this.change(options); + return this; + }, + + // Remove an attribute from the model, firing `"change"` unless you choose + // to silence it. `unset` is a noop if the attribute doesn't exist. + unset: function(attr, options) { + (options || (options = {})).unset = true; + return this.set(attr, null, options); + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + (options || (options = {})).unset = true; + return this.set(_.clone(this.attributes), options); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); + }; + options.error = Backbone.wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, value, options) { + var attrs, current; + + // Handle both `("key", value)` and `({key: value})` -style calls. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + options = options ? _.clone(options) : {}; + + // If we're "wait"-ing to set changed attributes, validate early. + if (options.wait) { + if (!this._validate(attrs, options)) return false; + current = _.clone(this.attributes); + } + + // Regular saves `set` attributes before persisting to the server. + var silentOptions = _.extend({}, options, {silent: true}); + if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + var serverAttrs = model.parse(resp, xhr); + if (options.wait) { + delete options.wait; + serverAttrs = _.extend(attrs || {}, serverAttrs); + } + if (!model.set(serverAttrs, options)) return false; + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + // Finish configuring and sending the Ajax request. + options.error = Backbone.wrapError(options.error, model, options); + var method = this.isNew() ? 'create' : 'update'; + var xhr = (this.sync || Backbone.sync).call(this, method, this, options); + if (options.wait) this.set(current, silentOptions); + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var triggerDestroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + if (this.isNew()) { + triggerDestroy(); + return false; + } + + options.success = function(resp) { + if (options.wait) triggerDestroy(); + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + options.error = Backbone.wrapError(options.error, model, options); + var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); + if (!options.wait) triggerDestroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, xhr) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Call this method to manually fire a `"change"` event for this model and + // a `"change:attribute"` event for each changed attribute. + // Calling this will cause all objects observing the model to update. + change: function(options) { + options || (options = {}); + var changing = this._changing; + this._changing = true; + + // Silent changes become pending changes. + for (var attr in this._silent) this._pending[attr] = true; + + // Silent changes are triggered. + var changes = _.extend({}, options.changes, this._silent); + this._silent = {}; + for (var attr in changes) { + this.trigger('change:' + attr, this, this.get(attr), options); + } + if (changing) return this; + + // Continue firing `"change"` events while there are pending changes. + while (!_.isEmpty(this._pending)) { + this._pending = {}; + this.trigger('change', this, options); + // Pending and silent changes still remain. + for (var attr in this.changed) { + if (this._pending[attr] || this._silent[attr]) continue; + delete this.changed[attr]; + } + this._previousAttributes = _.clone(this.attributes); + } + + this._changing = false; + return this; + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (!arguments.length) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false, old = this._previousAttributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (!arguments.length || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Check if the model is currently in a valid state. It's only possible to + // get into an *invalid* state if you're using silent changes. + isValid: function() { + return !this.validate(this.attributes); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. If a specific `error` callback has + // been passed, call that instead of firing the general `"error"` event. + _validate: function(attrs, options) { + if (options.silent || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validate(attrs, options); + if (!error) return true; + if (options && options.error) { + options.error(this, error, options); + } else { + this.trigger('error', this, error, options); + } + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // Provides a standard collection class for our sets of models, ordered + // or unordered. If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, {silent: true, parse: options.parse}); + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Add a model, or list of models to the set. Pass **silent** to avoid + // firing the `add` event for every new model. + add: function(models, options) { + var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + + // Begin by turning bare objects into model references, and preventing + // invalid models or duplicate models from being added. + for (i = 0, length = models.length; i < length; i++) { + if (!(model = models[i] = this._prepareModel(models[i], options))) { + throw new Error("Can't add an invalid model to a collection"); + } + cid = model.cid; + id = model.id; + if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) { + dups.push(i); + continue; + } + cids[cid] = ids[id] = model; + } + + // Remove duplicates. + i = dups.length; + while (i--) { + models.splice(dups[i], 1); + } + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + for (i = 0, length = models.length; i < length; i++) { + (model = models[i]).on('all', this._onModelEvent, this); + this._byCid[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + + // Insert models into the collection, re-sorting if needed, and triggering + // `add` events unless silenced. + this.length += length; + index = options.at != null ? options.at : this.models.length; + splice.apply(this.models, [index, 0].concat(models)); + if (this.comparator) this.sort({silent: true}); + if (options.silent) return this; + for (i = 0, length = this.models.length; i < length; i++) { + if (!cids[(model = this.models[i]).cid]) continue; + options.index = i; + model.trigger('add', model, this, options); + } + return this; + }, + + // Remove a model, or a list of models from the set. Pass silent to avoid + // firing the `remove` event for every model removed. + remove: function(models, options) { + var i, l, index, model; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + for (i = 0, l = models.length; i < l; i++) { + model = this.getByCid(models[i]) || this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byCid[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, options); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Get a model from the set by id. + get: function(id) { + if (id == null) return void 0; + return this._byId[id.id != null ? id.id : id]; + }, + + // Get a model from the set by client id. + getByCid: function(cid) { + return cid && this._byCid[cid.cid || cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of `filter`. + where: function(attrs) { + if (_.isEmpty(attrs)) return []; + return this.filter(function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + var boundComparator = _.bind(this.comparator, this); + if (this.comparator.length == 1) { + this.models = this.sortBy(boundComparator); + } else { + this.models.sort(boundComparator); + } + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any `add` or `remove` events. Fires `reset` when finished. + reset: function(models, options) { + models || (models = []); + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `add: true` is passed, appends the + // models to the collection instead of resetting. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === undefined) options.parse = true; + var collection = this; + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); + }; + options.error = Backbone.wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + var coll = this; + options = options ? _.clone(options) : {}; + model = this._prepareModel(model, options); + if (!model) return false; + if (!options.wait) coll.add(model, options); + var success = options.success; + options.success = function(nextModel, resp, xhr) { + if (options.wait) coll.add(nextModel, options); + if (success) { + success(nextModel, resp); + } else { + nextModel.trigger('sync', model, resp, options); + } + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, xhr) { + return resp; + }, + + // Proxy to _'s chain. Can't be proxied the same way the rest of the + // underscore methods are proxied because it relies on the underscore + // constructor. + chain: function () { + return _(this.models).chain(); + }, + + // Reset all internal state. Called when the collection is reset. + _reset: function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; + }, + + // Prepare a model or hash of attributes to be added to this collection. + _prepareModel: function(model, options) { + options || (options = {}); + if (!(model instanceof Model)) { + var attrs = model; + options.collection = this; + model = new this.model(attrs, options); + if (!model._validate(model.attributes, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + + // Internal method to remove a model's ties to a collection. + _removeReference: function(model) { + if (this == model.collection) { + delete model.collection; + } + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event == 'add' || event == 'remove') && collection != this) return; + if (event == 'destroy') { + this.remove(model, options); + } + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', + 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', + 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', + 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', + 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + return _[method].apply(_, [this.models].concat(_.toArray(arguments))); + }; + }); + + // Backbone.Router + // ------------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var namedParam = /:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + Backbone.history || (Backbone.history = new History); + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (!callback) callback = this[name]; + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + callback && callback.apply(this, args); + this.trigger.apply(this, ['route:' + name].concat(args)); + Backbone.history.trigger('route', this, name, args); + }, this)); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + var routes = []; + for (var route in this.routes) { + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]+)') + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters: function(route, fragment) { + return route.exec(fragment).slice(1); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on URL fragments. If the + // browser does not support `onhashchange`, falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning leading hashes and slashes . + var routeStripper = /^[#\/]/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(windowOverride) { + var loc = windowOverride ? windowOverride.location : window.location; + var match = loc.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + } else { + fragment = this.getHash(); + } + } + if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + if (oldIE) { + this.iframe = $(' + +
  • + +
  • +
  • ·
  • + +
  • + +
  • + + + +
    + +
    +

    Designed for everyone, everywhere.

    + +
    +
    + +

    Built for and by nerds

    +

    Like you, we love building awesome products on the web. We love it so much, we decided to help people just like us do it easier, better, and faster. Bootstrap is built for you.

    +
    +
    + +

    For all skill levels

    +

    Bootstrap is designed to help people of all skill levels—designer or developer, huge nerd or early beginner. Use it as a complete kit or use to start something more complex.

    +
    +
    + +

    Cross-everything

    +

    Originally built with only modern browsers in mind, Bootstrap has evolved to include support for all major browsers (even IE7!) and, with Bootstrap 2, tablets and smartphones, too.

    +
    +
    +
    +
    + +

    12-column grid

    +

    Grid systems aren't everything, but having a durable and flexible one at the core of your work can make development much simpler. Use our built-in grid classes or roll your own.

    +
    +
    + +

    Responsive design

    +

    With Bootstrap 2, we've gone fully responsive. Our components are scaled according to a range of resolutions and devices to provide a consistent experience, no matter what.

    +
    +
    + +

    Styleguide docs

    +

    Unlike other front-end toolkits, Bootstrap was designed first and foremost as a styleguide to document not only our features, but best practices and living, coded examples.

    +
    +
    +
    +
    + +

    Growing library

    +

    Despite being only 10kb (gzipped), Bootstrap is one of the most complete front-end toolkits out there with dozens of fully functional components ready to be put to use.

    +
    +
    + +

    Custom jQuery plugins

    +

    What good is an awesome design component without easy-to-use, proper, and extensible interactions? With Bootstrap, you get custom-built jQuery plugins to bring your projects to life.

    +
    +
    + +

    Built on LESS

    +

    Where vanilla CSS falters, LESS excels. Variables, nesting, operations, and mixins in LESS makes coding CSS faster and more efficient with minimal overhead.

    +
    +
    +
    +
    + +

    HTML5

    +

    Built to support new HTML5 elements and syntax.

    +
    +
    + +

    CSS3

    +

    Progressively enhanced components for ultimate style.

    +
    +
    + +

    Open-source

    +

    Built for and maintained by the community via GitHub.

    +
    +
    + +

    Made at Twitter

    +

    Brought to you by an experienced engineer and designer.

    +
    +
    + +
    + +

    Built with Bootstrap.

    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/bootstrap/docs/javascript.html b/services/web/public/bootstrap/docs/javascript.html new file mode 100644 index 0000000000..1d0afb9e0d --- /dev/null +++ b/services/web/public/bootstrap/docs/javascript.html @@ -0,0 +1,1476 @@ + + + + + Bootstrap, from Twitter + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +

    Javascript for Bootstrap

    +

    Bring Bootstrap's components to life—now with 12 custom jQuery plugins. +

    +
    + + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    Heads up! All javascript plugins require the latest version of jQuery.
    +
    + + + + +
    + +
    +
    +

    About modals

    +

    A streamlined, but flexible, take on the traditional javascript modal plugin with only the minimum required functionality and smart defaults.

    + Download file +
    +
    +

    Static example

    +

    Below is a statically rendered modal.

    + + +

    Live demo

    +

    Toggle a modal via javascript by clicking the button below. It will slide down and fade in from the top of the page.

    + + + Launch demo modal + +
    + +

    Using bootstrap-modal

    +

    Call the modal via javascript:

    +
    $('#myModal').modal(options)
    +

    Options

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Nametypedefaultdescription
    backdropbooleantrueIncludes a modal-backdrop element
    keyboardbooleantrueCloses the modal when escape key is pressed
    showbooleantrueShows the modal when initialized.
    +

    Markup

    +

    You can activate modals on your page easily without having to write a single line of javascript. Just set data-toggle="modal" on a controller element with a data-target="#foo" or href="#foo" which corresponds to a modal element id, and when clicked, it will launch your modal.

    +

    Also, to add options to your modal instance, just include them as additional data attributes on either the control element or the modal markup itself.

    +
    +<a class="btn" data-toggle="modal" href="#myModal" >Launch Modal</a>
    +
    + +
    +<div class="modal">
    +  <div class="modal-header">
    +    <a class="close" data-dismiss="modal">×</a>
    +    <h3>Modal header</h3>
    +  </div>
    +  <div class="modal-body">
    +    <p>One fine body…</p>
    +  </div>
    +  <div class="modal-footer">
    +    <a href="#" class="btn btn-primary">Save changes</a>
    +    <a href="#" class="btn">Close</a>
    +  </div>
    +</div>
    +
    +
    + Heads up! If you want your modal to animate in and out, just add a .fade class to the .modal element (refer to the demo to see this in action) and include bootstrap-transition.js. +
    +

    Methods

    +

    .modal(options)

    +

    Activates your content as a modal. Accepts an optional options object.

    +
    +$('#myModal').modal({
    +  keyboard: false
    +})
    +

    .modal('toggle')

    +

    Manually toggles a modal.

    +
    $('#myModal').modal('toggle')
    +

    .modal('show')

    +

    Manually opens a modal.

    +
    $('#myModal').modal('show')
    +

    .modal('hide')

    +

    Manually hides a modal.

    +
    $('#myModal').modal('hide')
    +

    Events

    +

    Bootstrap's modal class exposes a few events for hooking into modal functionality.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    EventDescription
    showThis event fires immediately when the show instance method is called.
    shownThis event is fired when the modal has been made visible to the user (will wait for css transitions to complete).
    hideThis event is fired immediately when the hide instance method has been called.
    hiddenThis event is fired when the modal has finished being hidden from the user (will wait for css transitions to complete).
    + +
    +$('#myModal').on('hidden', function () {
    +  // do something…
    +})
    +
    +
    +
    + + + + + + + + + +
    + +
    +
    +

    The ScrollSpy plugin is for automatically updating nav targets based on scroll position.

    + Download file +
    +
    +

    Example navbar with scrollspy

    +

    Scroll the area below and watch the navigation update. The dropdown sub items will be highlighted as well. Try it!

    + +
    +

    @fat

    +

    + Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat. +

    +

    @mdo

    +

    + Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard. Freegan beard aliqua cupidatat mcsweeney's vero. Cupidatat four loko nisi, ea helvetica nulla carles. Tattooed cosby sweater food truck, mcsweeney's quis non freegan vinyl. Lo-fi wes anderson +1 sartorial. Carles non aesthetic exercitation quis gentrify. Brooklyn adipisicing craft beer vice keytar deserunt. +

    +

    one

    +

    + Occaecat commodo aliqua delectus. Fap craft beer deserunt skateboard ea. Lomo bicycle rights adipisicing banh mi, velit ea sunt next level locavore single-origin coffee in magna veniam. High life id vinyl, echo park consequat quis aliquip banh mi pitchfork. Vero VHS est adipisicing. Consectetur nisi DIY minim messenger bag. Cred ex in, sustainable delectus consectetur fanny pack iphone. +

    +

    two

    +

    + In incididunt echo park, officia deserunt mcsweeney's proident master cleanse thundercats sapiente veniam. Excepteur VHS elit, proident shoreditch +1 biodiesel laborum craft beer. Single-origin coffee wayfarers irure four loko, cupidatat terry richardson master cleanse. Assumenda you probably haven't heard of them art party fanny pack, tattooed nulla cardigan tempor ad. Proident wolf nesciunt sartorial keffiyeh eu banh mi sustainable. Elit wolf voluptate, lo-fi ea portland before they sold out four loko. Locavore enim nostrud mlkshk brooklyn nesciunt. +

    +

    three

    +

    + Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat. +

    +

    Keytar twee blog, culpa messenger bag marfa whatever delectus food truck. Sapiente synth id assumenda. Locavore sed helvetica cliche irony, thundercats you probably haven't heard of them consequat hoodie gluten-free lo-fi fap aliquip. Labore elit placeat before they sold out, terry richardson proident brunch nesciunt quis cosby sweater pariatur keffiyeh ut helvetica artisan. Cardigan craft beer seitan readymade velit. VHS chambray laboris tempor veniam. Anim mollit minim commodo ullamco thundercats. +

    +
    +
    +

    Using bootstrap-scrollspy.js

    +

    Call the scrollspy via javascript:

    +
    $('#navbar').scrollspy()
    +

    Markup

    +

    To easily add scrollspy behavior to your topbar navigation, just add data-spy="scroll" to the element you want to spy on (most typically this would be the body).

    +
    <body data-spy="scroll" >...</body>
    +
    + Heads up! + Navbar links must have resolvable id targets. For example, a <a href="#home">home</a> must correspond to something in the dom like <div id="home"></div>. +
    +

    Options

    + + + + + + + + + + + + + + + + + +
    Nametypedefaultdescription
    offsetnumber10Pixels to offset from top when calculating position of scroll.
    +
    +
    +
    + + + + +
    + +
    +
    +

    This plugin adds quick, dynamic tab and pill functionality for transitioning through local content.

    + Download file +
    +
    +

    Example tabs

    +

    Click the tabs below to toggle between hidden panes, even via dropdown menus.

    + +
    +
    +

    Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

    +
    +
    +

    Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

    +
    + + +
    +
    +

    Using bootstrap-tab.js

    +

    Enable tabbable tabs via javascript:

    +
    $('#myTab').tab('show')
    +

    Markup

    +

    You can activate a tab or pill navigation without writing any javascript by simply specifying data-toggle="tab" or data-toggle="pill" on an element.

    +
    +<ul class="nav nav-tabs">
    +  <li><a href="#home" data-toggle="tab">Home</a></li>
    +  <li><a href="#profile" data-toggle="tab">Profile</a></li>
    +  <li><a href="#messages" data-toggle="tab">Messages</a></li>
    +  <li><a href="#settings" data-toggle="tab">Settings</a></li>
    +</ul>
    +

    Methods

    +

    $().tab

    +

    + Activates a tab element and content container. Tab should have either a `data-target` or an `href` targeting a container node in the dom. +

    +
    +<ul class="nav nav-tabs">
    +  <li class="active"><a href="#home">Home</a></li>
    +  <li><a href="#profile">Profile</a></li>
    +  <li><a href="#messages">Messages</a></li>
    +  <li><a href="#settings">Settings</a></li>
    +</ul>
    +
    +<div class="tab-content">
    +  <div class="tab-pane active" id="home">...</div>
    +  <div class="tab-pane" id="profile">...</div>
    +  <div class="tab-pane" id="messages">...</div>
    +  <div class="tab-pane" id="settings">...</div>
    +</div>
    +
    +<script>
    +  $(function () {
    +    $('.tabs a:last').tab('show')
    +  })
    +</script>
    +

    Events

    + + + + + + + + + + + + + + + + + +
    EventDescription
    showThis event fires on tab show, but before the new tab has been shown. Use event.target and event.relatedTarget to target the active tab and the previous active tab (if available) respectively.
    shownThis event fires on tab show after a tab has been shown. Use event.target and event.relatedTarget to target the active tab and the previous active tab (if available) respectively.
    + +
    +$('a[data-toggle="tab"]').on('shown', function (e) {
    +  e.target // activated tab
    +  e.relatedTarget // previous tab
    +})
    +
    +
    +
    + + + +
    + +
    +
    +

    About Tooltips

    +

    Inspired by the excellent jQuery.tipsy plugin written by Jason Frame; Tooltips are an updated version, which don't rely on images, use css3 for animations, and data-attributes for local title storage.

    + Download file +
    +
    +

    Example use of Tooltips

    +

    Hover over the links below to see tooltips:

    +
    +

    Tight pants next level keffiyeh you probably haven't heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's fixie sustainable quinoa 8-bit american apparel have a terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar, scenester farm-to-table banksy Austin twitter handle freegan cred raw denim single-origin coffee viral. +

    +
    +
    +

    Using bootstrap-tooltip.js

    +

    Trigger the tooltip via javascript:

    +
    $('#example').tooltip(options)
    +

    Options

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Nametypedefaultdescription
    animationbooleantrueapply a css fade transition to the tooltip
    placementstring'top'how to position the tooltip - top | bottom | left | right
    selectorstringfalseIf a selector is provided, tooltip objects will be delegated to the specified targets.
    titlestring | function''default title value if `title` tag isn't present
    triggerstring'hover'how tooltip is triggered - hover | focus | manual
    delaynumber | object0 +

    delay showing and hiding the tooltip (ms)

    +

    If a number is supplied, delay is applied to both hide/show

    +

    Object structure is: delay: { show: 500, hide: 100 }

    +
    +
    + Heads up! + Options for individual tooltips can alternatively be specified through the use of data attributes. +
    +

    Markup

    +

    For performance reasons, the Tooltip and Popover data-apis are opt in. If you would like to use them just specify a selector option.

    +
    +<a href="#" rel="tooltip" title="first tooltip">hover over me</a>
    +
    +

    Methods

    +

    $().tooltip(options)

    +

    Attaches a tooltip handler to an element collection.

    +

    .tooltip('show')

    +

    Reveals an elements tooltip.

    +
    $('#element').tooltip('show')
    +

    .tooltip('hide')

    +

    Hides an elements tooltip.

    +
    $('#element').tooltip('hide')
    +

    .tooltip('toggle')

    +

    Toggles an elements tooltip.

    +
    $('#element').tooltip('toggle')
    +
    +
    +
    + + + + +
    + +
    +
    +

    About popovers

    +

    Add small overlays of content, like those on the iPad, to any element for housing secondary information.

    +

    * Requires Tooltip to be included

    + Download file +
    +
    +

    Example hover popover

    +

    Hover over the button to trigger the popover.

    + +
    +

    Using bootstrap-popover.js

    +

    Enable popovers via javascript:

    +
    $('#example').popover(options)
    +

    Options

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Nametypedefaultdescription
    animationbooleantrueapply a css fade transition to the tooltip
    placementstring'right'how to position the popover - top | bottom | left | right
    selectorstringfalseif a selector is provided, tooltip objects will be delegated to the specified targets
    triggerstring'hover'how tooltip is triggered - hover | focus | manual
    titlestring | function''default title value if `title` attribute isn't present
    contentstring | function''default content value if `data-content` attribute isn't present
    delaynumber | object0 +

    delay showing and hiding the popover (ms)

    +

    If a number is supplied, delay is applied to both hide/show

    +

    Object structure is: delay: { show: 500, hide: 100 }

    +
    +
    + Heads up! + Options for individual popovers can alternatively be specified through the use of data attributes. +
    +

    Markup

    +

    + For performance reasons, the Tooltip and Popover data-apis are opt in. If you would like to use them just specify a the selector option. +

    +

    Methods

    +

    $().popover(options)

    +

    Initializes popovers for an element collection.

    +

    .popover('show')

    +

    Reveals an elements popover.

    +
    $('#element').popover('show')
    +

    .popover('hide')

    +

    Hides an elements popover.

    +
    $('#element').popover('hide')
    +

    .popover('toggle')

    +

    Toggles an elements popover.

    +
    $('#element').popover('toggle')
    +
    +
    +
    + + + + +
    + +
    +
    +

    About alerts

    +

    The alert plugin is a tiny class for adding close functionality to alerts.

    + Download +
    +
    +

    Example alerts

    +

    The alerts plugin works on regular alert messages, and block messages.

    +
    + × + Holy guacamole! Best check yo self, you're not looking too good. +
    +
    + × +

    Oh snap! You got an error!

    +

    Change this and that and try again. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cras mattis consectetur purus sit amet fermentum.

    +

    + Take this action Or do this +

    +
    +
    +

    Using bootstrap-alerts.js

    +

    Enable dismissal of an alert via javascript:

    +
    $(".alert").alert()
    +

    Markup

    +

    Just add data-dismiss="alert" to your close button to automatically give an alert close functionality.

    +
    <a class="close" data-dismiss="alert" href="#">&times;</a>
    +

    Methods

    +

    $().alert()

    +

    Wraps all alerts with close functionality. To have your alerts animate out when closed, make sure they have the .fade and .in class already applied to them.

    +

    .alert('close')

    +

    Closes an alert.

    +
    $(".alert").alert('close')
    +

    Events

    +

    Bootstrap's alert class exposes a few events for hooking into alert functionality.

    + + + + + + + + + + + + + + + + + +
    EventDescription
    closeThis event fires immediately when the close instance method is called.
    closedThis event is fired when the alert has been closed (will wait for css transitions to complete).
    +
    +$('#my-alert').bind('closed', function () {
    +  // do something…
    +})
    +
    +
    +
    + + + + +
    + +
    +
    +

    About

    +

    Do more with buttons. Control button states or create groups of buttons for more components like toolbars.

    + Download file +
    +
    +

    Example uses

    +

    Use the buttons plugin for states and toggles.

    + + + + + + + + + + + + + + + + + + + +
    Stateful + +
    Single toggle + +
    Checkbox +
    + + + +
    +
    Radio +
    + + + +
    +
    +
    +

    Using bootstrap-button.js

    +

    Enable buttons via javascript:

    +
    $('.tabs').button()
    +

    Markup

    +

    Data attributes are integral to the button plugin. Check out the example code below for the various markup types.

    +
    +<!-- Add data-toggle="button" to activate toggling on a single button -->
    +<button class="btn" data-toggle="button">Single Toggle</button>
    +
    +<!-- Add data-toggle="buttons-checkbox" for checkbox style toggling on btn-group -->
    +<div class="btn-group" data-toggle="buttons-checkbox">
    +  <button class="btn">Left</button>
    +  <button class="btn">Middle</button>
    +  <button class="btn">Right</button>
    +</div>
    +
    +<!-- Add data-toggle="buttons-radio" for radio style toggling on btn-group -->
    +<div class="btn-group" data-toggle="buttons-radio">
    +  <button class="btn">Left</button>
    +  <button class="btn">Middle</button>
    +  <button class="btn">Right</button>
    +</div>
    +
    +

    Methods

    +

    $().button('toggle')

    +

    Toggles push state. Gives btn the look that it hass been activated.

    +
    + Heads up! + You can enable auto toggling of a button by using the data-toggle attribute. +
    +
    <button class="btn" data-toggle="button" >…</button>
    +

    $().button('loading')

    +

    Sets button state to loading - disables button and swaps text to loading text. Loading text should be defined on the button element using the data attribute data-loading-text. +

    +
    <button class="btn" data-loading-text="loading stuff..." >...</button>
    +
    + Heads up! + Firefox persists the disabled state across page loads. A workaround for this is to use autocomplete="off". +
    +

    $().button('reset')

    +

    Resets button state - swaps text to original text.

    +

    $().button(string)

    +

    Resets button state - swaps text to any data defined text state.

    +
    <button class="btn" data-complete-text="finished!" >...</button>
    +<script>
    +  $('.btn').button('complete')
    +</script>
    +
    +
    +
    + + + + +
    + +
    +
    +

    About

    +

    Get base styles and flexible support for collapsible components like accordions and navigation.

    + Download file +
    +
    +

    Example accordion

    +

    Using the collapse plugin, we built a simple accordion style widget:

    + +
    +
    + +
    +
    + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. +
    +
    +
    +
    + +
    +
    + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. +
    +
    +
    +
    + +
    +
    + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. +
    +
    +
    +
    + + +
    +

    Using bootstrap-collapse.js

    +

    Enable via javascript:

    +
    $(".collapse").collapse()
    +

    Options

    + + + + + + + + + + + + + + + + + + + + + + + +
    Nametypedefaultdescription
    parentselectorfalseIf selector then all collapsible elements under the specified parent will be closed when this collapsible item is shown. (similar to traditional accordion behavior)
    togglebooleantrueToggles the collapsible element on invocation
    +

    Markup

    +

    Just add data-toggle="collapse" and a data-target to element to automatically assign control of a collapsible element. The data-target attribute accepts a css selector to apply the collapse to. Be sure to add the class collapse to the collapsible element. If you'd like it to default open, add the additional class in.

    +
    +<button class="btn btn-danger" data-toggle="collapse" data-target="#demo">
    +  simple collapsible
    +</button>
    +
    +<div id="demo" class="collapse in"> … </div>
    +
    + Heads up! + To add accordion-like group management to a collapsible control, add the data attribute data-parent="#selector". Refer to the demo to see this in action. +
    +

    Methods

    +

    .collapse(options)

    +

    Activates your content as a collapsible element. Accepts an optional options object. +

    +$('#myCollapsible').collapse({
    +  toggle: false
    +})
    +

    .collapse('toggle')

    +

    Toggles a collapsible element to shown or hidden.

    +

    .collapse('show')

    +

    Shows a collapsible element.

    +

    .collapse('hide')

    +

    Hides a collapsible element.

    +

    Events

    +

    + Bootstrap's collapse class exposes a few events for hooking into collapse functionality. +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    EventDescription
    showThis event fires immediately when the show instance method is called.
    shownThis event is fired when a collapse element has been made visible to the user (will wait for css transitions to complete).
    hide + This event is fired immediately when the hide method has been called. +
    hiddenThis event is fired when a collapse element has been hidden from the user (will wait for css transitions to complete).
    + +
    +$('#myCollapsible').on('hidden', function () {
    +  // do something…
    +})
    +
    +
    +
    + + + + + + + + + +
    + +
    +
    +

    About

    +

    A basic, easily extended plugin for quickly creating elegant typeaheads with any form text input.

    + Download file +
    +
    +

    Example

    +

    Start typing in the field below to show the typeahead results.

    +
    + +
    +
    +

    Using bootstrap-typeahead.js

    +

    Call the typeahead via javascript:

    +
    $('.typeahead').typeahead()
    +

    Options

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Nametypedefaultdescription
    sourcearray[ ]The data source to query against.
    itemsnumber8The max number of items to display in the dropdown.
    matcherfunctioncase insensitiveThe method used to determine if a query matches an item. Accepts a single argument, the item against which to test the query. Access the current query with this.query. Return a boolean true if query is a match.
    sorterfunctionexact match,
    case sensitive,
    case insensitive
    Method used to sort autocomplete results. Accepts a single argument items and has the scope of the typeahead instance. Reference the current query with this.query.
    highlighterfunctionhighlights all default matchesMethod used to highlight autocomplete results. Accepts a single argument item and has the scope of the typeahead instance. Should return html.
    + +

    Markup

    +

    Add data attributes to register an element with typeahead functionality.

    +
    +<input type="text" data-provide="typeahead">
    +
    +

    Methods

    +

    .typeahead(options)

    +

    Initializes an input with a typeahead.

    +
    +
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/bootstrap/docs/less.html b/services/web/public/bootstrap/docs/less.html new file mode 100644 index 0000000000..0d1527fb35 --- /dev/null +++ b/services/web/public/bootstrap/docs/less.html @@ -0,0 +1,795 @@ + + + + + Bootstrap, from Twitter + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +

    Using LESS with Bootstrap

    +

    Customize and extend Bootstrap with LESS, a CSS preprocessor, to take advantage of the variables, mixins, and more used to build Bootstrap's CSS.

    + +
    + + + + +
    + +
    +
    +

    Why LESS?

    +

    Bootstrap is made with LESS at its core, a dynamic stylesheet language created by our good friend, Alexis Sellier. It makes developing systems-based CSS faster, easier, and more fun.

    +
    +
    +

    What's included?

    +

    As an extension of CSS, LESS includes variables, mixins for reusable snippets of code, operations for simple math, nesting, and even color functions.

    +
    +
    +

    Learn more

    + LESS CSS +

    Visit the official website at http://lesscss.org to learn more.

    +
    +
    +
    +
    +

    Variables

    +

    Managing colors and pixel values in CSS can be a bit of a pain, usually full of copy and paste. Not with LESS though—assign colors or pixel values as variables and change them once.

    +
    +
    +

    Mixins

    +

    Those three border-radius declarations you need to make in regular ol' CSS? Now they're down to one line with the help of mixins, snippets of code you can reuse anywhere.

    +
    +
    +

    Operations

    +

    Make your grid, leading, and more super flexible by doing the math on the fly with operations. Multiply, divide, add, and subtract your way to CSS sanity.

    +
    +
    +
    + + + + +
    + + +
    +
    +

    Hyperlinks

    + + + + + + + + + + + + + + + +
    @linkColor#08cDefault link text color
    @linkColorHoverdarken(@linkColor, 15%)Default link text hover color
    +

    Grid system

    + + + + + + + + + + + + + + + + + + + + + + + +
    @gridColumns12
    @gridColumnWidth60px
    @gridGutterWidth20px
    @fluidGridColumnWidth6.382978723%
    @fluidGridGutterWidth2.127659574%
    +

    Typography

    + + + + + + + + + + + + + + + +
    @baseFontSize13px
    @baseFontFamily"Helvetica Neue", Helvetica, Arial, sans-serif
    @baseLineHeight18px
    +
    +
    +

    Grayscale colors

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @black#000
    @grayDarker#222
    @grayDark#333
    @gray#555
    @grayLight#999
    @grayLighter#eee
    @white#fff
    +

    Accent colors

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @blue#049cdb
    @green#46a546
    @red#9d261d
    @yellow#ffc40d
    @orange#f89406
    @pink#c3325f
    @purple#7a43b6
    +
    +
    + +

    Components

    +
    +
    +

    Buttons

    + + + + + + + + +
    @primaryButtonBackground@linkColor
    +

    Forms

    + + + + + + + + +
    @placeholderText@grayLight
    +

    Navbar

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @navbarHeight40px
    @navbarBackground@grayDarker
    @navbarBackgroundHighlight@grayDark
    @navbarText@grayLight
    @navbarLinkColor@grayLight
    @navbarLinkColorHover@white
    +
    +
    +

    Form states and alerts

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @warningText#c09853
    @warningBackground#f3edd2
    @errorText#b94a48
    @errorBackground#f2dede
    @successText#468847
    @successBackground#dff0d8
    @infoText#3a87ad
    @infoBackground#d9edf7
    +
    +
    + +
    + + + + +
    + +

    About mixins

    +
    +
    +

    Basic mixins

    +

    A basic mixin is essentially an include or a partial for a snippet of CSS. They're written just like a CSS class and can be called anywhere.

    +
    +.element {
    +  .clearfix();
    +}
    +
    +
    +
    +

    Parametric mixins

    +

    A parametric mixin is just like a basic mixin, but it also accepts parameters (hence the name) with optional default values.

    +
    +.element {
    +  .border-radius(4px);
    +}
    +
    +
    +
    +

    Easily add your own

    +

    Nearly all of Bootstrap's mixins are stored in mixins.less, a wonderful utility .less file that enables you to use a mixin in any of the .less files in the toolkit.

    +

    So, go ahead and use the existing ones or feel free to add your own as you need.

    +
    +
    +

    Included mixins

    +

    Utilities

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MixinParametersUsage
    .clearfix()noneAdd to any parent to clear floats within
    .tab-focus()noneApply the Webkit focus style and round Firefox outline
    .center-block()noneAuto center a block-level element using margin: auto
    .ie7-inline-block()noneUse in addition to regular display: inline-block to get IE7 support
    .size()@height: 5px, @width: 5pxQuickly set the height and width on one line
    .square()@size: 5pxBuilds on .size() to set the width and height as same value
    .opacity()@opacity: 100Set, in whole numbers, the opacity percentage (e.g., "50" or "75")
    +

    Forms

    + + + + + + + + + + + + + + + +
    MixinParametersUsage
    .placeholder()@color: @placeholderTextSet the placeholder text color for inputs
    +

    Typography

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MixinParametersUsage
    #font > #family > .serif()noneMake an element use a serif font stack
    #font > #family > .sans-serif()noneMake an element use a sans-serif font stack
    #font > #family > .monospace()noneMake an element use a monospace font stack
    #font > .shorthand()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeightEasily set font size, weight, and leading
    #font > .serif()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeightSet font family to serif, and control size, weight, and leading
    #font > .sans-serif()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeightSet font family to sans-serif, and control size, weight, and leading
    #font > .monospace()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeightSet font family to monospace, and control size, weight, and leading
    +

    Grid system

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MixinParametersUsage
    .container-fixed()noneProvide a fixed-width (set with @siteWidth) container for holding your content
    .columns()@columns: 1Build a grid column that spans any number of columns (defaults to 1 column)
    .offset()@columns: 1Offset a grid column with left margin that spans any number of columns
    .gridColumn()noneMake an element float like a grid column
    +

    CSS3 properties

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MixinParametersUsage
    .border-radius()@radius: 5pxRound the corners of an element. Can be a single value or four space-separated values
    .box-shadow()@shadow: 0 1px 3px rgba(0,0,0,.25)Add a drop shadow to an element
    .transition()@transitionAdd CSS3 transition effect (e.g., all .2s linear)
    .rotate()@degreesRotate an element n degrees
    .scale()@ratioScale an element to n times its original size
    .translate()@x: 0, @y: 0Move an element on the x and y planes
    .background-clip()@clipCrop the background of an element (useful for border-radius)
    .background-size()@sizeControl the size of background images via CSS3
    .box-sizing()@boxmodelChange the box model for an element (e.g., border-box for a full-width input)
    .user-select()@selectControl cursor selection of text on a page
    .resizable()@direction: bothMake any element resizable on the right and bottom
    .content-columns()@columnCount, @columnGap: @gridColumnGutterMake the content of any element use CSS3 columns
    +

    Backgrounds and gradients

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MixinParametersUsage
    .#translucent > .background()@color: @white, @alpha: 1Give an element a translucent background color
    .#translucent > .border()@color: @white, @alpha: 1Give an element a translucent border color
    .#gradient > .vertical()@startColor, @endColorCreate a cross-browser vertical background gradient
    .#gradient > .horizontal()@startColor, @endColorCreate a cross-browser horizontal background gradient
    .#gradient > .directional()@startColor, @endColor, @degCreate a cross-browser directional background gradient
    .#gradient > .vertical-three-colors()@startColor, @midColor, @colorStop, @endColorCreate a cross-browser three-color background gradient
    .#gradient > .radial()@innerColor, @outerColorCreate a cross-browser radial background gradient
    .#gradient > .striped()@color, @angleCreate a cross-browser striped background gradient
    .#gradientBar()@primaryColor, @secondaryColorUsed for buttons to assign a gradient and slightly darker border
    +
    + + + + +
    + +
    + Note: If you're submitting a pull request to GitHub with modified CSS, you must recompile the CSS via any of these methods. +
    +

    Tools for compiling

    +
    +
    +

    Node with makefile

    +

    Install the LESS command line compiler globally with npm by running the following command:

    +
    $ npm install -g less
    +

    Once installed just run make from the root of your bootstrap directory and you're all set.

    +

    Additionally, if you have watchr installed, you may run make watch to have bootstrap automatically rebuilt every time you edit a file in the bootstrap lib (this isn't required, just a convenience method).

    +
    +
    +

    Command line

    +

    Install the LESS command line tool via Node and run the following command:

    +
    $ lessc ./lib/bootstrap.less > bootstrap.css
    +

    Be sure to include --compress in that command if you're trying to save some bytes!

    +
    +
    +

    Javascript

    +

    Download the latest Less.js and include the path to it (and Bootstrap) in the <head>.

    +
    +<link rel="stylesheet/less" href="/path/to/bootstrap.less">
    +<script src="/path/to/less.js"></script>
    +
    +

    To recompile the .less files, just save them and reload your page. Less.js compiles them and stores them in local storage.

    +
    +
    +
    +
    +

    Unofficial Mac app

    +

    The unofficial Mac app watches directories of .less files and compiles the code to local files after every save of a watched .less file.

    +

    If you like, you can toggle preferences in the app for automatic minifying and which directory the compiled files end up in.

    +
    +
    +

    More Mac apps

    +

    Crunch

    +

    Crunch is a great looking LESS editor and compiler built on Adobe Air.

    +

    CodeKit

    +

    Created by the same guy as the unofficial Mac app, CodeKit is a Mac app that compiles LESS, SASS, Stylus, and CoffeeScript.

    +

    Simpless

    +

    Mac, Linux, and PC app for drag and drop compiling of LESS files. Plus, the source code is on GitHub.

    +
    +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/bootstrap/docs/scaffolding.html b/services/web/public/bootstrap/docs/scaffolding.html new file mode 100644 index 0000000000..6ed5857fff --- /dev/null +++ b/services/web/public/bootstrap/docs/scaffolding.html @@ -0,0 +1,442 @@ + + + + + Bootstrap, from Twitter + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +

    Scaffolding

    +

    Bootstrap is built on a responsive 12-column grid. We've also included fixed- and fluid-width layouts based on that system.

    + +
    + + + + +
    + + +

    Default grid

    +
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    +
    +
    4
    +
    4
    +
    4
    +
    +
    +
    4
    +
    8
    +
    +
    +
    6
    +
    6
    +
    +
    +
    12
    +
    +
    +
    +

    The default grid system provided as part of Bootstrap is a 940px-wide, 12-column grid.

    +

    It also has four responsive variations for various devices and resolutions: phone, tablet portrait, table landscape and small desktops, and large widescreen desktops.

    +
    +
    +
    +<div class="row">
    +  <div class="span4">...</div>
    +  <div class="span8">...</div>
    +</div>
    +
    +
    +
    +

    As shown here, a basic layout can be created with two "columns," each spanning a number of the 12 foundational columns we defined as part of our grid system.

    +
    +
    + +
    + +

    Offsetting columns

    +
    +
    4
    +
    4 offset 4
    +
    +
    +
    3 offset 3
    +
    3 offset 3
    +
    +
    +
    8 offset 4
    +
    +
    +<div class="row">
    +  <div class="span4">...</div>
    +  <div class="span4 offset4">...</div>
    +</div>
    +
    + +
    + +

    Nesting columns

    +
    +
    +

    With the static (non-fluid) grid system in Bootstrap, nesting is easy. To nest your content, just add a new .row and set of .span* columns within an existing .span* column.

    +

    Example

    +
    +
    + Level 1 of column +
    +
    + Level 2 +
    +
    + Level 2 +
    +
    +
    +
    +
    +
    +
    +<div class="row">
    +  <div class="span12">
    +    Level 1 of column
    +    <div class="row">
    +      <div class="span6">Level 2</div>
    +      <div class="span6">Level 2</div>
    +    </div>
    +  </div>
    +</div>
    +
    +
    +
    + +

    Grid customization

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    VariableDefault valueDescription
    @gridColumns12Number of columns
    @gridColumnWidth60pxWidth of each column
    @gridGutterWidth20pxNegative space between columns
    @siteWidthComputed sum of all columns and guttersCounts number of columns and gutters to set width of the .container-fixed() mixin
    +
    +
    +

    Variables in LESS

    +

    Built into Bootstrap are a handful of variables for customizing the default 940px grid system, documented above. All variables for the grid are stored in variables.less.

    +
    +
    +

    How to customize

    +

    Modifying the grid means changing the three @grid* variables and recompiling Bootstrap. Change the grid variables in variables.less and use one of the four ways documented to recompile. If you're adding more columns, be sure to add the CSS for those in grid.less.

    +
    +
    +

    Staying responsive

    +

    Customization of the grid only works at the default level, the 940px grid. To maintain the responsive aspects of Bootstrap, you'll also have to customize the grids in responsive.less.

    +
    +
    + +
    + + + + +
    + + +
    +
    +

    Fixed layout

    +

    The default and simple 940px-wide, centered layout for just about any website or page provided by a single <div class="container">.

    +
    +
    +
    +
    +<body>
    +  <div class="container">
    +    ...
    +  </div>
    +</body>
    +
    +
    +
    +

    Fluid layout

    +

    <div class="container-fluid"> gives flexible page structure, min- and max-widths, and a left-hand sidebar. It's great for apps and docs.

    +
    +
    +
    +
    +
    +<div class="container-fluid">
    +  <div class="row-fluid">
    +    <div class="span2">
    +      <!--Sidebar content-->
    +    </div>
    +    <div class="span10">
    +      <!--Body content-->
    +    </div>
    +  </div>
    +</div>
    +
    +
    +
    +
    + + + + + +
    + + +
    +
    + Responsive devices +
    +
    +

    Supported devices

    +

    Bootstrap supports a handful of media queries in a single file to help make your projects more appropriate on different devices and screen resolutions. Here's what's included:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    LabelLayout widthColumn widthGutter width
    Smartphones480px and belowFluid columns, no fixed widths
    Portrait tablets480px to 768pxFluid columns, no fixed widths
    Landscape tablets768px to 980px42px20px
    Default980px and up60px20px
    Large display1210px and up70px30px
    + +

    Requires meta tag

    +

    To ensure devices display responsive pages properly, include the viewport meta tag.

    +
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    + +

    What they do

    +

    Media queries allow for custom CSS based on a number of conditions—ratios, widths, display type, etc—but usually focuses around min-width and max-width.

    +
      +
    • Modify the width of column in our grid
    • +
    • Stack elements instead of float wherever necessary
    • +
    • Resize headings and text to be more appropriate for devices
    • +
    +
    +
    + +
    + + +

    Using the media queries

    +
    +
    +

    Bootstrap doesn't automatically include these media queries, but understanding and adding them is very easy and requires minimal setup. You have a few options for including the responsive features of Bootstrap:

    +
      +
    1. Use the compiled responsive version, bootstrap-responsive.css
    2. +
    3. Add @import "responsive.less" and recompile Bootstrap
    4. +
    5. Modify and recompile responsive.less as a separate file
    6. +
    +

    Why not just include it? Truth be told, not everything needs to be responsive. Instead of encouraging developers to remove this feature, we figure it best to enable it.

    +
    +
    +
    +  // Landscape phones and down
    +  @media (max-width: 480px) { ... }
    +
    +  // Landscape phone to portrait tablet
    +  @media (max-width: 768px) { ... }
    +
    +  // Portrait tablet to landscape and desktop
    +  @media (min-width: 768px) and (max-width: 980px) { ... }
    +
    +  // Large desktop
    +  @media (min-width: 1200px) { .. }
    +
    +
    +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/bootstrap/docs/templates/layout.mustache b/services/web/public/bootstrap/docs/templates/layout.mustache new file mode 100644 index 0000000000..a885b25beb --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/layout.mustache @@ -0,0 +1,132 @@ + + + + + Bootstrap, from Twitter + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +{{>body}} + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + {{#production}} + + + {{/production}} + + + diff --git a/services/web/public/bootstrap/docs/templates/pages/base-css.mustache b/services/web/public/bootstrap/docs/templates/pages/base-css.mustache new file mode 100644 index 0000000000..750957beb7 --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/base-css.mustache @@ -0,0 +1,1514 @@ + +
    +

    {{_i}}Base CSS{{/i}}

    +

    {{_i}}On top of the scaffolding, basic HTML elements are styled and enhanced with extensible classes to provide a fresh, consistent look and feel.{{/i}}

    + +
    + + + +
    + + +

    {{_i}}Headings & body copy{{/i}}

    + + +
    +
    +

    {{_i}}Typographic scale{{/i}}

    +

    {{_i}}The entire typographic grid is based on two Less variables in our variables.less file: @baseFontSize and @baseLineHeight. The first is the base font-size used throughout and the second is the base line-height.{{/i}}

    +

    {{_i}}We use those variables, and some math, to create the margins, paddings, and line-heights of all our type and more.{{/i}}

    +
    +
    +

    {{_i}}Example body text{{/i}}

    +

    Nullam quis risus eget urna mollis ornare vel eu leo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam id dolor id nibh ultricies vehicula ut id elit.

    +

    Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Donec sed odio dui.

    +
    +
    +
    +

    h1. {{_i}}Heading 1{{/i}}

    +

    h2. {{_i}}Heading 2{{/i}}

    +

    h3. {{_i}}Heading 3{{/i}}

    +

    h4. {{_i}}Heading 4{{/i}}

    +
    h5. {{_i}}Heading 5{{/i}}
    +
    h6. {{_i}}Heading 6{{/i}}
    +
    +
    +
    + + +

    {{_i}}Emphasis, address, and abbreviation{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Element{{/i}}{{_i}}Usage{{/i}}{{_i}}Optional{{/i}}
    + <strong> + + {{_i}}For emphasizing a snippet of text with important{{/i}} + + {{_i}}None{{/i}} +
    + <em> + + {{_i}}For emphasizing a snippet of text with stress{{/i}} + + {{_i}}None{{/i}} +
    + <abbr> + + {{_i}}Wraps abbreviations and acronyms to show the expanded version on hover{{/i}} + + {{_i}}Include optional title for expanded text{{/i}} +
    + <address> + + {{_i}}For contact information for its nearest ancestor or the entire body of work{{/i}} + + {{_i}}Preserve formatting by ending all lines with <br>{{/i}} +
    + +
    +
    +

    {{_i}}Using emphasis{{/i}}

    +

    Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Maecenas faucibus mollis interdum. Nulla vitae elit libero, a pharetra augue.

    +

    {{_i}}Note: Feel free to use <b> and <i> in HTML5, but their usage has changed a bit. <b> is meant to highlight words or phrases without conveying additional importance while <i> is mostly for voice, technical terms, etc.{{/i}}

    +
    +
    +

    {{_i}}Example addresses{{/i}}

    +

    {{_i}}Here are two examples of how the <address> tag can be used:{{/i}}

    +
    + Twitter, Inc.
    + 795 Folsom Ave, Suite 600
    + San Francisco, CA 94107
    + P: (123) 456-7890 +
    +
    + {{_i}}Full Name{{/i}}
    + {{_i}}first.last@gmail.com{{/i}} +
    +
    +
    +

    {{_i}}Example abbreviations{{/i}}

    +

    {{_i}}Abbreviations are styled with uppercase text and a light dotted bottom border. They also have a help cursor on hover so users have extra indication something will be shown on hover.{{/i}}

    +

    {{_i}}HTML is the best thing since sliced bread.{{/i}}

    +

    {{_i}}An abbreviation of the word attribute is attr.{{/i}}

    +
    +
    + + + +

    {{_i}}Blockquotes{{/i}}

    + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Element{{/i}}{{_i}}Usage{{/i}}{{_i}}Optional{{/i}}
    + <blockquote> + + {{_i}}Block-level element for quoting content from another source{{/i}} + +

    {{_i}}Add cite attribute for source URL{{/i}}

    + {{_i}}Use .pull-left and .pull-right classes for floated options{{/i}} +
    + <small> + + {{_i}}Optional element for adding a user-facing citation, typically an author with title of work{{/i}} + + {{_i}}Place the <cite> around the title or name of source{{/i}} +
    +
    +
    +

    {{_i}}To include a blockquote, wrap <blockquote> around any HTML as the quote. For straight quotes we recommend a <p>.{{/i}}

    +

    {{_i}}Include an optional <small> element to cite your source and you'll get an em dash &mdash; before it for styling purposes.{{/i}}

    +
    +
    +
    +<blockquote>
    +  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis.</p>
    +  <small>{{_i}}Someone famous{{/i}}</small>
    +</blockquote>
    +
    +
    +
    + +

    {{_i}}Example blockquotes{{/i}}

    +
    +
    +

    {{_i}}Default blockquotes are styled as such:{{/i}}

    +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis.

    + {{_i}}Someone famous in Body of work{{/i}} +
    +
    +
    +

    {{_i}}To float your blockquote to the right, add class="pull-right":{{/i}}

    +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis.

    + {{_i}}Someone famous in Body of work{{/i}} +
    +
    +
    + + + +

    {{_i}}Lists{{/i}}

    +
    +
    +

    {{_i}}Unordered{{/i}}

    +

    <ul>

    +
      +
    • Lorem ipsum dolor sit amet
    • +
    • Consectetur adipiscing elit
    • +
    • Integer molestie lorem at massa
    • +
    • Facilisis in pretium nisl aliquet
    • +
    • Nulla volutpat aliquam velit +
        +
      • Phasellus iaculis neque
      • +
      • Purus sodales ultricies
      • +
      • Vestibulum laoreet porttitor sem
      • +
      • Ac tristique libero volutpat at
      • +
      +
    • +
    • Faucibus porta lacus fringilla vel
    • +
    • Aenean sit amet erat nunc
    • +
    • Eget porttitor lorem
    • +
    +
    +
    +

    {{_i}}Unstyled{{/i}}

    +

    <ul class="unstyled">

    +
      +
    • Lorem ipsum dolor sit amet
    • +
    • Consectetur adipiscing elit
    • +
    • Integer molestie lorem at massa
    • +
    • Facilisis in pretium nisl aliquet
    • +
    • Nulla volutpat aliquam velit +
        +
      • Phasellus iaculis neque
      • +
      • Purus sodales ultricies
      • +
      • Vestibulum laoreet porttitor sem
      • +
      • Ac tristique libero volutpat at
      • +
      +
    • +
    • Faucibus porta lacus fringilla vel
    • +
    • Aenean sit amet erat nunc
    • +
    • Eget porttitor lorem
    • +
    +
    +
    +

    {{_i}}Ordered{{/i}}

    +

    <ol>

    +
      +
    1. Lorem ipsum dolor sit amet
    2. +
    3. Consectetur adipiscing elit
    4. +
    5. Integer molestie lorem at massa
    6. +
    7. Facilisis in pretium nisl aliquet
    8. +
    9. Nulla volutpat aliquam velit
    10. +
    11. Faucibus porta lacus fringilla vel
    12. +
    13. Aenean sit amet erat nunc
    14. +
    15. Eget porttitor lorem
    16. +
    +
    +
    +

    {{_i}}Description{{/i}}

    +

    <dl>

    +
    +
    {{_i}}Description lists{{/i}}
    +
    {{_i}}A description list is perfect for defining terms.{{/i}}
    +
    Euismod
    +
    Vestibulum id ligula porta felis euismod semper eget lacinia odio sem nec elit.
    +
    Donec id elit non mi porta gravida at eget metus.
    +
    Malesuada porta
    +
    Etiam porta sem malesuada magna mollis euismod.
    +
    +
    +
    +
    + + + + +
    + +
    +
    +

    Inline

    +

    Wrap inline snippets of code with <code>.

    +
    +{{_i}}For example, <code>section</code> should be wrapped as inline.{{/i}}
    +
    +
    +
    +

    Basic block

    +

    {{_i}}Use <pre> for multiple lines of code. Be sure to turn any angle brackets into unicode characters for proper rendering.{{/i}}

    +
    +<p>{{_i}}Sample text here...{{/i}}</p>
    +
    +
    +<pre>
    +  &lt;p&gt;{{_i}}Sample text here...{{/i}}&lt;/p&gt;
    +</pre>
    +
    +

    {{_i}}Note: Be sure to keep code within <pre> tags as close to the left as possible; it will render all tabs.{{/i}}

    +

    {{_i}}You may optionally add the .pre-scrollable class which will set a max-height of 350px and provide a y-axis scrollbar.{{/i}}

    +
    +
    +

    Google Prettify

    +

    Take the same <pre> element and add two optional classes for enhanced rendering.

    +
    +<p>{{_i}}Sample text here...{{/i}}</p>
    +
    +
    +<pre class="prettyprint
    +     linenums">
    +  &lt;p&gt;{{_i}}Sample text here...{{/i}}&lt;/p&gt;
    +</pre>
    +
    +

    {{_i}}Download google-code-prettify and view the readme for how to use.{{/i}}

    +
    +
    +
    + + + + +
    + + +

    {{_i}}Table markup{{/i}}

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Tag{{/i}}{{_i}}Description{{/i}}
    + <table> + + {{_i}}Wrapping element for displaying data in a tabular format{{/i}} +
    + <thead> + + {{_i}}Container element for table header rows (<tr>) to label table columns{{/i}} +
    + <tbody> + + {{_i}}Container element for table rows (<tr>) in the body of the table{{/i}} +
    + <tr> + + {{_i}}Container element for a set of table cells (<td> or <th>) that appears on a single row{{/i}} +
    + <td> + + {{_i}}Default table cell{{/i}} +
    + <th> + + {{_i}}Special table cell for column (or row, depending on scope and placement) labels{{/i}}
    + {{_i}}Must be used within a <thead>{{/i}} +
    + <caption> + + {{_i}}Description or summary of what the table holds, especially useful for screen readers{{/i}} +
    +
    +
    +
    +<table>
    +  <thead>
    +    <tr>
    +      <th>…</th>
    +      <th>…</th>
    +    </tr>
    +  </thead>
    +  <tbody>
    +    <tr>
    +      <td>…</td>
    +      <td>…</td>
    +    </tr>
    +  </tbody>
    +</table>
    +
    +
    +
    + +

    {{_i}}Table options{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}Class{{/i}}{{_i}}Description{{/i}}
    {{_i}}Default{{/i}}{{_i}}None{{/i}}{{_i}}No styles, just columns and rows{{/i}}
    {{_i}}Basic{{/i}} + .table + {{_i}}Only horizontal lines between rows{{/i}}
    {{_i}}Bordered{{/i}} + .table-bordered + {{_i}}Rounds corners and adds outer border{{/i}}
    {{_i}}Zebra-stripe{{/i}} + .table-striped + {{_i}}Adds light gray background color to odd rows (1, 3, 5, etc){{/i}}
    {{_i}}Condensed{{/i}} + .table-condensed + {{_i}}Cuts vertical padding in half, from 8px to 4px, within all td and th elements{{/i}}
    + + +

    {{_i}}Example tables{{/i}}

    + +

    1. {{_i}}Default table styles{{/i}}

    +
    +
    +

    {{_i}}Tables are automatically styled with only a few borders to ensure readability and maintain structure. With 2.0, the .table class is required.{{/i}}

    +
    +<table class="table">
    +  …
    +</table>
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{{_i}}First Name{{/i}}{{_i}}Last Name{{/i}}{{_i}}Language{{/i}}
    1MarkOttoCSS
    2JacobThorntonJavascript
    3StuDentHTML
    +
    +
    + + +

    2. {{_i}}Striped table{{/i}}

    +
    +
    +

    {{_i}}Get a little fancy with your tables by adding zebra-striping—just add the .table-striped class.{{/i}}

    +

    {{_i}}Note: Striped tables use the :nth-child CSS selector and is not available in IE7-IE8.{{/i}}

    +
    +<table class="table table-striped">
    +  …
    +</table>
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{{_i}}First Name{{/i}}{{_i}}Last Name{{/i}}{{_i}}Language{{/i}}
    1MarkOttoCSS
    2JacobThorntonJavascript
    3StuDentHTML
    +
    +
    + + +

    3. {{_i}}Bordered table{{/i}}

    +
    +
    +

    {{_i}}Add borders around the entire table and rounded corners for aesthetic purposes.{{/i}}

    +
    +<table class="table table-bordered">
    +  …
    +</table>
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{{_i}}First Name{{/i}}{{_i}}Last Name{{/i}}{{_i}}Language{{/i}}
    1Mark OttoCSS
    2JacobThorntonJavascript
    3StuDent
    3BrosefStalinHTML
    +
    +
    + + +

    4. {{_i}}Condensed table{{/i}}

    +
    +
    +

    {{_i}}Make your tables more compact by adding the .table-condensed class to cut table cell padding in half (from 8px to 4px).{{/i}}

    +
    +<table class="table table-condensed">
    +  …
    +</table>
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{{_i}}First Name{{/i}}{{_i}}Last Name{{/i}}{{_i}}Language{{/i}}
    1MarkOttoCSS
    2JacobThorntonJavascript
    3StuDentHTML
    +
    +
    + + + +

    5. {{_i}}Combine them all!{{/i}}

    +
    +
    +

    {{_i}}Feel free to combine any of the table classes to achieve different looks by utilizing any of the available classes.{{/i}}

    +
    +<table class="table table-striped table-bordered table-condensed">
    +  ...
    +</table>
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{{_i}}First Name{{/i}}{{_i}}Last Name{{/i}}{{_i}}Language{{/i}}
    1MarkOttoCSS
    2JacobThorntonJavascript
    3StuDentHTML
    4BrosefStalinHTML
    +
    +
    +
    + + + + +
    + +
    +
    +

    {{_i}}Flexible HTML and CSS{{/i}}

    +

    {{_i}}The best part about forms in Bootstrap is that all your inputs and controls look great no matter how you build them in your markup. No superfluous HTML is required, but we provide the patterns for those who require it.{{/i}}

    +

    {{_i}}More complicated layouts come with succinct and scalable classes for easy styling and event binding, so you're covered at every step.{{/i}}

    +
    +
    +

    {{_i}}Four layouts included{{/i}}

    +

    {{_i}}Bootstrap comes with support for four types of form layouts:{{/i}}

    +
      +
    • {{_i}}Vertical (default){{/i}}
    • +
    • {{_i}}Search{{/i}}
    • +
    • {{_i}}Inline{{/i}}
    • +
    • {{_i}}Horizontal{{/i}}
    • +
    +

    {{_i}}Different types of form layouts require some changes to markup, but the controls themselves remain and behave the same.{{/i}}

    +
    +
    +

    {{_i}}Control states and more{{/i}}

    +

    {{_i}}Bootstrap's forms include styles for all the base form controls like input, textarea, and select you'd expect. But it also comes with a number of custom components like appended and prepended inputs and support for lists of checkboxes.{{/i}}

    +

    {{_i}}States like error, warning, and success are included for each type of form control. Also included are styles for disabled controls.{{/i}}

    +
    +
    + +

    {{_i}}Four types of forms{{/i}}

    +

    {{_i}}Bootstrap provides simple markup and styles for four styles of common web forms.{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}Class{{/i}}{{_i}}Description{{/i}}
    {{_i}}Vertical (default){{/i}}.form-vertical ({{_i}}not required{{/i}}){{_i}}Stacked, left-aligned labels over controls{{/i}}
    {{_i}}Inline{{/i}}.form-inline{{_i}}Left-aligned label and inline-block controls for compact style{{/i}}
    {{_i}}Search{{/i}}.form-search{{_i}}Extra-rounded text input for a typical search aesthetic{{/i}}
    {{_i}}Horizontal{{/i}}.form-horizontal{{_i}}Float left, right-aligned labels on same line as controls{{/i}}
    + + +

    {{_i}}Example forms using just form controls, no extra markup{{/i}}

    +
    +
    +

    {{_i}}Basic form{{/i}}

    +

    {{_i}}With v2.0, we have lighter and smarter defaults for form styles. No extra markup, just form controls.{{/i}}

    +
    +
    +
    + + Associated help text! + + +
    +
    +<form class="well">
    +  <label>{{_i}}Label name{{/i}}</label>
    +  <input type="text" class="span3" placeholder="{{_i}}Type something…{{/i}}">
    +  <span class="help-inline">Associated help text!</span>
    +  <label class="checkbox">
    +    <input type="checkbox"> {{_i}}Check me out{{/i}}
    +  </label>
    +  <button type="submit" class="btn">{{_i}}Submit{{/i}}</button>
    +</form>
    +
    +
    +
    +
    +
    +

    {{_i}}Search form{{/i}}

    +

    {{_i}}Reflecting default WebKit styles, just add .form-search for extra rounded search fields.{{/i}}

    +
    +
    + +
    +<form class="well form-search">
    +  <input type="text" class="input-medium search-query">
    +  <button type="submit" class="btn">{{_i}}Search{{/i}}</button>
    +</form>
    +
    +
    +
    +
    +
    +

    {{_i}}Inline form{{/i}}

    +

    {{_i}}Inputs are block level to start. For .form-inline and .form-horizontal, we use inline-block.{{/i}}

    +
    +
    +
    + + + +
    +
    +<form class="well form-inline">
    +  <input type="text" class="input-small" placeholder="{{_i}}Email{{/i}}">
    +  <input type="password" class="input-small" placeholder="{{_i}}Password{{/i}}">
    +  <button type="submit" class="btn">{{_i}}Go{{/i}}</button>
    +</form>
    +
    +
    +
    + +
    + +

    {{_i}}Horizontal forms{{/i}}

    +
    +
    +
    +
    + {{_i}}Controls Bootstrap supports{{/i}} +
    + +
    + +

    {{_i}}In addition to freeform text, any HTML5 text-based input appears like so.{{/i}}

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    +
    +

    {{_i}}Example markup{{/i}}

    +

    {{_i}}Given the above example form layout, here's the markup associated with the first input and control group. The .control-group, .control-label, and .controls classes are all required for styling.{{/i}}

    +
    +<form class="form-horizontal">
    +  <fieldset>
    +    <legend>{{_i}}Legend text{{/i}}</legend>
    +    <div class="control-group">
    +      <label class="control-label" for="input01">{{_i}}Text input{{/i}}</label>
    +      <div class="controls">
    +        <input type="text" class="input-xlarge" id="input01">
    +        <p class="help-block">{{_i}}Supporting help text{{/i}}</p>
    +      </div>
    +    </div>
    +  </fieldset>
    +</form>
    +
    +
    +
    +

    {{_i}}What's included{{/i}}

    +

    {{_i}}Shown on the left are all the default form controls we support. Here's the bulleted list:{{/i}}

    +
      +
    • {{_i}}text inputs (text, password, email, etc){{/i}}
    • +
    • {{_i}}checkbox{{/i}}
    • +
    • {{_i}}radio{{/i}}
    • +
    • {{_i}}select{{/i}}
    • +
    • {{_i}}multiple select{{/i}}
    • +
    • {{_i}}file input{{/i}}
    • +
    • {{_i}}textarea{{/i}}
    • +
    +
    +

    {{_i}}New defaults with v2.0{{/i}}

    +

    {{_i}}Up to v1.4, Bootstrap's default form styles used the horizontal layout. With Bootstrap 2, we removed that constraint to have smarter, more scalable defaults for any form.{{/i}}

    +
    +
    + +
    + +
    +
    +
    +
    + {{_i}}Form control states{{/i}} +
    + +
    + +
    +
    +
    + +
    + Some value here +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + {{_i}}Something may have gone wrong{{/i}} +
    +
    +
    + +
    + + {{_i}}Please correct the error{{/i}} +
    +
    +
    + +
    + + {{_i}}Woohoo!{{/i}} +
    +
    +
    + +
    + + {{_i}}Woohoo!{{/i}} +
    +
    +
    + + +
    +
    +
    +
    +
    +

    {{_i}}Redesigned browser states{{/i}}

    +

    {{_i}}Bootstrap features styles for browser-supported focused and disabled states. We remove the default Webkit outline and apply a box-shadow in its place for :focus.{{/i}}

    +
    +

    {{_i}}Form validation{{/i}}

    +

    {{_i}}It also includes validation styles for errors, warnings, and success. To use, add the error class to the surrounding .control-group.{{/i}}

    +
    +<fieldset
    +  class="control-group error">
    +  …
    +</fieldset>
    +
    +
    +
    + +
    + +
    +
    +
    +
    + {{_i}}Extending form controls{{/i}} +
    + +
    + + + + + + +

    {{_i}}Use the same .span* classes from the grid system for input sizes.{{/i}}

    +
    +
    +
    + +
    +
    + @ + +
    +

    {{_i}}Here's some help text{{/i}}

    +
    +
    +
    + +
    +
    + + .00 +
    +

    {{_i}}Here's more help text{{/i}}

    +
    +
    +
    + +
    + + + +
    +
    +
    + +
    + + + +

    {{_i}}Note: Labels surround all the options for much larger click areas and a more usable form.{{/i}}

    +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    +
    +
    +
    +

    {{_i}}Prepend & append inputs{{/i}}

    +

    {{_i}}Input groups—with appended or prepended text—provide an easy way to give more context for your inputs. Great examples include the @ sign for Twitter usernames or $ for finances.{{/i}}

    +
    +

    {{_i}}Checkboxes and radios{{/i}}

    +

    {{_i}}Up to v1.4, Bootstrap required extra markup around checkboxes and radios to stack them. Now, it's a simple matter of repeating the <label class="checkbox"> that wraps the <input type="checkbox">.{{/i}}

    +

    {{_i}}Inline checkboxes and radios are also supported. Just add .inline to any .checkbox or .radio and you're done.{{/i}}

    +
    +

    {{_i}}Inline forms and append/prepend{{/i}}

    +

    {{_i}}To use prepend or append inputs in an inline form, be sure to place the .add-on and input on the same line, without spaces.{{/i}}

    +
    +

    {{_i}}Form help text{{/i}}

    +

    {{_i}}To add help text for your form inputs, include inline help text with <span class="help-inline"> or a help text block with <p class="help-block"> after the input element.{{/i}}

    +
    +
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Button{{/i}}{{_i}}Class{{/i}}{{_i}}Description{{/i}}
    {{_i}}Default{{/i}}.btn{{_i}}Standard gray button with gradient{{/i}}
    {{_i}}Primary{{/i}}.btn-primary{{_i}}Provides extra visual weight and identifies the primary action in a set of buttons{{/i}}
    {{_i}}Info{{/i}}.btn-info{{_i}}Used as an alternate to the default styles{{/i}}
    {{_i}}Success{{/i}}.btn-success{{_i}}Indicates a successful or positive action{{/i}}
    {{_i}}Warning{{/i}}.btn-warning{{_i}}Indicates caution should be taken with this action{{/i}}
    {{_i}}Danger{{/i}}.btn-danger{{_i}}Indicates a dangerous or potentially negative action{{/i}}
    + +
    +
    +

    {{_i}}Buttons for actions{{/i}}

    +

    {{_i}}As a convention, buttons should only be used for actions while hyperlinks are to be used for objects. For instance, "Download" should be a button while "recent activity" should be a link.{{/i}}

    +

    {{_i}}Button styles can be applied to anything with the .btn class applied. However, typically you'll want to apply these to only <a> and <button> elements.{{/i}}

    +

    {{_i}}Cross browser compatibility{{/i}}

    +

    {{_i}}IE9 doesn't crop background gradients on rounded corners, so we remove it. Related, IE9 jankifies disabled button elements, rendering text gray with a nasty text-shadow that we cannot fix.{{/i}}

    +
    +
    +

    {{_i}}Multiple sizes{{/i}}

    +

    {{_i}}Fancy larger or smaller buttons? Add .btn-large or .btn-small for two additional sizes.{{/i}}

    +

    + {{_i}}Primary action{{/i}} + {{_i}}Action{{/i}} +

    +

    + {{_i}}Primary action{{/i}} + {{_i}}Action{{/i}} +

    +
    +

    {{_i}}Disabled state{{/i}}

    +

    {{_i}}For disabled buttons, use .btn-disabled for links and :disabled for <button> elements.{{/i}}

    +

    + {{_i}}Primary action{{/i}} + {{_i}}Action{{/i}} +

    +

    + + +

    +
    +
    +

    {{_i}}One class, multiple tags{{/i}}

    +

    {{_i}}Use the .btn class on an <a>, <button>, or <input> element.{{/i}}

    +
    +{{_i}}Link{{/i}} + + + +
    +
    +<a class="btn" href="">{{_i}}Link{{/i}}</a>
    +<button class="btn" type="submit">
    +  {{_i}}Button{{/i}}
    +</button>
    +<input class="btn" type="button" 
    +         value="{{_i}}Input{{/i}}">
    +<input class="btn" type="submit" 
    +         value="{{_i}}Submit{{/i}}">
    +
    +

    {{_i}}As a best practice, try to match the element for you context to ensure matching cross-browser rendering. If you have an input, use an <input type="submit"> for your button.{{/i}}

    +
    +
    +
    + + + + +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + {{_i}}Heads up! Icon classes are echoed via CSS :after. In the docs, we show a light red background color on hover to highlight the icon's size.{{/i}} +
    + +
    + +
    +
    +

    {{_i}}Built as a sprite{{/i}}

    +

    {{_i}}Instead of making every icon an extra request, we've compiled them into a sprite—a bunch of images in one file that uses CSS to position the images with background-position. This is the same method we use on Twitter.com and it has worked well for us.{{/i}}

    +

    {{_i}}All icons classes are prefixed with .icon- for proper namespacing and scoping, much like our other components. This will help avoid conflicts with other tools.{{/i}}

    +

    {{_i}}Glyphicons has granted us use of the Halflings set in our open-source toolkit so long as we provide a link and credit here in the docs. Please consider doing the same in your projects.{{/i}}

    +
    +
    +

    {{_i}}How to use{{/i}}

    +

    {{_i}}With v2.0.0, we have opted to use an <i> tag for all our icons, but they have no case class—only a shared prefix. To use, place the following code just about anywhere:{{/i}}

    +
    +<i class="icon-search"></i>
    +
    +

    {{_i}}There are also styles available for inverted (white) icons, made ready with one extra class:{{/i}}

    +
    +<i class="icon-search icon-white"></i>
    +
    +

    {{_i}}There are 120 classes to choose from for your icons. Just add an <i> tag with the right classes and you're set. You can find the full list in sprites.less or right here in this document.{{/i}}

    +
    +
    +

    {{_i}}Use cases{{/i}}

    +

    {{_i}}Icons are great, but where would one use them? Here are a few ideas:{{/i}}

    +
      +
    • {{_i}}As visuals for your sidebar navigation{{/i}}
    • +
    • {{_i}}For a purely icon-driven navigation{{/i}}
    • +
    • {{_i}}For buttons to help convey the meaning of an action{{/i}}
    • +
    • {{_i}}With links to share context on a user's destination{{/i}}
    • +
    +

    {{_i}}Essentially, anywhere you can put an <i> tag, you can put an icon.{{/i}}

    +
    +
    + +

    {{_i}}Examples{{/i}}

    +

    {{_i}}Use them in buttons, button groups for a toolbar, navigation, or prepended form inputs.{{/i}}

    + +
    diff --git a/services/web/public/bootstrap/docs/templates/pages/components.mustache b/services/web/public/bootstrap/docs/templates/pages/components.mustache new file mode 100644 index 0000000000..62a7083b61 --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/components.mustache @@ -0,0 +1,1414 @@ + +
    +

    {{_i}}Components{{/i}}

    +

    {{_i}}Dozens of reusable components are built into Bootstrap to provide navigation, alerts, popovers, and much more.{{/i}}

    + +
    + + + + +
    + +
    +
    +

    {{_i}}Button groups{{/i}}

    +

    {{_i}}Use button groups to join multiple buttons together as one composite component. Build them with a series of <a> or <button> elements.{{/i}}

    +

    {{_i}}Best practices{{/i}}

    +

    {{_i}}We recommend the following guidelines for using button groups and toolbars:{{/i}}

    +
      +
    • {{_i}}Always use the same element in a single button group, <a> or <button>.{{/i}}
    • +
    • {{_i}}Don't mix buttons of different colors in the same button group.{{/i}}
    • +
    • {{_i}}Use icons in addition to or instead of text, but be sure include alt and title text where appropriate.{{/i}}
    • +
    +

    {{_i}}Related Button groups with dropdowns (see below) should be called out separately and always include a dropdown caret to indicate intended behavior.{{/i}}

    +
    +
    +

    {{_i}}Default example{{/i}}

    +

    {{_i}}Here's how the HTML looks for a standard button group built with anchor tag buttons:{{/i}}

    + +
    +<div class="btn-group">
    +  <a class="btn" href="#">1</a>
    +  <a class="btn" href="#">2</a>
    +  <a class="btn" href="#">3</a>
    +</div>
    +
    +

    {{_i}}Toolbar example{{/i}}

    +

    {{_i}}Combine sets of <div class="btn-group"> into a <div class="btn-toolbar"> for more complex components.{{/i}}

    +
    +
    + 1 + 2 + 3 + 4 +
    +
    + 5 + 6 + 7 +
    +
    + 8 +
    +
    +
    +<div class="btn-toolbar">
    +  <div class="btn-group">
    +    ...
    +  </div>
    +</div>
    +
    +
    +
    +

    {{_i}}Checkbox and radio flavors{{/i}}

    +

    {{_i}}Button groups can also function as radios, where only one button may be active, or checkboxes, where any number of buttons may be active. View the Javascript docs for that.{{/i}}

    +

    {{_i}}Get the javascript »{{/i}}

    +
    +

    {{_i}}Heads up{{/i}}

    +

    {{_i}}CSS for button groups is in a separate file, button-groups.less.{{/i}}

    +
    +
    +
    + + + + +
    + + +
    + +
    +

    {{_i}}Example markup{{/i}}

    +

    {{_i}}Similar to a button group, our markup uses regular button markup, but with a handful of additions to refine the style and support Bootstrap's dropdown jQuery plugin.{{/i}}

    +
    +<div class="btn-group">
    +  <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
    +    {{_i}}Action{{/i}}
    +    <span class="caret"></span>
    +  </a>
    +  <ul class="dropdown-menu">
    +    <!-- {{_i}}dropdown menu links{{/i}} -->
    +  </ul>
    +</div>
    +
    +
    +
    + +
    + +
    +

    {{_i}}Example markup{{/i}}

    +

    {{_i}}We expand on the normal button dropdowns to provide a second button action that operates as a separate dropdown trigger.{{/i}}

    +
    +<div class="btn-group">
    +  <a class="btn" href="#">{{_i}}Action{{/i}}</a>
    +  <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
    +    <span class="caret"></span>
    +  </a>
    +  <ul class="dropdown-menu">
    +    <!-- {{_i}}dropdown menu links{{/i}} -->
    +  </ul>
    +</div>
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    + + +

    {{_i}}Multicon-page pagination{{/i}}

    +
    +
    +

    {{_i}}When to use{{/i}}

    +

    {{_i}}Ultra simplistic and minimally styled pagination inspired by Rdio, great for apps and search results. The large block is hard to miss, easily scalable, and provides large click areas.{{/i}}

    +

    {{_i}}Stateful page links{{/i}}

    +

    {{_i}}Links are customizable and work in a number of circumstances with the right class. .disabled for unclickable links and .active for current page.{{/i}}

    +

    {{_i}}Flexible alignment{{/i}}

    +

    {{_i}}Add either of two optional classes to change the alignment of pagination links: .pagination-centered and .pagination-right.{{/i}}

    +
    +
    +

    {{_i}}Examples{{/i}}

    +

    {{_i}}The default pagination component is flexible and works in a number of variations.{{/i}}

    + + + + +
    +
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}Wrapped in a <div>, pagination is just a <ul>.{{/i}}

    +
    +<div class="pagination">
    +  <ul>
    +    <li><a href="#">Prev</a></li>
    +    <li class="active">
    +      <a href="#">1</a>
    +    </li>
    +    <li><a href="#">2</a></li>
    +    <li><a href="#">3</a></li>
    +    <li><a href="#">4</a></li>
    +    <li><a href="#">Next</a></li>
    +  </ul>
    +</div>
    +
    +
    +
    + +

    {{_i}}Pager{{/i}} {{_i}}For quick previous and next links{{/i}}

    +
    +
    +

    {{_i}}About pager{{/i}}

    +

    {{_i}}The pager component is a set of links for simple pagination implementations with light markup and even lighter styles. It's great for simple sites like blogs or magazines.{{/i}}

    +
    +
    +

    {{_i}}Default example{{/i}}

    +

    {{_i}}By default, the pager centers links.{{/i}}

    + +
    +<ul class="pager">
    +  <li>
    +    <a href="#">{{_i}}Previous{{/i}}</a>
    +  </li>
    +  <li>
    +    <a href="#">{{_i}}Next{{/i}}</a>
    +  </li>
    +</ul>
    +
    +
    +
    +

    {{_i}}Aligned links{{/i}}

    +

    {{_i}}Alternatively, you can align each link to the sides:{{/i}}

    + +
    +<ul class="pager">
    +  <li class="previous">
    +    <a href="#">{{_i}}&larr; Older{{/i}}</a>
    +  </li>
    +  <li class="next">
    +    <a href="#">{{_i}}Newer &rarr;{{/i}}</a>
    +  </li>
    +</ul>
    +
    +
    +
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Labels{{/i}}{{_i}}Markup{{/i}}
    + {{_i}}Default{{/i}} + + <span class="label">{{_i}}Default{{/i}}</span> +
    + {{_i}}Success{{/i}} + + <span class="label label-success">{{/_i}}Success{{/i}}</span> +
    + {{_i}}Warning{{/i}} + + <span class="label label-warning">{{_i}}Warning{{/i}}</span> +
    + {{_i}}Important{{/i}} + + <span class="label label-important">{{_i}}Important{{/i}}</span> +
    + {{_i}}Info{{/i}} + + <span class="label label-info">{{_i}}Info{{/i}}</span> +
    +
    + + + + +
    + + +
    +
    +

    {{_i}}Default thumbnails{{/i}}

    +

    {{_i}}By default, Bootstrap's thumbnails are designed to showcase linked images with minimal required markup.{{/i}}

    + +
    +
    +

    {{_i}}Highly customizable{{/i}}

    +

    {{_i}}With a bit of extra markup, it's possible to add any kind of HTML content like headings, paragraphs, or buttons into thumbnails.{{/i}}

    +
      +
    • +
      + +
      +
      {{_i}}Thumbnail label{{/i}}
      +

      Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.

      +

      {{_i}}Action{{/i}} {{_i}}Action{{/i}}

      +
      +
      +
    • +
    • +
      + +
      +
      {{_i}}Thumbnail label{{/i}}
      +

      Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.

      +

      {{_i}}Action{{/i}} {{_i}}Action{{/i}}

      +
      +
      +
    • +
    +
    +
    + +
    +
    +

    {{_i}}Why use thumbnails{{/i}}

    +

    {{_i}}Thumbnails (previously .media-grid up until v1.4) are great for grids of photos or videos, image search results, retail products, portfolios, and much more. They can be links or static content.{{/i}}

    +
    +
    +

    {{_i}}Simple, flexible markup{{/i}}

    +

    {{_i}}Thumbnail markup is simple—a ul with any number of li elements is all that is required. It's also super flexible, allowing for any type of content with just a bit more markup to wrap your contents.{{/i}}

    +
    +
    +

    {{_i}}Uses grid column sizes{{/i}}

    +

    {{_i}}Lastly, the thumbnails component uses existing grid system classes—like .span2 or .span3—for control of thumbnail dimensions.{{/i}}

    +
    +
    + +
    +
    +

    {{_i}}The markup{{/i}}

    +

    {{_i}}As mentioned previously, the required markup for thumbnails is light and straightforward. Here's a look at the default setup for linked images:{{/i}}

    +
    +<ul class="thumbnails">
    +  <li class="span3">
    +    <a href="#" class="thumbnail">
    +      <img src="http://placehold.it/260x180" alt="">
    +    </a>
    +  </li>
    +  ...
    +</ul>
    +
    +

    {{_i}}For custom HTML content in thumbnails, the markup changes slightly. To allow block level content anywhere, we swap the <a> for a <div> like so:{{/i}}

    +
    +<ul class="thumbnails">
    +  <li class="span3">
    +    <div class="thumbnail">
    +      <img src="http://placehold.it/260x180" alt="">
    +      <h5>{{_i}}Thumbnail label{{/i}}</h5>
    +      <p>{{_i}}Thumbnail caption right here...{{/i}}</p>
    +    </div>
    +  </li>
    +  ...
    +</ul>
    +
    +
    +
    +

    {{_i}}More examples{{/i}}

    +

    {{_i}}Explore all your options with the various grid classes available to you. You can also mix and match different sizes.{{/i}}

    + +
    +
    + +
    + + + + +
    + + +

    {{_i}}Lightweight defaults{{/i}}

    +
    +
    +

    {{_i}}Rewritten base class{{/i}}

    +

    {{_i}}With Bootstrap 2, we've simplified the base class: .alert instead of .alert-message. We've also reduced the minimum required markup—no <p> is required by default, just the outer <div>.{{/i}}

    +

    {{_i}}Single alert message{{/i}}

    +

    {{_i}}For a more durable component with less code, we've removed the differentiating look for block alerts, messages that come with more padding and typically more text. The class also has changed to .alert-block.{{/i}}

    +
    +

    {{_i}}Goes great with javascript{{/i}}

    +

    {{_i}}Bootstrap comes with a great jQuery plugin that supports alert messages, making dismissing them quick and easy.{{/i}}

    +

    {{_i}}Get the plugin »{{/i}}

    +
    +
    +

    {{_i}}Example alerts{{/i}}

    +

    {{_i}}Wrap your message and an optional close icon in a div with simple class.{{/i}}

    +
    + × + {{_i}}Warning!{{/i}} {{_i}}Best check yo self, you're not looking too good.{{/i}} +
    +
    +<div class="alert">
    +  <a class="close" data-dismiss="alert">×</a>
    +  <strong>{{_i}}Warning!{{/i}}</strong> {{_i}}Best check yo self, you're not looking too good.{{/i}}
    +</div>
    +
    +

    {{_i}}Easily extend the standard alert message with two optional classes: .alert-block for more padding and text controls and .alert-heading for a matching heading.{{/i}}

    +
    + × +

    {{_i}}Warning!{{/i}}

    +

    {{_i}}Best check yo self, you're not looking too good.{{/i}} Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

    +
    +
    +<div class="alert alert-block">
    +  <a class="close" data-dismiss="alert">×</a>
    +  <h4 class="alert-heading">{{_i}}Warning!{{/i}}</h4>
    +  {{_i}}Best check yo self, you're not...{{/i}}
    +</div>
    +
    +
    +
    + +

    {{_i}}Contextual alternatives{{/i}} {{_i}}Add optional classes to change an alert's connotation{{/i}}

    +
    +
    +

    {{_i}}Error or danger{{/i}}

    +
    + × + {{_i}}Oh snap!{{/i}} {{_i}}Change a few things up and try submitting again.{{/i}} +
    +
    +<div class="alert alert-error">
    +  ...
    +</div>
    +
    +
    +
    +

    {{_i}}Success{{/i}}

    +
    + × + {{_i}}Well done!{{/i}} {{_i}}You successfully read this important alert message.{{/i}} +
    +
    +<div class="alert alert-success">
    +  ...
    +</div>
    +
    +
    +
    +

    {{_i}}Information{{/i}}

    +
    + × + {{_i}}Heads up!{{/i}} {{_i}}This alert needs your attention, but it's not super important.{{/i}} +
    +
    +<div class="alert alert-info">
    +  ...
    +</div>
    +
    +
    +
    + +
    + + + + +
    + + +

    {{_i}}Examples and markup{{/i}}

    +
    +
    +

    {{_i}}Basic{{/i}}

    +

    {{_i}}Default progress bar with a vertical gradient.{{/i}}

    +
    +
    +
    +
    +<div class="progress">
    +  <div class="bar"
    +       style="width: 60%;"></div>
    +</div>
    +
    +
    +
    +

    {{_i}}Striped{{/i}}

    +

    {{_i}}Uses a gradient to create a striped effect.{{/i}}

    +
    +
    +
    +
    +<div class="progress progress-info
    +     progress-striped">
    +  <div class="bar"
    +       style="width: 20%;"></div>
    +</div>
    +
    +
    +
    +

    {{_i}}Animated{{/i}}

    +

    {{_i}}Takes the striped example and animates it.{{/i}}

    +
    +
    +
    +
    +<div class="progress progress-danger
    +     progress-striped active">
    +  <div class="bar"
    +       style="width: 40%;"></div>
    +</div>
    +
    +
    +
    + +

    {{_i}}Options and browser support{{/i}}

    +
    +
    +

    {{_i}}Additional colors{{/i}}

    +

    {{_i}}Progress bars utilize some of the same class names as buttons and alerts for similar styling.{{/i}}

    +
      +
    • .progress-info
    • +
    • .progress-success
    • +
    • .progress-danger
    • +
    +

    {{_i}}Alternatively, you can customize the LESS files and roll your own colors and sizes.{{/i}}

    +
    +
    +

    {{_i}}Behavior{{/i}}

    +

    {{_i}}Progress bars use CSS3 transitions, so if you dynamically adjust the width via javascript, it will smoothly resize.{{/i}}

    +

    {{_i}}If you use the .active class, your .progress-striped progress bars will animate the stripes left to right.{{/i}}

    +
    +
    +

    {{_i}}Browser support{{/i}}

    +

    {{_i}}Progress bars use CSS3 gradients, transitions, and animations to achieve all their effects. These features are not supported in IE7-8 or older versions of Firefox.{{/i}}

    +

    {{_i}}Opera does not support animations at this time.{{/i}}

    +
    +
    + +
    + + + + + + +
    + +
    +
    +

    {{_i}}Wells{{/i}}

    +

    {{_i}}Use the well as a simple effect on an element to give it an inset effect.{{/i}}

    +
    + {{_i}}Look, I'm in a well!{{/i}} +
    +
    +<div class="well">
    +  ...
    +</div>
    +
    +
    + +
    +

    {{_i}}Close icon{{/i}}

    +

    {{_i}}Use the generic close icon for dismissing content like modals and alerts.{{/i}}

    +

    ×

    +
    <a class="close">&times;</a>
    +
    +
    +
    diff --git a/services/web/public/bootstrap/docs/templates/pages/download.mustache b/services/web/public/bootstrap/docs/templates/pages/download.mustache new file mode 100644 index 0000000000..fe38d1450e --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/download.mustache @@ -0,0 +1,248 @@ + +
    +

    {{_i}}Customize and download{{/i}}

    +

    {{_i}}Download the full repository or customize your entire Bootstrap build by selecting only the components, javascript plugins, and assets you need.{{/i}}

    + +
    + +
    + +
    +
    +

    {{_i}}Scaffolding{{/i}}

    + + + + +

    {{_i}}Base CSS{{/i}}

    + + + + + + + +
    +
    +

    {{_i}}Components{{/i}}

    + + + + + + + + + + +
    +
    +

    {{_i}}JS Components{{/i}}

    + + + + + + +
    +
    +

    {{_i}}Miscellaneous{{/i}}

    + + + + +

    {{_i}}Responsive{{/i}}

    + +
    +
    +
    + +
    + +
    +
    + + + + + + +
    +
    + + + + + + +
    +
    +

    {{_i}}Heads up!{{/i}}

    +

    {{_i}}All plugins require the latest version of jQuery to be included.{{/i}}

    +
    +
    +
    + + +
    + +
    +
    +

    {{_i}}Links{{/i}}

    + + + + +

    {{_i}}Colors{{/i}}

    + + + + + + + + + + + + + + +
    +
    +

    {{_i}}Grid system{{/i}}

    + + + + + + +

    {{_i}}Fluid grid system{{/i}}

    + + + + +

    {{_i}}Typography{{/i}}

    + + + + + + +
    +
    +

    {{_i}}Forms{{/i}}

    + + + + +

    {{_i}}Navbar{{/i}}

    + + + + + + + + + + + + +
    +
    +

    {{_i}}Form states & alerts{{/i}}

    + + + + + + + + + + + + + + + + +
    +
    +
    + +
    + +
    + {{_i}}Customize and Download{{/i}} +

    {{_i}}What's included?{{/i}}

    +

    {{_i}}Downloads include compiled CSS, compiled and minified CSS, and compiled jQuery plugins, all nicely packed up into a zipball for your convenience.{{/i}}

    +
    +
    diff --git a/services/web/public/bootstrap/docs/templates/pages/examples.mustache b/services/web/public/bootstrap/docs/templates/pages/examples.mustache new file mode 100644 index 0000000000..485fbc5701 --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/examples.mustache @@ -0,0 +1,31 @@ + +
    +

    {{_i}}Bootstrap examples{{/i}}

    +

    {{_i}}We've included a few basic examples as starting points for your work with Bootstrap. We encourage folks to iterate on these examples and not simply use them as an end result.{{/i}}

    +
    + + +
      +
    • + + + +

      {{_i}}Basic marketing site{{/i}}

      +

      {{_i}}Featuring a hero unit for a primary message and three supporting elements.{{/i}}

      +
    • +
    • + + + +

      {{_i}}Fluid layout{{/i}}

      +

      {{_i}}Uses our new responsive, fluid grid system to create seamless liquid layout.{{/i}}

      +
    • +
    • + + + +

      {{_i}}Starter template{{/i}}

      +

      {{_i}}A barebones HTML document with all the Bootstrap CSS and javascript included.{{/i}}

      +
    • +
    diff --git a/services/web/public/bootstrap/docs/templates/pages/index.mustache b/services/web/public/bootstrap/docs/templates/pages/index.mustache new file mode 100644 index 0000000000..c95e85362c --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/index.mustache @@ -0,0 +1,135 @@ + +
    +
    +

    {{_i}}Bootstrap, from Twitter{{/i}}

    +

    {{_i}}Simple and flexible HTML, CSS, and Javascript for popular user interface components and interactions.{{/i}}

    +

    + {{_i}}View project on GitHub{{/i}} + {{_i}}Download Bootstrap{{/i}} +

    +
    + + +
    + +
    + +
    +

    {{_i}}Designed for everyone, everywhere.{{/i}}

    + +
    +
    + +

    {{_i}}Built for and by nerds{{/i}}

    +

    {{_i}}Like you, we love building awesome products on the web. We love it so much, we decided to help people just like us do it easier, better, and faster. Bootstrap is built for you.{{/i}}

    +
    +
    + +

    {{_i}}For all skill levels{{/i}}

    +

    {{_i}}Bootstrap is designed to help people of all skill levels—designer or developer, huge nerd or early beginner. Use it as a complete kit or use to start something more complex.{{/i}}

    +
    +
    + +

    {{_i}}Cross-everything{{/i}}

    +

    {{_i}}Originally built with only modern browsers in mind, Bootstrap has evolved to include support for all major browsers (even IE7!) and, with Bootstrap 2, tablets and smartphones, too.{{/i}}

    +
    +
    +
    +
    + +

    {{_i}}12-column grid{{/i}}

    +

    {{_i}}Grid systems aren't everything, but having a durable and flexible one at the core of your work can make development much simpler. Use our built-in grid classes or roll your own.{{/i}}

    +
    +
    + +

    {{_i}}Responsive design{{/i}}

    +

    {{_i}}With Bootstrap 2, we've gone fully responsive. Our components are scaled according to a range of resolutions and devices to provide a consistent experience, no matter what.{{/i}}

    +
    +
    + +

    {{_i}}Styleguide docs{{/i}}

    +

    {{_i}}Unlike other front-end toolkits, Bootstrap was designed first and foremost as a styleguide to document not only our features, but best practices and living, coded examples.{{/i}}

    +
    +
    +
    +
    + +

    {{_i}}Growing library{{/i}}

    +

    {{_i}}Despite being only 10kb (gzipped), Bootstrap is one of the most complete front-end toolkits out there with dozens of fully functional components ready to be put to use.{{/i}}

    +
    +
    + +

    {{_i}}Custom jQuery plugins{{/i}}

    +

    {{_i}}What good is an awesome design component without easy-to-use, proper, and extensible interactions? With Bootstrap, you get custom-built jQuery plugins to bring your projects to life.{{/i}}

    +
    +
    + +

    {{_i}}Built on LESS{{/i}}

    +

    {{_i}}Where vanilla CSS falters, LESS excels. Variables, nesting, operations, and mixins in LESS makes coding CSS faster and more efficient with minimal overhead.{{/i}}

    +
    +
    +
    +
    + +

    HTML5

    +

    {{_i}}Built to support new HTML5 elements and syntax.{{/i}}

    +
    +
    + +

    CSS3

    +

    {{_i}}Progressively enhanced components for ultimate style.{{/i}}

    +
    +
    + +

    {{_i}}Open-source{{/i}}

    +

    {{_i}}Built for and maintained by the community via GitHub.{{/i}}

    +
    +
    + +

    {{_i}}Made at Twitter{{/i}}

    +

    {{_i}}Brought to you by an experienced engineer and designer.{{/i}}

    +
    +
    + +
    + +

    {{_i}}Built with Bootstrap.{{/i}}

    + + + +
    \ No newline at end of file diff --git a/services/web/public/bootstrap/docs/templates/pages/javascript.mustache b/services/web/public/bootstrap/docs/templates/pages/javascript.mustache new file mode 100644 index 0000000000..012e195f59 --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/javascript.mustache @@ -0,0 +1,1363 @@ + +
    +

    {{_i}}Javascript for Bootstrap{{/i}}

    +

    {{_i}}Bring Bootstrap's components to life—now with 12 custom jQuery plugins.{{/i}} +

    +
    + + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    {{_i}}Heads up!{{/i}} {{_i}}All javascript plugins require the latest version of jQuery.{{/i}}
    +
    + + + + +
    + +
    +
    +

    {{_i}}About modals{{/i}}

    +

    {{_i}}A streamlined, but flexible, take on the traditional javascript modal plugin with only the minimum required functionality and smart defaults.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Static example{{/i}}

    +

    {{_i}}Below is a statically rendered modal.{{/i}}

    + + +

    {{_i}}Live demo{{/i}}

    +

    {{_i}}Toggle a modal via javascript by clicking the button below. It will slide down and fade in from the top of the page.{{/i}}

    + + + {{_i}}Launch demo modal{{/i}} + +
    + +

    {{_i}}Using bootstrap-modal{{/i}}

    +

    {{_i}}Call the modal via javascript:{{/i}}

    +
    $('#myModal').modal(options)
    +

    {{_i}}Options{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}type{{/i}}{{_i}}default{{/i}}{{_i}}description{{/i}}
    {{_i}}backdrop{{/i}}{{_i}}boolean{{/i}}{{_i}}true{{/i}}{{_i}}Includes a modal-backdrop element{{/i}}
    {{_i}}keyboard{{/i}}{{_i}}boolean{{/i}}{{_i}}true{{/i}}{{_i}}Closes the modal when escape key is pressed{{/i}}
    {{_i}}show{{/i}}{{_i}}boolean{{/i}}{{_i}}true{{/i}}{{_i}}Shows the modal when initialized.{{/i}}
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}You can activate modals on your page easily without having to write a single line of javascript. Just set data-toggle="modal" on a controller element with a data-target="#foo" or href="#foo" which corresponds to a modal element id, and when clicked, it will launch your modal.

    +

    Also, to add options to your modal instance, just include them as additional data attributes on either the control element or the modal markup itself.{{/i}}

    +
    +<a class="btn" data-toggle="modal" href="#myModal" >{{_i}}Launch Modal{{/i}}</a>
    +
    + +
    +<div class="modal">
    +  <div class="modal-header">
    +    <a class="close" data-dismiss="modal">×</a>
    +    <h3>Modal header</h3>
    +  </div>
    +  <div class="modal-body">
    +    <p>{{_i}}One fine body…{{/i}}</p>
    +  </div>
    +  <div class="modal-footer">
    +    <a href="#" class="btn btn-primary">{{_i}}Save changes{{/i}}</a>
    +    <a href="#" class="btn">{{_i}}Close{{/i}}</a>
    +  </div>
    +</div>
    +
    +
    + {{_i}}Heads up!{{/i}} {{_i}}If you want your modal to animate in and out, just add a .fade class to the .modal element (refer to the demo to see this in action) and include bootstrap-transition.js.{{/i}} +
    + Methods{{/i}} +

    .modal({{_i}}options{{/i}})

    +

    {{_i}}Activates your content as a modal. Accepts an optional options object.{{/i}}

    +
    +$('#myModal').modal({
    +  keyboard: false
    +})
    +

    .modal('toggle')

    +

    {{_i}}Manually toggles a modal.{{/i}}

    +
    $('#myModal').modal('toggle')
    +

    .modal('show')

    +

    {{_i}}Manually opens a modal.{{/i}}

    +
    $('#myModal').modal('show')
    +

    .modal('hide')

    +

    {{_i}}Manually hides a modal.{{/i}}

    +
    $('#myModal').modal('hide')
    +

    {{_i}}Events{{/i}}

    +

    {{_i}}Bootstrap's modal class exposes a few events for hooking into modal functionality.{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Event{{/i}}{{_i}}Description{{/i}}
    {{_i}}show{{/i}}{{_i}}This event fires immediately when the show instance method is called.{{/i}}
    {{_i}}shown{{/i}}{{_i}}This event is fired when the modal has been made visible to the user (will wait for css transitions to complete).{{/i}}
    {{_i}}hide{{/i}}{{_i}}This event is fired immediately when the hide instance method has been called.{{/i}}
    {{_i}}hidden{{/i}}{{_i}}This event is fired when the modal has finished being hidden from the user (will wait for css transitions to complete).{{/i}}
    + +
    +$('#myModal').on('hidden', function () {
    +  // {{_i}}do something…{{/i}}
    +})
    +
    +
    +
    + + + + + + + + + +
    + +
    +
    +

    {{_i}}The ScrollSpy plugin is for automatically updating nav targets based on scroll position.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example navbar with scrollspy{{/i}}

    +

    {{_i}}Scroll the area below and watch the navigation update. The dropdown sub items will be highlighted as well. Try it!{{/i}}

    + +
    +

    @fat

    +

    + Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat. +

    +

    @mdo

    +

    + Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard. Freegan beard aliqua cupidatat mcsweeney's vero. Cupidatat four loko nisi, ea helvetica nulla carles. Tattooed cosby sweater food truck, mcsweeney's quis non freegan vinyl. Lo-fi wes anderson +1 sartorial. Carles non aesthetic exercitation quis gentrify. Brooklyn adipisicing craft beer vice keytar deserunt. +

    +

    one

    +

    + Occaecat commodo aliqua delectus. Fap craft beer deserunt skateboard ea. Lomo bicycle rights adipisicing banh mi, velit ea sunt next level locavore single-origin coffee in magna veniam. High life id vinyl, echo park consequat quis aliquip banh mi pitchfork. Vero VHS est adipisicing. Consectetur nisi DIY minim messenger bag. Cred ex in, sustainable delectus consectetur fanny pack iphone. +

    +

    two

    +

    + In incididunt echo park, officia deserunt mcsweeney's proident master cleanse thundercats sapiente veniam. Excepteur VHS elit, proident shoreditch +1 biodiesel laborum craft beer. Single-origin coffee wayfarers irure four loko, cupidatat terry richardson master cleanse. Assumenda you probably haven't heard of them art party fanny pack, tattooed nulla cardigan tempor ad. Proident wolf nesciunt sartorial keffiyeh eu banh mi sustainable. Elit wolf voluptate, lo-fi ea portland before they sold out four loko. Locavore enim nostrud mlkshk brooklyn nesciunt. +

    +

    three

    +

    + Ad leggings keytar, brunch id art party dolor labore. Pitchfork yr enim lo-fi before they sold out qui. Tumblr farm-to-table bicycle rights whatever. Anim keffiyeh carles cardigan. Velit seitan mcsweeney's photo booth 3 wolf moon irure. Cosby sweater lomo jean shorts, williamsburg hoodie minim qui you probably haven't heard of them et cardigan trust fund culpa biodiesel wes anderson aesthetic. Nihil tattooed accusamus, cred irony biodiesel keffiyeh artisan ullamco consequat. +

    +

    Keytar twee blog, culpa messenger bag marfa whatever delectus food truck. Sapiente synth id assumenda. Locavore sed helvetica cliche irony, thundercats you probably haven't heard of them consequat hoodie gluten-free lo-fi fap aliquip. Labore elit placeat before they sold out, terry richardson proident brunch nesciunt quis cosby sweater pariatur keffiyeh ut helvetica artisan. Cardigan craft beer seitan readymade velit. VHS chambray laboris tempor veniam. Anim mollit minim commodo ullamco thundercats. +

    +
    +
    +

    {{_i}}Using bootstrap-scrollspy.js{{/i}}

    +

    {{_i}}Call the scrollspy via javascript:{{/i}}

    +
    $('#navbar').scrollspy()
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}To easily add scrollspy behavior to your topbar navigation, just add data-spy="scroll" to the element you want to spy on (most typically this would be the body).{{/i}}

    +
    <body data-spy="scroll" >...</body>
    +
    + {{_i}}Heads up!{{/i}} + {{_i}}Navbar links must have resolvable id targets. For example, a <a href="#home">home</a> must correspond to something in the dom like <div id="home"></div>.{{/i}} +
    +

    {{_i}}Options{{/i}}

    + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}type{{/i}}{{_i}}default{{/i}}{{_i}}description{{/i}}
    {{_i}}offset{{/i}}{{_i}}number{{/i}}{{_i}}10{{/i}}{{_i}}Pixels to offset from top when calculating position of scroll.{{/i}}
    +
    +
    +
    + + + + +
    + +
    +
    +

    {{_i}}This plugin adds quick, dynamic tab and pill functionality for transitioning through local content.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example tabs{{/i}}

    +

    {{_i}}Click the tabs below to toggle between hidden panes, even via dropdown menus.{{/i}}

    + +
    +
    +

    Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

    +
    +
    +

    Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.

    +
    + + +
    +
    +

    {{_i}}Using bootstrap-tab.js{{/i}}

    +

    {{_i}}Enable tabbable tabs via javascript:{{/i}}

    +
    $('#myTab').tab('show')
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}You can activate a tab or pill navigation without writing any javascript by simply specifying data-toggle="tab" or data-toggle="pill" on an element.{{/i}}

    +
    +<ul class="nav nav-tabs">
    +  <li><a href="#home" data-toggle="tab">{{_i}}Home{{/i}}</a></li>
    +  <li><a href="#profile" data-toggle="tab">{{_i}}Profile{{/i}}</a></li>
    +  <li><a href="#messages" data-toggle="tab">{{_i}}Messages{{/i}}</a></li>
    +  <li><a href="#settings" data-toggle="tab">{{_i}}Settings{{/i}}</a></li>
    +</ul>
    +

    {{_i}}Methods{{/i}}

    +

    $().tab

    +

    + {{_i}}Activates a tab element and content container. Tab should have either a `data-target` or an `href` targeting a container node in the dom.{{/i}} +

    +
    +<ul class="nav nav-tabs">
    +  <li class="active"><a href="#home">{{_i}}Home{{/i}}</a></li>
    +  <li><a href="#profile">{{_i}}Profile{{/i}}</a></li>
    +  <li><a href="#messages">{{_i}}Messages{{/i}}</a></li>
    +  <li><a href="#settings">{{_i}}Settings{{/i}}</a></li>
    +</ul>
    +
    +<div class="tab-content">
    +  <div class="tab-pane active" id="home">...</div>
    +  <div class="tab-pane" id="profile">...</div>
    +  <div class="tab-pane" id="messages">...</div>
    +  <div class="tab-pane" id="settings">...</div>
    +</div>
    +
    +<script>
    +  $(function () {
    +    $('.tabs a:last').tab('show')
    +  })
    +</script>
    +

    {{_i}}Events{{/i}}

    + + + + + + + + + + + + + + + + + +
    {{_i}}Event{{/i}}{{_i}}Description{{/i}}
    {{_i}}show{{/i}}{{_i}}This event fires on tab show, but before the new tab has been shown. Use event.target and event.relatedTarget to target the active tab and the previous active tab (if available) respectively.{{/i}}
    {{_i}}shown{{/i}}{{_i}}This event fires on tab show after a tab has been shown. Use event.target and event.relatedTarget to target the active tab and the previous active tab (if available) respectively.{{/i}}
    + +
    +$('a[data-toggle="tab"]').on('shown', function (e) {
    +  e.target // activated tab
    +  e.relatedTarget // previous tab
    +})
    +
    +
    +
    + + + +
    + +
    +
    +

    {{_i}}About Tooltips{{/i}}

    +

    {{_i}}Inspired by the excellent jQuery.tipsy plugin written by Jason Frame; Tooltips are an updated version, which don't rely on images, use css3 for animations, and data-attributes for local title storage.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example use of Tooltips{{/i}}

    +

    {{_i}}Hover over the links below to see tooltips:{{/i}}

    +
    +

    {{_i}}Tight pants next level keffiyeh you probably haven't heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's fixie sustainable quinoa 8-bit american apparel have a terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar, scenester farm-to-table banksy Austin twitter handle freegan cred raw denim single-origin coffee viral.{{/i}} +

    +
    +
    +

    {{_i}}Using{{/i}} bootstrap-tooltip.js

    +

    {{_i}}Trigger the tooltip via javascript:{{/i}}

    +
    $('#example').tooltip({{_i}}options{{/i}})
    +

    {{_i}}Options{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}type{{/i}}{{_i}}default{{/i}}{{_i}}description{{/i}}
    {{_i}}animation{{/i}}{{_i}}boolean{{/i}}true{{_i}}apply a css fade transition to the tooltip{{/i}}
    {{_i}}placement{{/i}}{{_i}}string{{/i}}'top'{{_i}}how to position the tooltip{{/i}} - top | bottom | left | right
    {{_i}}selector{{/i}}{{_i}}string{{/i}}false{{_i}}If a selector is provided, tooltip objects will be delegated to the specified targets.{{/i}}
    {{_i}}title{{/i}}{{_i}}string | function{{/i}}''{{_i}}default title value if `title` tag isn't present{{/i}}
    {{_i}}trigger{{/i}}{{_i}}string{{/i}}'hover'{{_i}}how tooltip is triggered{{/i}} - hover | focus | manual
    {{_i}}delay{{/i}}{{_i}}number | object{{/i}}0 +

    {{_i}}delay showing and hiding the tooltip (ms){{/i}}

    +

    {{_i}}If a number is supplied, delay is applied to both hide/show{{/i}}

    +

    {{_i}}Object structure is: delay: { show: 500, hide: 100 }{{/i}}

    +
    +
    + {{_i}}Heads up!{{/i}} + {{_i}}Options for individual tooltips can alternatively be specified through the use of data attributes.{{/i}} +
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}For performance reasons, the Tooltip and Popover data-apis are opt in. If you would like to use them just specify a selector option.{{/i}}

    +
    +<a href="#" rel="tooltip" title="{{_i}}first tooltip{{/i}}">{{_i}}hover over me{{/i}}</a>
    +
    +

    {{_i}}Methods{{/i}}

    +

    $().tooltip({{_i}}options{{/i}})

    +

    {{_i}}Attaches a tooltip handler to an element collection.{{/i}}

    +

    .tooltip('show')

    +

    {{_i}}Reveals an elements tooltip.{{/i}}

    +
    $('#element').tooltip('show')
    +

    .tooltip('hide')

    +

    {{_i}}Hides an elements tooltip.{{/i}}

    +
    $('#element').tooltip('hide')
    +

    .tooltip('toggle')

    +

    {{_i}}Toggles an elements tooltip.{{/i}}

    +
    $('#element').tooltip('toggle')
    +
    +
    +
    + + + + +
    + +
    +
    +

    {{_i}}About popovers{{/i}}

    +

    {{_i}}Add small overlays of content, like those on the iPad, to any element for housing secondary information.{{/i}}

    +

    * {{_i}}Requires Tooltip to be included{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example hover popover{{/i}}

    +

    {{_i}}Hover over the button to trigger the popover.{{/i}}

    + +
    +

    {{_i}}Using bootstrap-popover.js{{/i}}

    +

    {{_i}}Enable popovers via javascript:{{/i}}

    +
    $('#example').popover({{_i}}options{{/i}})
    +

    {{_i}}Options{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}type{{/i}}{{_i}}default{{/i}}{{_i}}description{{/i}}
    {{_i}}animation{{/i}}{{_i}}boolean{{/i}}true{{_i}}apply a css fade transition to the tooltip{{/i}}
    {{_i}}placement{{/i}}{{_i}}string{{/i}}'right'{{_i}}how to position the popover{{/i}} - top | bottom | left | right
    {{_i}}selector{{/i}}{{_i}}string{{/i}}false{{_i}}if a selector is provided, tooltip objects will be delegated to the specified targets{{/i}}
    {{_i}}trigger{{/i}}{{_i}}string{{/i}}'hover'{{_i}}how tooltip is triggered{{/i}} - hover | focus | manual
    {{_i}}title{{/i}}{{_i}}string | function{{/i}}''{{_i}}default title value if `title` attribute isn't present{{/i}}
    {{_i}}content{{/i}}{{_i}}string | function{{/i}}''{{_i}}default content value if `data-content` attribute isn't present{{/i}}
    {{_i}}delay{{/i}}{{_i}}number | object{{/i}}0 +

    {{_i}}delay showing and hiding the popover (ms){{/i}}

    +

    {{_i}}If a number is supplied, delay is applied to both hide/show{{/i}}

    +

    {{_i}}Object structure is: delay: { show: 500, hide: 100 }{{/i}}

    +
    +
    + {{_i}}Heads up!{{/i}} + {{_i}}Options for individual popovers can alternatively be specified through the use of data attributes.{{/i}} +
    +

    {{_i}}Markup{{/i}}

    +

    + {{_i}}For performance reasons, the Tooltip and Popover data-apis are opt in. If you would like to use them just specify a the selector option.{{/i}} +

    +

    {{_i}}Methods{{/i}}

    +

    $().popover({{_i}}options{{/i}})

    +

    {{_i}}Initializes popovers for an element collection.{{/i}}

    +

    .popover('show')

    +

    {{_i}}Reveals an elements popover.{{/i}}

    +
    $('#element').popover('show')
    +

    .popover('hide')

    +

    {{_i}}Hides an elements popover.{{/i}}

    +
    $('#element').popover('hide')
    +

    .popover('toggle')

    +

    {{_i}}Toggles an elements popover.{{/i}}

    +
    $('#element').popover('toggle')
    +
    +
    +
    + + + + +
    + +
    +
    +

    {{_i}}About alerts{{/i}}

    +

    {{_i}}The alert plugin is a tiny class for adding close functionality to alerts.{{/i}}

    + {{_i}}Download{{/i}} +
    +
    +

    {{_i}}Example alerts{{/i}}

    +

    {{_i}}The alerts plugin works on regular alert messages, and block messages.{{/i}}

    +
    + × + {{_i}}Holy guacamole!{{/i}} {{_i}}Best check yo self, you're not looking too good.{{/i}} +
    +
    + × +

    {{_i}}Oh snap! You got an error!{{/i}}

    +

    {{_i}}Change this and that and try again. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Cras mattis consectetur purus sit amet fermentum.{{/i}}

    +

    + {{_i}}Take this action{{/i}} {{_i}}Or do this{{/i}} +

    +
    +
    +

    {{_i}}Using bootstrap-alerts.js{{/i}}

    +

    {{_i}}Enable dismissal of an alert via javascript:{{/i}}

    +
    $(".alert").alert()
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}Just add data-dismiss="alert" to your close button to automatically give an alert close functionality.{{/i}}

    +
    <a class="close" data-dismiss="alert" href="#">&times;</a>
    +

    {{_i}}Methods{{/i}}

    +

    $().alert()

    +

    {{_i}}Wraps all alerts with close functionality. To have your alerts animate out when closed, make sure they have the .fade and .in class already applied to them.{{/i}}

    +

    .alert('close')

    +

    {{_i}}Closes an alert.{{/i}}

    +
    $(".alert").alert('close')
    +

    {{_i}}Events{{/i}}

    +

    {{_i}}Bootstrap's alert class exposes a few events for hooking into alert functionality.{{/i}}

    + + + + + + + + + + + + + + + + + +
    {{_i}}Event{{/i}}{{_i}}Description{{/i}}
    {{_i}}close{{/i}}{{_i}}This event fires immediately when the close instance method is called.{{/i}}
    {{_i}}closed{{/i}}{{_i}}This event is fired when the alert has been closed (will wait for css transitions to complete).{{/i}}
    +
    +$('#my-alert').bind('closed', function () {
    +  // {{_i}}do something…{{/i}}
    +})
    +
    +
    +
    + + + + +
    + +
    +
    +

    {{_i}}About{{/i}}

    +

    {{_i}}Do more with buttons. Control button states or create groups of buttons for more components like toolbars.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example uses{{/i}}

    +

    {{_i}}Use the buttons plugin for states and toggles.{{/i}}

    + + + + + + + + + + + + + + + + + + + +
    {{_i}}Stateful{{/i}} + +
    {{_i}}Single toggle{{/i}} + +
    {{_i}}Checkbox{{/i}} +
    + + + +
    +
    {{_i}}Radio{{/i}} +
    + + + +
    +
    +
    +

    {{_i}}Using bootstrap-button.js{{/i}}

    +

    {{_i}}Enable buttons via javascript:{{/i}}

    +
    $('.tabs').button()
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}Data attributes are integral to the button plugin. Check out the example code below for the various markup types.{{/i}}

    +
    +<!-- {{_i}}Add data-toggle="button" to activate toggling on a single button{{/i}} -->
    +<button class="btn" data-toggle="button">Single Toggle</button>
    +
    +<!-- {{_i}}Add data-toggle="buttons-checkbox" for checkbox style toggling on btn-group{{/i}} -->
    +<div class="btn-group" data-toggle="buttons-checkbox">
    +  <button class="btn">Left</button>
    +  <button class="btn">Middle</button>
    +  <button class="btn">Right</button>
    +</div>
    +
    +<!-- {{_i}}Add data-toggle="buttons-radio" for radio style toggling on btn-group{{/i}} -->
    +<div class="btn-group" data-toggle="buttons-radio">
    +  <button class="btn">Left</button>
    +  <button class="btn">Middle</button>
    +  <button class="btn">Right</button>
    +</div>
    +
    +

    {{_i}}Methods{{/i}}

    +

    $().button('toggle')

    +

    {{_i}}Toggles push state. Gives btn the look that it hass been activated.{{/i}}

    +
    + {{_i}}Heads up!{{/i}} + {{_i}}You can enable auto toggling of a button by using the data-toggle attribute.{{/i}} +
    +
    <button class="btn" data-toggle="button" >…</button>
    +

    $().button('loading')

    +

    {{_i}}Sets button state to loading - disables button and swaps text to loading text. Loading text should be defined on the button element using the data attribute data-loading-text.{{/i}} +

    +
    <button class="btn" data-loading-text="loading stuff..." >...</button>
    +
    + {{_i}}Heads up!{{/i}} + {{_i}}Firefox persists the disabled state across page loads. A workaround for this is to use autocomplete="off".{{/i}} +
    +

    $().button('reset')

    +

    {{_i}}Resets button state - swaps text to original text.{{/i}}

    +

    $().button(string)

    +

    {{_i}}Resets button state - swaps text to any data defined text state.{{/i}}

    +
    <button class="btn" data-complete-text="finished!" >...</button>
    +<script>
    +  $('.btn').button('complete')
    +</script>
    +
    +
    +
    + + + + +
    + +
    +
    +

    {{_i}}About{{/i}}

    +

    {{_i}}Get base styles and flexible support for collapsible components like accordions and navigation.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example accordion{{/i}}

    +

    {{_i}}Using the collapse plugin, we built a simple accordion style widget:{{/i}}

    + +
    +
    + +
    +
    + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. +
    +
    +
    +
    + +
    +
    + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. +
    +
    +
    +
    + +
    +
    + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. +
    +
    +
    +
    + + +
    +

    {{_i}}Using bootstrap-collapse.js{{/i}}

    +

    {{_i}}Enable via javascript:{{/i}}

    +
    $(".collapse").collapse()
    +

    {{_i}}Options{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}type{{/i}}{{_i}}default{{/i}}{{_i}}description{{/i}}
    {{_i}}parent{{/i}}{{_i}}selector{{/i}}false{{_i}}If selector then all collapsible elements under the specified parent will be closed when this collapsible item is shown. (similar to traditional accordion behavior){{/i}}
    {{_i}}toggle{{/i}}{{_i}}boolean{{/i}}true{{_i}}Toggles the collapsible element on invocation{{/i}}
    +

    {{_i}}Markup{{/i}}

    +

    {{_i}}Just add data-toggle="collapse" and a data-target to element to automatically assign control of a collapsible element. The data-target attribute accepts a css selector to apply the collapse to. Be sure to add the class collapse to the collapsible element. If you'd like it to default open, add the additional class in.{{/i}}

    +
    +<button class="btn btn-danger" data-toggle="collapse" data-target="#demo">
    +  {{_i}}simple collapsible{{/i}}
    +</button>
    +
    +<div id="demo" class="collapse in"> … </div>
    +
    + {{_i}}Heads up!{{/i}} + {{_i}}To add accordion-like group management to a collapsible control, add the data attribute data-parent="#selector". Refer to the demo to see this in action.{{/i}} +
    +

    {{_i}}Methods{{/i}}

    +

    .collapse({{_i}}options{{/i}})

    +

    {{_i}}Activates your content as a collapsible element. Accepts an optional options object.{{/i}} +

    +$('#myCollapsible').collapse({
    +  toggle: false
    +})
    +

    .collapse('toggle')

    +

    {{_i}}Toggles a collapsible element to shown or hidden.{{/i}}

    +

    .collapse('show')

    +

    {{_i}}Shows a collapsible element.{{/i}}

    +

    .collapse('hide')

    +

    {{_i}}Hides a collapsible element.{{/i}}

    +

    {{_i}}Events{{/i}}

    +

    + {{_i}}Bootstrap's collapse class exposes a few events for hooking into collapse functionality.{{/i}} +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Event{{/i}}{{_i}}Description{{/i}}
    {{_i}}show{{/i}}{{_i}}This event fires immediately when the show instance method is called.{{/i}}
    {{_i}}shown{{/i}}{{_i}}This event is fired when a collapse element has been made visible to the user (will wait for css transitions to complete).{{/i}}
    {{_i}}hide{{/i}} + {{_i}}This event is fired immediately when the hide method has been called.{{/i}} +
    {{_i}}hidden{{/i}}{{_i}}This event is fired when a collapse element has been hidden from the user (will wait for css transitions to complete).{{/i}}
    + +
    +$('#myCollapsible').on('hidden', function () {
    +  // {{_i}}do something…{{/i}}
    +})
    +
    +
    +
    + + + + + + + + + +
    + +
    +
    +

    {{_i}}About{{/i}}

    +

    {{_i}}A basic, easily extended plugin for quickly creating elegant typeaheads with any form text input.{{/i}}

    + {{_i}}Download file{{/i}} +
    +
    +

    {{_i}}Example{{/i}}

    +

    {{_i}}Start typing in the field below to show the typeahead results.{{/i}}

    +
    + +
    +
    +

    {{_i}}Using bootstrap-typeahead.js{{/i}}

    +

    {{_i}}Call the typeahead via javascript:{{/i}}

    +
    $('.typeahead').typeahead()
    +

    {{_i}}Options{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Name{{/i}}{{_i}}type{{/i}}{{_i}}default{{/i}}{{_i}}description{{/i}}
    {{_i}}source{{/i}}{{_i}}array{{/i}}[ ]{{_i}}The data source to query against.{{/i}}
    {{_i}}items{{/i}}{{_i}}number{{/i}}8{{_i}}The max number of items to display in the dropdown.{{/i}}
    {{_i}}matcher{{/i}}{{_i}}function{{/i}}{{_i}}case insensitive{{/i}}{{_i}}The method used to determine if a query matches an item. Accepts a single argument, the item against which to test the query. Access the current query with this.query. Return a boolean true if query is a match.{{/i}}
    {{_i}}sorter{{/i}}{{_i}}function{{/i}}{{_i}}exact match,
    case sensitive,
    case insensitive{{/i}}
    {{_i}}Method used to sort autocomplete results. Accepts a single argument items and has the scope of the typeahead instance. Reference the current query with this.query.{{/i}}
    {{_i}}highlighter{{/i}}{{_i}}function{{/i}}{{_i}}highlights all default matches{{/i}}{{_i}}Method used to highlight autocomplete results. Accepts a single argument item and has the scope of the typeahead instance. Should return html.{{/i}}
    + +

    {{_i}}Markup{{/i}}

    +

    {{_i}}Add data attributes to register an element with typeahead functionality.{{/i}}

    +
    +<input type="text" data-provide="typeahead">
    +
    +

    {{_i}}Methods{{/i}}

    +

    .typeahead({{_i}}options{{/i}})

    +

    {{_i}}Initializes an input with a typeahead.{{/i}}

    +
    +
    +
    \ No newline at end of file diff --git a/services/web/public/bootstrap/docs/templates/pages/less.mustache b/services/web/public/bootstrap/docs/templates/pages/less.mustache new file mode 100644 index 0000000000..742b166d27 --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/less.mustache @@ -0,0 +1,681 @@ + +
    +

    {{_i}}Using LESS with Bootstrap{{/i}}

    +

    {{_i}}Customize and extend Bootstrap with LESS, a CSS preprocessor, to take advantage of the variables, mixins, and more used to build Bootstrap's CSS.{{/i}}

    + +
    + + + + +
    + +
    +
    +

    {{_i}}Why LESS?{{/i}}

    +

    {{_i}}Bootstrap is made with LESS at its core, a dynamic stylesheet language created by our good friend, Alexis Sellier. It makes developing systems-based CSS faster, easier, and more fun.{{/i}}

    +
    +
    +

    {{_i}}What's included?{{/i}}

    +

    {{_i}}As an extension of CSS, LESS includes variables, mixins for reusable snippets of code, operations for simple math, nesting, and even color functions.{{/i}}

    +
    +
    +

    {{_i}}Learn more{{/i}}

    + LESS CSS +

    {{_i}}Visit the official website at http://lesscss.org to learn more.{{/i}}

    +
    +
    +
    +
    +

    {{_i}}Variables{{/i}}

    +

    {{_i}}Managing colors and pixel values in CSS can be a bit of a pain, usually full of copy and paste. Not with LESS though—assign colors or pixel values as variables and change them once.{{/i}}

    +
    +
    +

    {{_i}}Mixins{{/i}}

    +

    {{_i}}Those three border-radius declarations you need to make in regular ol' CSS? Now they're down to one line with the help of mixins, snippets of code you can reuse anywhere.{{/i}}

    +
    +
    +

    {{_i}}Operations{{/i}}

    +

    {{_i}}Make your grid, leading, and more super flexible by doing the math on the fly with operations. Multiply, divide, add, and subtract your way to CSS sanity.{{/i}}

    +
    +
    +
    + + + + +
    + + +
    +
    +

    {{_i}}Hyperlinks{{/i}}

    + + + + + + + + + + + + + + + +
    @linkColor#08c{{_i}}Default link text color{{/i}}
    @linkColorHoverdarken(@linkColor, 15%){{_i}}Default link text hover color{{/i}}
    +

    {{_i}}Grid system{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + +
    @gridColumns12
    @gridColumnWidth60px
    @gridGutterWidth20px
    @fluidGridColumnWidth6.382978723%
    @fluidGridGutterWidth2.127659574%
    +

    {{_i}}Typography{{/i}}

    + + + + + + + + + + + + + + + +
    @baseFontSize13px
    @baseFontFamily"Helvetica Neue", Helvetica, Arial, sans-serif
    @baseLineHeight18px
    +
    +
    +

    {{_i}}Grayscale colors{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @black#000
    @grayDarker#222
    @grayDark#333
    @gray#555
    @grayLight#999
    @grayLighter#eee
    @white#fff
    +

    {{_i}}Accent colors{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @blue#049cdb
    @green#46a546
    @red#9d261d
    @yellow#ffc40d
    @orange#f89406
    @pink#c3325f
    @purple#7a43b6
    +
    +
    + +

    {{_i}}Components{{/i}}

    +
    +
    +

    {{_i}}Buttons{{/i}}

    + + + + + + + + +
    @primaryButtonBackground@linkColor
    +

    {{_i}}Forms{{/i}}

    + + + + + + + + +
    @placeholderText@grayLight
    +

    {{_i}}Navbar{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @navbarHeight40px
    @navbarBackground@grayDarker
    @navbarBackgroundHighlight@grayDark
    @navbarText@grayLight
    @navbarLinkColor@grayLight
    @navbarLinkColorHover@white
    +
    +
    +

    {{_i}}Form states and alerts{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @warningText#c09853
    @warningBackground#f3edd2
    @errorText#b94a48
    @errorBackground#f2dede
    @successText#468847
    @successBackground#dff0d8
    @infoText#3a87ad
    @infoBackground#d9edf7
    +
    +
    + +
    + + + + +
    + +

    {{_i}}About mixins{{/i}}

    +
    +
    +

    {{_i}}Basic mixins{{/i}}

    +

    {{_i}}A basic mixin is essentially an include or a partial for a snippet of CSS. They're written just like a CSS class and can be called anywhere.{{/i}}

    +
    +.element {
    +  .clearfix();
    +}
    +
    +
    +
    +

    {{_i}}Parametric mixins{{/i}}

    +

    {{_i}}A parametric mixin is just like a basic mixin, but it also accepts parameters (hence the name) with optional default values.{{/i}}

    +
    +.element {
    +  .border-radius(4px);
    +}
    +
    +
    +
    +

    {{_i}}Easily add your own{{/i}}

    +

    {{_i}}Nearly all of Bootstrap's mixins are stored in mixins.less, a wonderful utility .less file that enables you to use a mixin in any of the .less files in the toolkit.{{/i}}

    +

    {{_i}}So, go ahead and use the existing ones or feel free to add your own as you need.{{/i}}

    +
    +
    +

    {{_i}}Included mixins{{/i}}

    +

    {{_i}}Utilities{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Mixin{{/i}}{{_i}}Parameters{{/i}}{{_i}}Usage{{/i}}
    .clearfix()none{{_i}}Add to any parent to clear floats within{{/i}}
    .tab-focus()none{{_i}}Apply the Webkit focus style and round Firefox outline{{/i}}
    .center-block()none{{_i}}Auto center a block-level element using margin: auto{{/i}}
    .ie7-inline-block()none{{_i}}Use in addition to regular display: inline-block to get IE7 support{{/i}}
    .size()@height: 5px, @width: 5px{{_i}}Quickly set the height and width on one line{{/i}}
    .square()@size: 5px{{_i}}Builds on .size() to set the width and height as same value{{/i}}
    .opacity()@opacity: 100{{_i}}Set, in whole numbers, the opacity percentage (e.g., "50" or "75"){{/i}}
    +

    Forms

    + + + + + + + + + + + + + + + +
    {{_i}}Mixin{{/i}}{{_i}}Parameters{{/i}}{{_i}}Usage{{/i}}
    .placeholder()@color: @placeholderText{{_i}}Set the placeholder text color for inputs{{/i}}
    +

    Typography

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Mixin{{/i}}{{_i}}Parameters{{/i}}{{_i}}Usage{{/i}}
    #font > #family > .serif()none{{_i}}Make an element use a serif font stack{{/i}}
    #font > #family > .sans-serif()none{{_i}}Make an element use a sans-serif font stack{{/i}}
    #font > #family > .monospace()none{{_i}}Make an element use a monospace font stack{{/i}}
    #font > .shorthand()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight{{_i}}Easily set font size, weight, and leading{{/i}}
    #font > .serif()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight{{_i}}Set font family to serif, and control size, weight, and leading{{/i}}
    #font > .sans-serif()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight{{_i}}Set font family to sans-serif, and control size, weight, and leading{{/i}}
    #font > .monospace()@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight{{_i}}Set font family to monospace, and control size, weight, and leading{{/i}}
    +

    Grid system

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Mixin{{/i}}{{_i}}Parameters{{/i}}{{_i}}Usage{{/i}}
    .container-fixed()none{{_i}}Provide a fixed-width (set with @siteWidth) container for holding your content{{/i}}
    .columns()@columns: 1{{_i}}Build a grid column that spans any number of columns (defaults to 1 column){{/i}}
    .offset()@columns: 1{{_i}}Offset a grid column with left margin that spans any number of columns{{/i}}
    .gridColumn()none{{_i}}Make an element float like a grid column{{/i}}
    +

    {{_i}}CSS3 properties{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Mixin{{/i}}{{_i}}Parameters{{/i}}{{_i}}Usage{{/i}}
    .border-radius()@radius: 5px{{_i}}Round the corners of an element. Can be a single value or four space-separated values{{/i}}
    .box-shadow()@shadow: 0 1px 3px rgba(0,0,0,.25){{_i}}Add a drop shadow to an element{{/i}}
    .transition()@transition{{_i}}Add CSS3 transition effect (e.g., all .2s linear){{/i}}
    .rotate()@degrees{{_i}}Rotate an element n degrees{{/i}}
    .scale()@ratio{{_i}}Scale an element to n times its original size{{/i}}
    .translate()@x: 0, @y: 0{{_i}}Move an element on the x and y planes{{/i}}
    .background-clip()@clip{{_i}}Crop the background of an element (useful for border-radius){{/i}}
    .background-size()@size{{_i}}Control the size of background images via CSS3{{/i}}
    .box-sizing()@boxmodel{{_i}}Change the box model for an element (e.g., border-box for a full-width input){{/i}}
    .user-select()@select{{_i}}Control cursor selection of text on a page{{/i}}
    .resizable()@direction: both{{_i}}Make any element resizable on the right and bottom{{/i}}
    .content-columns()@columnCount, @columnGap: @gridColumnGutter{{_i}}Make the content of any element use CSS3 columns{{/i}}
    +

    {{_i}}Backgrounds and gradients{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Mixin{{/i}}{{_i}}Parameters{{/i}}{{_i}}Usage{{/i}}
    .#translucent > .background()@color: @white, @alpha: 1{{_i}}Give an element a translucent background color{{/i}}
    .#translucent > .border()@color: @white, @alpha: 1{{_i}}Give an element a translucent border color{{/i}}
    .#gradient > .vertical()@startColor, @endColor{{_i}}Create a cross-browser vertical background gradient{{/i}}
    .#gradient > .horizontal()@startColor, @endColor{{_i}}Create a cross-browser horizontal background gradient{{/i}}
    .#gradient > .directional()@startColor, @endColor, @deg{{_i}}Create a cross-browser directional background gradient{{/i}}
    .#gradient > .vertical-three-colors()@startColor, @midColor, @colorStop, @endColor{{_i}}Create a cross-browser three-color background gradient{{/i}}
    .#gradient > .radial()@innerColor, @outerColor{{_i}}Create a cross-browser radial background gradient{{/i}}
    .#gradient > .striped()@color, @angle{{_i}}Create a cross-browser striped background gradient{{/i}}
    .#gradientBar()@primaryColor, @secondaryColor{{_i}}Used for buttons to assign a gradient and slightly darker border{{/i}}
    +
    + + + + +
    + +
    + {{_i}}Note: If you're submitting a pull request to GitHub with modified CSS, you must recompile the CSS via any of these methods.{{/i}} +
    +

    {{_i}}Tools for compiling{{/i}}

    +
    +
    +

    {{_i}}Node with makefile{{/i}}

    +

    {{_i}}Install the LESS command line compiler globally with npm by running the following command:{{/i}}

    +
    $ npm install -g less
    +

    {{_i}}Once installed just run make from the root of your bootstrap directory and you're all set.{{/i}}

    +

    {{_i}}Additionally, if you have watchr installed, you may run make watch to have bootstrap automatically rebuilt every time you edit a file in the bootstrap lib (this isn't required, just a convenience method).{{/i}}

    +
    +
    +

    {{_i}}Command line{{/i}}

    +

    {{_i}}Install the LESS command line tool via Node and run the following command:{{/i}}

    +
    $ lessc ./lib/bootstrap.less > bootstrap.css
    +

    {{_i}}Be sure to include --compress in that command if you're trying to save some bytes!{{/i}}

    +
    +
    +

    {{_i}}Javascript{{/i}}

    +

    {{_i}}Download the latest Less.js and include the path to it (and Bootstrap) in the <head>.{{/i}}

    +
    +<link rel="stylesheet/less" href="/path/to/bootstrap.less">
    +<script src="/path/to/less.js"></script>
    +
    +

    {{_i}}To recompile the .less files, just save them and reload your page. Less.js compiles them and stores them in local storage.{{/i}}

    +
    +
    +
    +
    +

    {{_i}}Unofficial Mac app{{/i}}

    +

    {{_i}}The unofficial Mac app watches directories of .less files and compiles the code to local files after every save of a watched .less file.{{/i}}

    +

    {{_i}}If you like, you can toggle preferences in the app for automatic minifying and which directory the compiled files end up in.{{/i}}

    +
    +
    +

    {{_i}}More Mac apps{{/i}}

    +

    Crunch

    +

    {{_i}}Crunch is a great looking LESS editor and compiler built on Adobe Air.{{/i}}

    +

    CodeKit

    +

    {{_i}}Created by the same guy as the unofficial Mac app, CodeKit is a Mac app that compiles LESS, SASS, Stylus, and CoffeeScript.{{/i}}

    +

    Simpless

    +

    {{_i}}Mac, Linux, and PC app for drag and drop compiling of LESS files. Plus, the source code is on GitHub.{{/i}}

    +
    +
    +
    diff --git a/services/web/public/bootstrap/docs/templates/pages/scaffolding.mustache b/services/web/public/bootstrap/docs/templates/pages/scaffolding.mustache new file mode 100644 index 0000000000..46437782ba --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/scaffolding.mustache @@ -0,0 +1,328 @@ + +
    +

    {{_i}}Scaffolding{{/i}}

    +

    {{_i}}Bootstrap is built on a responsive 12-column grid. We've also included fixed- and fluid-width layouts based on that system.{{/i}}

    + +
    + + + + +
    + + +

    {{_i}}Default grid{{/i}}

    +
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    1
    +
    +
    +
    4
    +
    4
    +
    4
    +
    +
    +
    4
    +
    8
    +
    +
    +
    6
    +
    6
    +
    +
    +
    12
    +
    +
    +
    +

    {{_i}}The default grid system provided as part of Bootstrap is a 940px-wide, 12-column grid.{{/i}}

    +

    {{_i}}It also has four responsive variations for various devices and resolutions: phone, tablet portrait, table landscape and small desktops, and large widescreen desktops.{{/i}}

    +
    +
    +
    +<div class="row">
    +  <div class="span4">...</div>
    +  <div class="span8">...</div>
    +</div>
    +
    +
    +
    +

    {{_i}}As shown here, a basic layout can be created with two "columns," each spanning a number of the 12 foundational columns we defined as part of our grid system.{{/i}}

    +
    +
    + +
    + +

    {{_i}}Offsetting columns{{/i}}

    +
    +
    4
    +
    4 offset 4
    +
    +
    +
    3 offset 3
    +
    3 offset 3
    +
    +
    +
    8 offset 4
    +
    +
    +<div class="row">
    +  <div class="span4">...</div>
    +  <div class="span4 offset4">...</div>
    +</div>
    +
    + +
    + +

    {{_i}}Nesting columns{{/i}}

    +
    +
    +

    {{_i}}With the static (non-fluid) grid system in Bootstrap, nesting is easy. To nest your content, just add a new .row and set of .span* columns within an existing .span* column.{{/i}}

    +

    {{_i}}Example{{/i}}

    +
    +
    + {{_i}}Level 1 of column{{/i}} +
    +
    + {{_i}}Level 2{{/i}} +
    +
    + {{_i}}Level 2{{/i}} +
    +
    +
    +
    +
    +
    +
    +<div class="row">
    +  <div class="span12">
    +    {{_i}}Level 1 of column{{/i}}
    +    <div class="row">
    +      <div class="span6">{{_i}}Level 2{{/i}}</div>
    +      <div class="span6">{{_i}}Level 2{{/i}}</div>
    +    </div>
    +  </div>
    +</div>
    +
    +
    +
    + +

    {{_i}}Grid customization{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Variable{{/i}}{{_i}}Default value{{/i}}{{_i}}Description{{/i}}
    @gridColumns12{{_i}}Number of columns{{/i}}
    @gridColumnWidth60px{{_i}}Width of each column{{/i}}
    @gridGutterWidth20px{{_i}}Negative space between columns{{/i}}
    @siteWidth{{_i}}Computed sum of all columns and gutters{{/i}}{{_i}}Counts number of columns and gutters to set width of the .container-fixed() mixin{{/i}}
    +
    +
    +

    {{_i}}Variables in LESS{{/i}}

    +

    {{_i}}Built into Bootstrap are a handful of variables for customizing the default 940px grid system, documented above. All variables for the grid are stored in variables.less.{{/i}}

    +
    +
    +

    {{_i}}How to customize{{/i}}

    +

    {{_i}}Modifying the grid means changing the three @grid* variables and recompiling Bootstrap. Change the grid variables in variables.less and use one of the four ways documented to recompile. If you're adding more columns, be sure to add the CSS for those in grid.less.{{/i}}

    +
    +
    +

    {{_i}}Staying responsive{{/i}}

    +

    {{_i}}Customization of the grid only works at the default level, the 940px grid. To maintain the responsive aspects of Bootstrap, you'll also have to customize the grids in responsive.less.{{/i}}

    +
    +
    + +
    + + + + +
    + + +
    +
    +

    {{_i}}Fixed layout{{/i}}

    +

    {{_i}}The default and simple 940px-wide, centered layout for just about any website or page provided by a single <div class="container">.{{/i}}

    +
    +
    +
    +
    +<body>
    +  <div class="container">
    +    ...
    +  </div>
    +</body>
    +
    +
    +
    +

    {{_i}}Fluid layout{{/i}}

    +

    {{_i}}<div class="container-fluid"> gives flexible page structure, min- and max-widths, and a left-hand sidebar. It's great for apps and docs.{{/i}}

    +
    +
    +
    +
    +
    +<div class="container-fluid">
    +  <div class="row-fluid">
    +    <div class="span2">
    +      <!--{{_i}}Sidebar content{{/i}}-->
    +    </div>
    +    <div class="span10">
    +      <!--{{_i}}Body content{{/i}}-->
    +    </div>
    +  </div>
    +</div>
    +
    +
    +
    +
    + + + + + +
    + + +
    +
    + Responsive devices +
    +
    +

    {{_i}}Supported devices{{/i}}

    +

    {{_i}}Bootstrap supports a handful of media queries in a single file to help make your projects more appropriate on different devices and screen resolutions. Here's what's included:{{/i}}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{_i}}Label{{/i}}{{_i}}Layout width{{/i}}{{_i}}Column width{{/i}}{{_i}}Gutter width{{/i}}
    {{_i}}Smartphones{{/i}}480px and below{{_i}}Fluid columns, no fixed widths{{/i}}
    {{_i}}Portrait tablets{{/i}}480px to 768px{{_i}}Fluid columns, no fixed widths{{/i}}
    {{_i}}Landscape tablets{{/i}}768px to 980px42px20px
    {{_i}}Default{{/i}}980px and up60px20px
    {{_i}}Large display{{/i}}1210px and up70px30px
    + +

    {{_i}}Requires meta tag{{/i}}

    +

    {{_i}}To ensure devices display responsive pages properly, include the viewport meta tag.{{/i}}

    +
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    + +

    {{_i}}What they do{{/i}}

    +

    {{_i}}Media queries allow for custom CSS based on a number of conditions—ratios, widths, display type, etc—but usually focuses around min-width and max-width.{{/i}}

    +
      +
    • {{_i}}Modify the width of column in our grid{{/i}}
    • +
    • {{_i}}Stack elements instead of float wherever necessary{{/i}}
    • +
    • {{_i}}Resize headings and text to be more appropriate for devices{{/i}}
    • +
    +
    +
    + +
    + + +

    {{_i}}Using the media queries{{/i}}

    +
    +
    +

    {{_i}}Bootstrap doesn't automatically include these media queries, but understanding and adding them is very easy and requires minimal setup. You have a few options for including the responsive features of Bootstrap:{{/i}}

    +
      +
    1. {{_i}}Use the compiled responsive version, bootstrap-responsive.css{{/i}}
    2. +
    3. {{_i}}Add @import "responsive.less" and recompile Bootstrap{{/i}}
    4. +
    5. {{_i}}Modify and recompile responsive.less as a separate file{{/i}}
    6. +
    +

    {{_i}}Why not just include it? Truth be told, not everything needs to be responsive. Instead of encouraging developers to remove this feature, we figure it best to enable it.{{/i}}

    +
    +
    +
    +  // {{_i}}Landscape phones and down{{/i}}
    +  @media (max-width: 480px) { ... }
    +
    +  // {{_i}}Landscape phone to portrait tablet{{/i}}
    +  @media (max-width: 768px) { ... }
    +
    +  // {{_i}}Portrait tablet to landscape and desktop{{/i}}
    +  @media (min-width: 768px) and (max-width: 980px) { ... }
    +
    +  // {{_i}}Large desktop{{/i}}
    +  @media (min-width: 1200px) { .. }
    +
    +
    +
    +
    diff --git a/services/web/public/bootstrap/docs/templates/pages/upgrading.mustache b/services/web/public/bootstrap/docs/templates/pages/upgrading.mustache new file mode 100644 index 0000000000..77e2e02cd9 --- /dev/null +++ b/services/web/public/bootstrap/docs/templates/pages/upgrading.mustache @@ -0,0 +1,193 @@ + +
    +

    {{_i}}Upgrading to Bootstrap 2{{/i}}

    +

    {{_i}}Learn about significant changes and additions since v1.4 with this handy guide.{{/i}}

    +
    + + + + +
    + +
      +
    • {{_i}}Docs: major updates across the board to general structure, examples, and code snippets. Also made responsive with new media queries.{{/i}}
    • +
    • {{_i}}Docs: all docs pages are now powered by Mustache templates and strings are wrapped in i18n tags for translation by the Twitter Translation Center. All changes to documentation must be done here and then compiled (similar to our CSS and LESS).{{/i}}
    • +
    • {{_i}}Repo directory structure: removed the compiled CSS from the root in favor of a large direct download link on the docs homepage. Compiled CSS is in /docs/assets/css/.{{/i}}
    • +
    • {{_i}}Docs and repo: one makefile, just type make in the Terminal and get updated docs and CSS.{{/i}}
    • +
    +
    + + + + +
    + +

    {{_i}}Grid system{{/i}}

    +
      +
    • {{_i}}Updated grid system, now only 12 columns instead of 16{{/i}} +
    • {{_i}}Responsive approach means your projects virtually work out of the box on smartphones, tablets, and more{{/i}}
    • +
    • {{_i}}Removed unused (by default) grid columns support for 17-24 columns{{/i}}
    • +
    +

    {{_i}}Responsive (media queries){{/i}}

    +
      +
    • {{_i}}Media queries added for basic support across mobile and tablet devices{{/i}} +
    • {{_i}}Responsive CSS is compiled separately, as bootstrap-responsive.css{{/i}}
    • +
    +
    + + + + +
    + +

    {{_i}}Typography{{/i}}

    +
      +
    • {{_i}}h4 elements were dropped from 16px to 14px with a default line-height of 18px{{/i}}
    • +
    • {{_i}}h5 elements were dropped from 14px to 12px{{/i}}
    • +
    • {{_i}}h6 elements were dropped from 13px to 11px{{/i}}
    • +
    • {{_i}}Right-aligned option for blockquotes if float: right;{{/i}}
    • +
    +

    {{_i}}Code{{/i}}

    +
      +
    • {{_i}}New graphical style for <code>{{/i}}
    • +
    • {{_i}}Google Code Prettify styles updated (based on GitHub's gists){{/i}}
    • +
    +

    {{_i}}Tables{{/i}}

    +
      +
    • {{_i}}Improved support for colspan and rowspan{{/i}}
    • +
    • {{_i}}Styles now restricted to new base class, .table{{/i}}
    • +
    • {{_i}}Table classes standardized with .table- required as a prefix{{/i}}
    • +
    • {{_i}}Removed unused table color options (too much code for such little impact){{/i}}
    • +
    • {{_i}}Dropped support for TableSorter{{/i}}
    • +
    +

    {{_i}}Buttons{{/i}}

    +
      +
    • {{_i}}New classes for colors and sizes, all prefixed with .btn-{{/i}}
    • +
    • {{_i}}IE9: removed gradients and added rounded corners{{/i}}
    • +
    • {{_i}}Updated active state to make styling clearer in button groups (new) and look better with custom transition{{/i}}
    • +
    • {{_i}}New mixin, .buttonBackground, to set button gradients{{/i}}
    • +
    • {{_i}}The .secondary class was removed from modal examples in our docs as it never had associated styles.{{/i}}
    • +
    +

    {{_i}}Forms{{/i}}

    +
      +
    • {{_i}}Default form style is now vertical (stacked) to use less CSS and add greater flexibility{{/i}}
    • +
    • {{_i}}Form classes standardized with .form- required as a prefix{{/i}}
    • +
    • {{_i}}New built-in form defaults for search, inline, and horizontal forms{{/i}}
    • +
    • {{_i}}For horizontal forms, previous classes .clearfix and .input are equivalent to the new .control-group and .controls.{{/i}}
    • +
    • {{_i}}More flexible horizontal form markup with classes for all styling, including new optional class for the label{{/i}}
    • +
    • {{_i}}Form states: colors updated and customizable via new LESS variables{{/i}}
    • +
    +

    {{_i}}Icons, by Glyphicons{{/i}}

    +
      +
    • {{_i}}New Glyphicons Halflings icon set added in sprite form, in black and white{{/i}}
    • +
    • {{_i}}Simple markup required for an icon in tons of contexts: <i class="icon-cog"></>{{/i}}
    • +
    • {{_i}}Add another class, .icon-white, for white variation of the same icon{{/i}}
    • +
    +
    + + + + +
    + +

    {{_i}}Button groups and dropdowns{{/i}}

    +
      +
    • {{_i}}Two brand new components in 2.0: button groups and button dropdowns{{/i}}
    • +
    • {{_i}}Dependency: button dropdowns are built on button groups, and therefore require all their styles{{/i}}
    • +
    • {{_i}}Button groups, .btn-group, can be grouped one level higher with a button toolbar, .btn-toolbar{{/i}}
    • +
    +

    {{_i}}Navigation{{/i}}

    +
      +
    • {{_i}}Tabs and pills now require the use of a new base class, .nav, on their <ul> and the class names are now .nav-pills and .nav-tabs.{{/i}}
    • +
    • {{_i}}New nav list variation added that uses the same base class, .nav{{/i}}
    • +
    • {{_i}}Vertical tabs and pills have been added—just add .nav-stacked to the <ul>{{/i}}
    • +
    • {{_i}}Pills were restyled to be less rounded by default{{/i}}
    • +
    • {{_i}}Pills now have dropdown menu support (they share the same markup and styles as tabs){{/i}}
    • +
    +

    {{_i}}Navbar (formerly topbar){{/i}}

    +
      +
    • {{_i}}Base class changed from .topbar to .navbar{{/i}}
    • +
    • {{_i}}Now supports static position (default behavior, not fixed) and fixed to the top of viewport via .navbar-fixed-top (previously only supported fixed){{/i}}
    • +
    • {{_i}}Added vertical dividers to top-level nav{{/i}}
    • +
    • {{_i}}Improved support for inline forms in the navbar, which now require .navbar-form to properly scope styles to only the intended forms.{{/i}}
    • +
    • {{_i}}Navbar search form now requires use of the .navbar-search class and its input the use of .search-query. To position the search form, you must use .pull-left or .pull-right.{{/i}}
    • +
    • {{_i}}Added optional responsive markup for collapsing navbar contents for smaller resolutions and devices. See navbar docs for how to utilize.{{/i}}
    • +
    +

    {{_i}}Dropdown menus{{/i}}

    +
      +
    • {{_i}}Updated the .dropdown-menu to tighten up spacing{{/i}}
    • +
    • {{_i}}Now requires you to add a <span class="caret"></span> to show the dropdown arrow{{/i}}
    • +
    • {{_i}}Now requires you to add a data-toggle="dropdown" attribute to obtain toggling behavior{{/i}}
    • +
    • {{_i}}The navbar (fixed topbar) has brand new dropdowns. Gone are the dark versions and in their place are the standard white ones with an additional caret at their tops for clarity of position.{{/i}}
    • +
    +

    {{_i}}Labels{{/i}}

    +
      +
    • {{_i}}Label colors updated to match form state colors{{/i}}
    • +
    • {{_i}}Not only do they match graphically, but they are powered by the same new variables{{/i}}
    • +
    +

    {{_i}}Thumbnails{{/i}}

    +
      +
    • {{_i}}Formerly .media-grid, now just .thumbnails, we've thoroughly extended this component for more uses while maintaining overall simplicity out of the box.{{/i}}
    • +
    • {{_i}}Individual thumbnails now require .thumbnail class{{/i}}
    • +
    +

    {{_i}}Alerts{{/i}}

    +
      +
    • {{_i}}New base class: .alert instead of .alert-message{{/i}}
    • +
    • {{_i}}Class names standardized for other options, now all starting with .alert-{{/i}}
    • +
    • {{_i}}Redesigned base alert styles to combine the default alerts and block-level alerts into one{{/i}}
    • +
    • {{_i}}Block level alert class changed: .alert-block instead of .block-message{{/i}}
    • +
    +

    {{_i}}Progress bars{{/i}}

    +
      +
    • {{_i}}New in 2.0{{/i}}
    • +
    • {{_i}}Features multiple styles via classes, including striped and animated variations via CSS3{{/i}}
    • +
    +

    {{_i}}Miscellaneous components{{/i}}

    +
      +
    • {{_i}}Added documentation for the well component and the close icon (used in modals and alerts){{/i}}
    • +
    +
    + + + + +
    + +
    + {{_i}}Heads up!{{/i}} {{_i}}We're rewritten just about everything for our plugins, so head on over to the Javascript page to learn more.{{/i}} +
    +

    {{_i}}Tooltips{{/i}}

    +
      +
    • {{_i}}The plugin method has been renamed from twipsy() to tooltip(), and the class name changed from twipsy to tooltip.{{/i}}
    • +
    • {{_i}}The placement option value that was below is now bottom, and above is now top.{{/i}}
    • +
    • {{_i}}The animate option was renamed to animation.{{/i}}
    • +
    • {{_i}}The html option was removed, as the tooltips default to allowing HTML now.{{/i}}
    • +
    +

    {{_i}}Popovers{{/i}}

    +
      +
    • {{_i}}Child elements now properly namespaced: .title to .popover-title, .inner to .popover-inner, and .content to .popover-content.{{/i}}
    • +
    +

    {{_i}}New plugins{{/i}}

    + +
    + diff --git a/services/web/public/bootstrap/docs/upgrading.html b/services/web/public/bootstrap/docs/upgrading.html new file mode 100644 index 0000000000..5ab8a39ba1 --- /dev/null +++ b/services/web/public/bootstrap/docs/upgrading.html @@ -0,0 +1,307 @@ + + + + + Bootstrap, from Twitter + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +

    Upgrading to Bootstrap 2

    +

    Learn about significant changes and additions since v1.4 with this handy guide.

    +
    + + + + +
    + +
      +
    • Docs: major updates across the board to general structure, examples, and code snippets. Also made responsive with new media queries.
    • +
    • Docs: all docs pages are now powered by Mustache templates and strings are wrapped in i18n tags for translation by the Twitter Translation Center. All changes to documentation must be done here and then compiled (similar to our CSS and LESS).
    • +
    • Repo directory structure: removed the compiled CSS from the root in favor of a large direct download link on the docs homepage. Compiled CSS is in /docs/assets/css/.
    • +
    • Docs and repo: one makefile, just type make in the Terminal and get updated docs and CSS.
    • +
    +
    + + + + +
    + +

    Grid system

    +
      +
    • Updated grid system, now only 12 columns instead of 16 +
    • Responsive approach means your projects virtually work out of the box on smartphones, tablets, and more
    • +
    • Removed unused (by default) grid columns support for 17-24 columns
    • +
    +

    Responsive (media queries)

    +
      +
    • Media queries added for basic support across mobile and tablet devices +
    • Responsive CSS is compiled separately, as bootstrap-responsive.css
    • +
    +
    + + + + +
    + +

    Typography

    +
      +
    • h4 elements were dropped from 16px to 14px with a default line-height of 18px
    • +
    • h5 elements were dropped from 14px to 12px
    • +
    • h6 elements were dropped from 13px to 11px
    • +
    • Right-aligned option for blockquotes if float: right;
    • +
    +

    Code

    +
      +
    • New graphical style for <code>
    • +
    • Google Code Prettify styles updated (based on GitHub's gists)
    • +
    +

    Tables

    +
      +
    • Improved support for colspan and rowspan
    • +
    • Styles now restricted to new base class, .table
    • +
    • Table classes standardized with .table- required as a prefix
    • +
    • Removed unused table color options (too much code for such little impact)
    • +
    • Dropped support for TableSorter
    • +
    +

    Buttons

    +
      +
    • New classes for colors and sizes, all prefixed with .btn-
    • +
    • IE9: removed gradients and added rounded corners
    • +
    • Updated active state to make styling clearer in button groups (new) and look better with custom transition
    • +
    • New mixin, .buttonBackground, to set button gradients
    • +
    • The .secondary class was removed from modal examples in our docs as it never had associated styles.
    • +
    +

    Forms

    +
      +
    • Default form style is now vertical (stacked) to use less CSS and add greater flexibility
    • +
    • Form classes standardized with .form- required as a prefix
    • +
    • New built-in form defaults for search, inline, and horizontal forms
    • +
    • For horizontal forms, previous classes .clearfix and .input are equivalent to the new .control-group and .controls.
    • +
    • More flexible horizontal form markup with classes for all styling, including new optional class for the label
    • +
    • Form states: colors updated and customizable via new LESS variables
    • +
    +

    Icons, by Glyphicons

    +
      +
    • New Glyphicons Halflings icon set added in sprite form, in black and white
    • +
    • Simple markup required for an icon in tons of contexts: <i class="icon-cog"></>
    • +
    • Add another class, .icon-white, for white variation of the same icon
    • +
    +
    + + + + +
    + +

    Button groups and dropdowns

    +
      +
    • Two brand new components in 2.0: button groups and button dropdowns
    • +
    • Dependency: button dropdowns are built on button groups, and therefore require all their styles
    • +
    • Button groups, .btn-group, can be grouped one level higher with a button toolbar, .btn-toolbar
    • +
    +

    Navigation

    +
      +
    • Tabs and pills now require the use of a new base class, .nav, on their <ul> and the class names are now .nav-pills and .nav-tabs.
    • +
    • New nav list variation added that uses the same base class, .nav
    • +
    • Vertical tabs and pills have been added—just add .nav-stacked to the <ul>
    • +
    • Pills were restyled to be less rounded by default
    • +
    • Pills now have dropdown menu support (they share the same markup and styles as tabs)
    • +
    +

    Navbar (formerly topbar)

    +
      +
    • Base class changed from .topbar to .navbar
    • +
    • Now supports static position (default behavior, not fixed) and fixed to the top of viewport via .navbar-fixed-top (previously only supported fixed)
    • +
    • Added vertical dividers to top-level nav
    • +
    • Improved support for inline forms in the navbar, which now require .navbar-form to properly scope styles to only the intended forms.
    • +
    • Navbar search form now requires use of the .navbar-search class and its input the use of .search-query. To position the search form, you must use .pull-left or .pull-right.
    • +
    • Added optional responsive markup for collapsing navbar contents for smaller resolutions and devices. See navbar docs for how to utilize.
    • +
    +

    Dropdown menus

    +
      +
    • Updated the .dropdown-menu to tighten up spacing
    • +
    • Now requires you to add a <span class="caret"></span> to show the dropdown arrow
    • +
    • Now requires you to add a data-toggle="dropdown" attribute to obtain toggling behavior
    • +
    • The navbar (fixed topbar) has brand new dropdowns. Gone are the dark versions and in their place are the standard white ones with an additional caret at their tops for clarity of position.
    • +
    +

    Labels

    +
      +
    • Label colors updated to match form state colors
    • +
    • Not only do they match graphically, but they are powered by the same new variables
    • +
    +

    Thumbnails

    +
      +
    • Formerly .media-grid, now just .thumbnails, we've thoroughly extended this component for more uses while maintaining overall simplicity out of the box.
    • +
    • Individual thumbnails now require .thumbnail class
    • +
    +

    Alerts

    +
      +
    • New base class: .alert instead of .alert-message
    • +
    • Class names standardized for other options, now all starting with .alert-
    • +
    • Redesigned base alert styles to combine the default alerts and block-level alerts into one
    • +
    • Block level alert class changed: .alert-block instead of .block-message
    • +
    +

    Progress bars

    +
      +
    • New in 2.0
    • +
    • Features multiple styles via classes, including striped and animated variations via CSS3
    • +
    +

    Miscellaneous components

    +
      +
    • Added documentation for the well component and the close icon (used in modals and alerts)
    • +
    +
    + + + + +
    + +
    + Heads up! We're rewritten just about everything for our plugins, so head on over to the Javascript page to learn more. +
    +

    Tooltips

    +
      +
    • The plugin method has been renamed from twipsy() to tooltip(), and the class name changed from twipsy to tooltip.
    • +
    • The placement option value that was below is now bottom, and above is now top.
    • +
    • The animate option was renamed to animation.
    • +
    • The html option was removed, as the tooltips default to allowing HTML now.
    • +
    +

    Popovers

    +
      +
    • Child elements now properly namespaced: .title to .popover-title, .inner to .popover-inner, and .content to .popover-content.
    • +
    +

    New plugins

    + +
    + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/bootstrap/img/glyphicons-halflings-white.png b/services/web/public/bootstrap/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf6484a29d8da269f9bc874b25493a45fae3bae GIT binary patch literal 8777 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 B7G?kd literal 0 HcmV?d00001 diff --git a/services/web/public/bootstrap/img/glyphicons-halflings.png b/services/web/public/bootstrap/img/glyphicons-halflings.png new file mode 100644 index 0000000000000000000000000000000000000000..a9969993201f9cee63cf9f49217646347297b643 GIT binary patch literal 12799 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 literal 0 HcmV?d00001 diff --git a/services/web/public/bootstrap/js/README.md b/services/web/public/bootstrap/js/README.md new file mode 100644 index 0000000000..3aa09c960f --- /dev/null +++ b/services/web/public/bootstrap/js/README.md @@ -0,0 +1,106 @@ +## 2.0 BOOTSTRAP JS PHILOSOPHY +These are the high-level design rules which guide the development of Bootstrap's plugin apis. + +--- + +### DATA-ATTRIBUTE API + +We believe you should be able to use all plugins provided by Bootstrap purely through the markup API without writing a single line of javascript. + +We acknowledge that this isn't always the most performant and sometimes it may be desirable to turn this functionality off altogether. Therefore, as of 2.0 we provide the ability to disable the data attribute API by unbinding all events on the body namespaced with `'data-api'`. This looks like this: + + $('body').off('.data-api') + +To target a specific plugin, just include the plugins name as a namespace along with the data-api namespace like this: + + $('body').off('.alert.data-api') + +--- + +### PROGRAMATIC API + +We also believe you should be able to use all plugins provided by Bootstrap purely through the JS API. + +All public APIs should be single, chainable methods, and return the collection acted upon. + + $(".btn.danger").button("toggle").addClass("fat") + +All methods should accept an optional options object, a string which targets a particular method, or null which initiates the default behavior: + + $("#myModal").modal() // initialized with defaults + $("#myModal").modal({ keyboard: false }) // initialized with now keyboard + $("#myModal").modal('show') // initializes and invokes show immediately afterqwe2 + +--- + +### OPTIONS + +Options should be sparse and add universal value. We should pick the right defaults. + +All plugins should have a default object which can be modified to affect all instances' default options. The defaults object should be available via `$.fn.plugin.defaults`. + + $.fn.modal.defaults = { … } + +An options definition should take the following form: + + *noun*: *adjective* - describes or modifies a quality of an instance + +examples: + + backdrop: true + keyboard: false + placement: 'top' + +--- + +### EVENTS + +All events should have an infinitive and past participle form. The infinitive is fired just before an action takes place, the past participle on completion of the action. + + show | shown + hide | hidden + +--- + +### CONSTRUCTORS + +Each plugin should expose its raw constructor on a `Constructor` property -- accessed in the following way: + + + $.fn.popover.Constructor + +--- + +### DATA ACCESSOR + +Each plugin stores a copy of the invoked class on an object. This class instance can be accessed directly through jQuery's data API like this: + + $('[rel=popover]').data('popover') instanceof $.fn.popover.Constructor + +--- + +### DATA ATTRIBUTES + +Data attributes should take the following form: + +- data-{{verb}}={{plugin}} - defines main interaction +- data-target || href^=# - defined on "control" element (if element controls an element other than self) +- data-{{noun}} - defines class instance options + +examples: + + // control other targets + data-toggle="modal" data-target="#foo" + data-toggle="collapse" data-target="#foo" data-parent="#bar" + + // defined on element they control + data-spy="scroll" + + data-dismiss="modal" + data-dismiss="alert" + + data-toggle="dropdown" + + data-toggle="button" + data-toggle="buttons-checkbox" + data-toggle="buttons-radio" \ No newline at end of file diff --git a/services/web/public/bootstrap/js/bootstrap-alert.js b/services/web/public/bootstrap/js/bootstrap-alert.js new file mode 100644 index 0000000000..4a65b135ab --- /dev/null +++ b/services/web/public/bootstrap/js/bootstrap-alert.js @@ -0,0 +1,91 @@ +/* ========================================================== + * bootstrap-alert.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function( $ ){ + + "use strict" + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function ( el ) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype = { + + constructor: Alert + + , close: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.trigger('close') + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.removeClass('in') + + function removeElement() { + $parent.remove() + $parent.trigger('closed') + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT DATA-API + * ============== */ + + $(function () { + $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) + }) + +}( window.jQuery ) diff --git a/services/web/public/bootstrap/js/bootstrap-button.js b/services/web/public/bootstrap/js/bootstrap-button.js new file mode 100644 index 0000000000..a3f4657e8d --- /dev/null +++ b/services/web/public/bootstrap/js/bootstrap-button.js @@ -0,0 +1,98 @@ +/* ============================================================ + * bootstrap-button.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + +!function( $ ){ + + "use strict" + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function ( element, options ) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype = { + + constructor: Button + + , setState: function ( state ) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + , toggle: function () { + var $parent = this.$element.parent('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + $.fn.button = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON DATA-API + * =============== */ + + $(function () { + $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { + $(e.currentTarget).button('toggle') + }) + }) + +}( window.jQuery ) diff --git a/services/web/public/bootstrap/js/bootstrap-carousel.js b/services/web/public/bootstrap/js/bootstrap-carousel.js new file mode 100644 index 0000000000..2f47edb8dc --- /dev/null +++ b/services/web/public/bootstrap/js/bootstrap-carousel.js @@ -0,0 +1,154 @@ +/* ========================================================== + * bootstrap-carousel.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function( $ ){ + + "use strict" + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.carousel.defaults, options) + this.options.slide && this.slide(this.options.slide) + } + + Carousel.prototype = { + + cycle: function () { + this.interval = setInterval($.proxy(this.next, this), this.options.interval) + return this + } + + , to: function (pos) { + var $active = this.$element.find('.active') + , children = $active.parent().children() + , activePos = children.index($active) + , that = this + + if (pos > (children.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activePos == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) + } + + , pause: function () { + clearInterval(this.interval) + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + if (!$.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger('slide') + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } else { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.trigger('slide') + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + $.fn.carousel = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = typeof option == 'object' && option + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (typeof option == 'string' || (option = options.slide)) data[option]() + else data.cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL DATA-API + * ================= */ + + $(function () { + $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) + $target.carousel(options) + e.preventDefault() + }) + }) + +}( window.jQuery ) diff --git a/services/web/public/bootstrap/js/bootstrap-collapse.js b/services/web/public/bootstrap/js/bootstrap-collapse.js new file mode 100644 index 0000000000..8134cc42ff --- /dev/null +++ b/services/web/public/bootstrap/js/bootstrap-collapse.js @@ -0,0 +1,136 @@ +/* ============================================================= + * bootstrap-collapse.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + +!function( $ ){ + + "use strict" + + var Collapse = function ( element, options ) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options["parent"]) { + this.$parent = $(this.options["parent"]) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension = this.dimension() + , scroll = $.camelCase(['scroll', dimension].join('-')) + , actives = this.$parent && this.$parent.find('.in') + , hasData + + if (actives && actives.length) { + hasData = actives.data('collapse') + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', 'show', 'shown') + this.$element[dimension](this.$element[0][scroll]) + + } + + , hide: function () { + var dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', 'hide', 'hidden') + this.$element[dimension](0) + } + + , reset: function ( size ) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element.addClass('collapse') + } + + , transition: function ( method, startEvent, completeEvent ) { + var that = this + , complete = function () { + if (startEvent == 'show') that.reset() + that.$element.trigger(completeEvent) + } + + this.$element + .trigger(startEvent) + [method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + /* COLLAPSIBLE PLUGIN DEFINITION + * ============================== */ + + $.fn.collapse = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = typeof option == 'object' && option + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSIBLE DATA-API + * ==================== */ + + $(function () { + $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $(target).collapse(option) + }) + }) + +}( window.jQuery ) diff --git a/services/web/public/bootstrap/js/bootstrap-dropdown.js b/services/web/public/bootstrap/js/bootstrap-dropdown.js new file mode 100644 index 0000000000..48d3ce0f85 --- /dev/null +++ b/services/web/public/bootstrap/js/bootstrap-dropdown.js @@ -0,0 +1,92 @@ +/* ============================================================ + * bootstrap-dropdown.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function( $ ){ + + "use strict" + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle="dropdown"]' + , Dropdown = function ( element ) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + , isActive + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + isActive = $parent.hasClass('open') + + clearMenus() + !isActive && $parent.toggleClass('open') + + return false + } + + } + + function clearMenus() { + $(toggle).parent().removeClass('open') + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html').on('click.dropdown.data-api', clearMenus) + $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) + }) + +}( window.jQuery ) diff --git a/services/web/public/bootstrap/js/bootstrap-modal.js b/services/web/public/bootstrap/js/bootstrap-modal.js new file mode 100644 index 0000000000..180f0b64d9 --- /dev/null +++ b/services/web/public/bootstrap/js/bootstrap-modal.js @@ -0,0 +1,210 @@ +/* ========================================================= + * bootstrap-modal.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function ( content, options ) { + this.options = options + this.$element = $(content) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + + if (this.isShown) return + + $('body').addClass('modal-open') + + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function ( e ) { + e && e.preventDefault() + + if (!this.isShown) return + + var that = this + this.isShown = false + + $('body').removeClass('modal-open') + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + hideModal.call(that) + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal( that ) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('