You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2015/02/02 08:32:12 UTC
[1/2] fauxton commit: updated refs/heads/master to ddef3d5
Repository: couchdb-fauxton
Updated Branches:
refs/heads/master 9f26ac1e2 -> ddef3d54c
Initial add CORS to Fauxton
Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/a9636bdc
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/a9636bdc
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/a9636bdc
Branch: refs/heads/master
Commit: a9636bdcdd7ccabb868c678d9be0ac339df282e0
Parents: 9f26ac1
Author: Christian Hogan <gi...@infliction.org>
Authored: Fri Nov 28 13:19:13 2014 -0500
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Feb 2 09:30:46 2015 +0200
----------------------------------------------------------------------
app/addons/config/assets/less/config.less | 4 +
app/addons/config/routes.js | 33 +-
app/addons/config/templates/dashboard.html | 2 +-
app/addons/config/templates/sidebartabs.html | 10 +
app/addons/config/views.js | 24 ++
app/addons/cors/assets/less/cors.less | 126 +++++++
app/addons/cors/base.js | 24 ++
app/addons/cors/resources.js | 64 ++++
app/addons/cors/routes.js | 21 ++
app/addons/cors/templates/cors.html | 50 +++
.../cors/templates/origin_domain_row.html | 33 ++
.../cors/templates/origin_domain_table.html | 17 +
app/addons/cors/views.js | 337 +++++++++++++++++++
settings.json.default | 1 +
14 files changed, 741 insertions(+), 5 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/assets/less/config.less
----------------------------------------------------------------------
diff --git a/app/addons/config/assets/less/config.less b/app/addons/config/assets/less/config.less
index 6cd0ed2..fd496c6 100644
--- a/app/addons/config/assets/less/config.less
+++ b/app/addons/config/assets/less/config.less
@@ -41,6 +41,10 @@
}
table.config {
+ thead th {
+ border-left: none;
+ border-bottom: 2px solid #999;
+ }
tr {
th, td {
vertical-align: middle;
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js
index 62fd9b1..32dc4cc 100644
--- a/app/addons/config/routes.js
+++ b/app/addons/config/routes.js
@@ -14,16 +14,33 @@ define([
'app',
'api',
'addons/config/resources',
- 'addons/config/views'
+ 'addons/config/views',
+ 'addons/cors/views'
],
-function(app, FauxtonAPI, Config, Views) {
+function(app, FauxtonAPI, Config, Views, CORS) {
var ConfigRouteObject = FauxtonAPI.RouteObject.extend({
- layout: 'one_pane',
+ layout: 'with_tabs_sidebar',
initialize: function () {
this.configs = new Config.Collection();
+ this.cors = new CORS.config();
+
+ this.sidebar = this.setView("#sidebar-content", new Views.Tabs({
+ sidebarItems: [
+ {
+ title: 'Main config',
+ typeSelect: 'main',
+ link: '_config'
+ },
+ {
+ title: 'CORS',
+ typeSelect: 'cors',
+ link: '_config/cors'
+ }
+ ]
+ }));
},
roles: ['_admin'],
@@ -38,12 +55,20 @@ function(app, FauxtonAPI, Config, Views) {
},
routes: {
- '_config': 'config'
+ '_config': 'config',
+ '_config/cors':'configCORS'
},
config: function () {
this.newSection = this.setView('#right-header', new Views.ConfigHeader({ collection: this.configs }));
this.setView('#dashboard-content', new Views.Table({ collection: this.configs }));
+ this.sidebar.setSelectedTab("main");
+ },
+
+ configCORS: function() {
+ this.removeView('#right-header');
+ this.newSection = this.setView('#dashboard-content', new CORS.Views.CORSMain({ model: this.cors }));
+ this.sidebar.setSelectedTab("cors");
},
establish: function () {
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/templates/dashboard.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/dashboard.html b/app/addons/config/templates/dashboard.html
index f7fae00..0af857d 100644
--- a/app/addons/config/templates/dashboard.html
+++ b/app/addons/config/templates/dashboard.html
@@ -14,7 +14,7 @@ the License.
<table class="config table table-striped table-bordered">
<thead>
- <th id="config-section" width="20%">Section</th>
+ <th id="config-section" width="22%">Section</th>
<th id="config-option" width="20%">Option</th>
<th id="config-value">Value</th>
<th id="config-trash"></th>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/templates/sidebartabs.html
----------------------------------------------------------------------
diff --git a/app/addons/config/templates/sidebartabs.html b/app/addons/config/templates/sidebartabs.html
new file mode 100644
index 0000000..b48b0ae
--- /dev/null
+++ b/app/addons/config/templates/sidebartabs.html
@@ -0,0 +1,10 @@
+<ul class="nav nav-list">
+ <% _.each(sidebarItems, function (item) { %>
+ <li>
+ <a data-type-select="<%- item.typeSelect %>" href="#<%- item.link %>">
+ <span class="<%- item.icon %>"></span>
+ <%- item.title %>
+ </a>
+ </li>
+ <% }); %>
+</ul>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/config/views.js
----------------------------------------------------------------------
diff --git a/app/addons/config/views.js b/app/addons/config/views.js
index 034205a..1ccdc3e 100644
--- a/app/addons/config/views.js
+++ b/app/addons/config/views.js
@@ -243,6 +243,30 @@ function(app, FauxtonAPI, Config, Components) {
});
}
});
+
+ Views.Tabs = FauxtonAPI.View.extend({
+ className: "sidenav",
+ tagName: "nav",
+ template: 'addons/config/templates/sidebartabs',
+ initialize: function (options) {
+ this.sidebarItems = options.sidebarItems;
+ },
+
+ setSelectedTab: function (selectedTab) {
+ this.selectedTab = selectedTab;
+ this.$('li').removeClass('active');
+ this.$('a[data-type-select="'+this.selectedTab+'"]').parent("li").addClass('active');
+ },
+ afterRender: function(){
+ this.setSelectedTab(this.selectedTab);
+ },
+
+ serialize: function () {
+ return {
+ sidebarItems: this.sidebarItems
+ };
+ }
+ });
return Views;
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/assets/less/cors.less
----------------------------------------------------------------------
diff --git a/app/addons/cors/assets/less/cors.less b/app/addons/cors/assets/less/cors.less
new file mode 100644
index 0000000..4e06032
--- /dev/null
+++ b/app/addons/cors/assets/less/cors.less
@@ -0,0 +1,126 @@
+/* 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.
+ */
+
+/* =cors
+ ---------------------------------------------------------------------- */
+@import "../../../../../assets/less/bootstrap/variables.less";
+@import "../../../../../assets/less/variables.less";
+@import "../../../../../assets/less/bootstrap/mixins.less";
+
+#sidebar-content .nav-list li a span {
+ display: none;
+}
+
+#cors-header {
+ margin: 18px 30px 0 30px;
+}
+
+#corsForm {
+ margin: 0;
+
+ .checkbox {
+ font-size: 16px;
+
+ input[type='checkbox'] {
+ margin-top: 4px;
+ }
+ }
+ .radio {
+ font-size: 16px;
+
+ input[type='radio'] {
+ margin-top: 4px;
+ }
+ }
+
+ .cors-enable {
+ padding: 10px 30px 18px 30px;
+ border-bottom: 1px solid #ccc;
+ }
+ #origin-domains-container {
+ display: none;
+
+ .new-origin-domain {
+ width: 100%;
+ border-radius: 6px;
+ }
+ }
+ #collapsing-container {
+ border-top: 1px solid #fff;
+ border-bottom: 1px solid #ccc;
+ padding: 18px 30px;
+ display: none;
+ }
+ .origin-domains {
+ margin: 0;
+ padding: 18px 0px;
+
+ p {
+ line-height: 36px;
+ }
+ .controls {
+ margin-top: 18px;
+ }
+ }
+ .localhost-tip {
+ padding: 18px 30px;
+ border-top: 1px solid #fff;
+ border-bottom: 1px solid #ccc;
+ }
+ .form-actions {
+ padding: 18px 30px;
+ margin-top: 0;
+ border-top: 1px solid #fff;
+ }
+}
+
+#origin-domain-table {
+ td {
+ padding: 8px 2px;
+ }
+ span {
+ cursor: pointer;
+ color: #E33F3B;
+ display: inline-block;
+ padding-top: 9px;
+ }
+
+ .edit-domain-section {
+ position: relative;
+ margin-right: 20px;
+
+ .field-wrapper {
+ margin-right: 78px;
+ }
+ input {
+ width: 100%;
+ padding: 9px;
+ margin: 0 0 0 4px;
+ }
+ }
+ .url-display {
+ padding: 10px;
+ }
+
+ .hide {
+ display: none;
+ }
+
+ .update-domain-btn {
+ position: absolute;
+ right: -2px;
+ top: 0;
+ padding: 9px;
+ border-radius: 0 6px 6px 0;
+ }
+}
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/base.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/base.js b/app/addons/cors/base.js
new file mode 100644
index 0000000..b5824e3
--- /dev/null
+++ b/app/addons/cors/base.js
@@ -0,0 +1,24 @@
+// 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.
+
+define([
+ "app",
+ "api",
+ "addons/cors/routes"
+],
+
+function (app, FauxtonAPI, CORS) {
+
+ CORS.initialize = function() {};
+
+ return CORS;
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/resources.js b/app/addons/cors/resources.js
new file mode 100644
index 0000000..bd3809e
--- /dev/null
+++ b/app/addons/cors/resources.js
@@ -0,0 +1,64 @@
+// 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.
+
+define([
+ "app",
+ "api"
+],
+
+function (app, FauxtonAPI) {
+ var CORS = FauxtonAPI.addon();
+
+
+ CORS.config = FauxtonAPI.Model.extend({
+ url: function() {
+ return app.host+"/_config/cors";
+ }
+ });
+
+ CORS.ConfigModel = Backbone.Model.extend({
+ documentation: "cors",
+
+ url: function () {
+ return app.host + '/_config/' + encodeURIComponent(this.get("section")) + '/' + encodeURIComponent(this.get("attribute"));
+ },
+
+ isNew: function () { return false; },
+
+ sync: function (method, model, options) {
+
+ var params = {
+ url: model.url(),
+ contentType: 'application/json',
+ dataType: 'json',
+ data: JSON.stringify(model.get('value'))
+ };
+
+ if (method === 'delete') {
+ params.type = 'DELETE';
+ } else {
+ params.type = 'PUT';
+ }
+
+ return $.ajax(params);
+ }
+
+ });
+
+ // simple helper function to validate the user entered a valid domain starting with http(s) and
+ // not including any subfolder
+ CORS.validateCORSDomain = function (str) {
+ return (/^https?:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$/).test(str);
+ };
+
+ return CORS;
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/routes.js b/app/addons/cors/routes.js
new file mode 100644
index 0000000..017211b
--- /dev/null
+++ b/app/addons/cors/routes.js
@@ -0,0 +1,21 @@
+// 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.
+
+define([
+ 'app',
+ 'api',
+ 'addons/cors/views'
+],
+
+function (app, FauxtonAPI, CORS) {
+ return CORS;
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/templates/cors.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/cors.html b/app/addons/cors/templates/cors.html
new file mode 100644
index 0000000..8db6a60
--- /dev/null
+++ b/app/addons/cors/templates/cors.html
@@ -0,0 +1,50 @@
+<!--
+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.
+-->
+
+<header id="cors-header">
+ <p>Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.</p>
+</header>
+
+<form id="corsForm">
+
+ <div class="cors-enable">
+ <label class="checkbox">
+ <input type="checkbox" class="js-enable-cors" name="enable_cors" <% if (typeof enableCors !== 'undefined' && enableCors === 'true') { %> checked="checked" <% } %>> Enable CORS
+ </label>
+ </div>
+
+ <div id="collapsing-container">
+ <p><strong>Origin Domains</strong></p>
+
+ <p>Databases will accept requests from these domains:</p>
+
+ <label class="checkbox"><input type="checkbox" class="js-all-origin-domains" name="all_origin_domains"> All origin domains ( * )</label></li>
+
+ <label class="checkbox"><input type="checkbox" class="js-restrict-origin-domains" name="restrict_origin_domains"> Restrict to specific origin domains</label>
+
+ <div id="origin-domains-container">
+ <div class="origin-domains"></div>
+ <input type="text" class="new-origin-domain" name="new_origin_domain" value="" placeholder="e.g., https://site.com" />
+ </div>
+
+ </div>
+
+ </div>
+
+ <div class="form-actions">
+ <input class="btn btn-success" type="submit" value="Save" />
+ </div>
+
+</form>
+
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/templates/origin_domain_row.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_row.html b/app/addons/cors/templates/origin_domain_row.html
new file mode 100644
index 0000000..19f6954
--- /dev/null
+++ b/app/addons/cors/templates/origin_domain_row.html
@@ -0,0 +1,33 @@
+<!--
+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.
+-->
+
+<td>
+ <div class="js-url url-display"><%-url%></div>
+
+ <div class="js-edit-domain edit-domain-section hide">
+ <div class="field-wrapper">
+ <input type="text" value="<%-url%>" />
+ </div>
+ <a class="js-save-domain btn update-domain-btn">
+ <i class="fonticon-save"></i>
+ Update
+ </a>
+ </div>
+</td>
+<td width="30">
+ <span class="js-edit fonticon-pencil" title="Click to edit"></span>
+</td>
+<td width="30">
+ <span class="js-delete fonticon-trash" title="Click to delete"></span>
+</td>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/templates/origin_domain_table.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_table.html b/app/addons/cors/templates/origin_domain_table.html
new file mode 100644
index 0000000..7192bea
--- /dev/null
+++ b/app/addons/cors/templates/origin_domain_table.html
@@ -0,0 +1,17 @@
+<!--
+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.
+-->
+
+<table id="origin-domain-table" class="table table-striped">
+ <tbody></tbody>
+</table>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/app/addons/cors/views.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/views.js b/app/addons/cors/views.js
new file mode 100644
index 0000000..c390d5a
--- /dev/null
+++ b/app/addons/cors/views.js
@@ -0,0 +1,337 @@
+// 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.
+
+define([
+ "app",
+ "api",
+ "addons/cors/resources"
+],
+
+
+function (app, FauxtonAPI, CORS) {
+ var Views= {};
+
+ Views.CORSMain = FauxtonAPI.View.extend({
+ className: 'cors-page',
+ template: 'addons/cors/templates/cors',
+ events: {
+ 'submit form#corsForm': 'submit',
+ 'click .js-enable-cors': 'corsClick',
+ 'click .js-restrict-origin-domains': 'restrictOrigins',
+ 'click .js-all-origin-domains': 'allOrigins'
+ },
+
+ initialize: function () {
+ this.originDomainTable = this.setView('.origin-domains', new Views.OriginDomainTable({
+ model: this.model
+ }));
+ },
+
+ serialize: function () {
+ return {
+ enableCors: this.model.get('credentials')
+ };
+ },
+
+ establish: function(){
+ return [this.model.fetch()];
+ },
+
+ afterRender: function () {
+ var corsEnabled = this.$('.js-enable-cors').is(':checked');
+ this.$('#collapsing-container').toggle(corsEnabled);
+ this.setupOrigins();
+ },
+
+ corsClick: function (e) {
+ var isChecked = this.$(e.target).prop('checked');
+ this.$('#collapsing-container').toggle(isChecked);
+ this.setupOrigins();
+ },
+
+
+ setupOrigins: function() {
+ var storedOrigins = this.model.get('origins');
+ if (storedOrigins && storedOrigins != '*') {
+ this.restrictOrigins();
+ } else {
+ this.allOrigins();
+ }
+ },
+
+ allOrigins: function() {
+ this.$('.js-all-origin-domains').prop('checked', true);
+ this.$('.js-restrict-origin-domains').prop('checked', false);
+ this.$('#origin-domains-container').hide();
+ },
+
+ restrictOrigins: function() {
+ this.$('.js-restrict-origin-domains').prop('checked', true);
+ this.$('.js-all-origin-domains').prop('checked', false);
+ this.$('#origin-domains-container').show();
+ },
+
+ formToJSON: function(formSelector){
+ var formObject = $(formSelector).serializeArray(),
+ formJSON={};
+ _.map(formObject, function(field){
+ formJSON[field.name]=field.value;
+ });
+ return formJSON;
+ },
+
+ submit: function(e){
+ e.preventDefault();
+ var data = this.formToJSON(e.currentTarget);
+
+ if (data.enable_cors === 'on') {
+
+ // CORS checked, save data
+ if (data.restrict_origin_domains === 'on') {
+ var storedOrigins = this.model.get('origins').split(',');
+ var newDomain = $.trim(data.new_origin_domain);
+
+ // if a new domain has been entered, check it's valid
+ if (!_.isEmpty(newDomain) && !CORS.validateCORSDomain(newDomain)) {
+ FauxtonAPI.addNotification({
+ msg: 'Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).',
+ type: 'error',
+ clear: true
+ });
+ return;
+ }
+
+ // check that the user has entered at least one new origin domain
+ if (storedOrigins && storedOrigins.length > 0 && storedOrigins !== '*') {
+ this.originData = storedOrigins.concat(newDomain).toString();
+ } else {
+ if (_.isEmpty(newDomain)) {
+ FauxtonAPI.addNotification({
+ msg: 'Please enter a new origin domain.',
+ type: 'error',
+ clear: true
+ });
+ this.$('.new-origin-domain').focus();
+ return;
+ }
+ this.originData = data.new_origin_domain;
+ }
+
+ } else {
+ this.originData = "*";
+ }
+
+
+ var enableOption = new CORS.ConfigModel({
+ section: 'httpd',
+ attribute: 'enable_cors',
+ value: 'true'
+ });
+
+ var enableCreds = new CORS.ConfigModel({
+ section: 'cors',
+ attribute: 'credentials',
+ value: 'true'
+ });
+
+ var allowOrigins = new CORS.ConfigModel({
+ section: 'cors',
+ attribute: 'origins',
+ value: this.originData
+ });
+
+ enableOption.save().then(function (response) {
+ var notification = FauxtonAPI.addNotification({
+ msg: 'Your settings have been saved.',
+ type: 'success',
+ clear: true
+ });
+ },
+ function (response, errorCode, errorMsg) {
+ var notification = FauxtonAPI.addNotification({
+ msg: 'Sorry! There was an error. Code ' + errorCode + '.',
+ type: 'error',
+ clear: true
+ });
+ });
+
+ enableCreds.save();
+ allowOrigins.save();
+ this.$('.new-origin-domain').val('');
+
+ } else {
+
+ // Disable CORS
+ var disableOption = new CORS.ConfigModel({
+ section: 'httpd',
+ attribute: 'enable_cors',
+ value: 'false'
+ });
+
+ var disableCreds = new CORS.ConfigModel({
+ section: 'cors',
+ attribute: 'credentials',
+ value: 'false'
+ });
+
+ var disableOrigins = new CORS.ConfigModel({
+ section: 'cors',
+ attribute: 'origins',
+ value: ''
+ });
+
+ disableOption.save().then(function (response) {
+ var notification = FauxtonAPI.addNotification({
+ msg: 'Your settings have been saved.',
+ type: 'success',
+ clear: true
+ });
+ },
+ function (response, errorCode, errorMsg) {
+ var notification = FauxtonAPI.addNotification({
+ msg: 'Sorry! There was an error. Code ' + errorCode + '.',
+ type: 'error',
+ clear: true
+ });
+ });
+
+ disableCreds.save();
+ disableOrigins.save();
+ }
+ }
+ });
+
+ Views.OriginDomainTable = FauxtonAPI.View.extend({
+ template: 'addons/cors/templates/origin_domain_table',
+
+ initialize: function () {
+ // listen for any server-side changes to the object (i.e. saves/deletes). Only then, re-render the table
+ this.listenTo(this.model, 'sync', this.render);
+ },
+
+ beforeRender: function () {
+ var origins = this.model.get('origins');
+
+ // if the stored origins are set to '*' or nothing's defined, show nothing
+ if (_.isEmpty(origins) || origins === '*') {
+ return;
+ }
+ this.showRows();
+ },
+
+ showRows: function () {
+ var originsArray = this.model.get('origins').split(',');
+ _.each(originsArray, function (url, index) {
+ this.insertView('#origin-domain-table tbody', new Views.OriginDomainRow({
+ model: this.model,
+ index: index
+ }));
+ }, this);
+ }
+ });
+
+
+ // this gets passed the entire model so it can manipulate it directly (add/update the row)
+ Views.OriginDomainRow = FauxtonAPI.View.extend({
+ template: 'addons/cors/templates/origin_domain_row',
+ tagName: 'tr',
+
+ events: {
+ 'click .js-edit': 'onEditDomain',
+ 'click .js-cancel-edit': 'onCancelEditDomain',
+ 'click .js-delete': 'onDeleteDomain',
+ 'click .js-save-domain': 'onSaveDomain'
+ },
+
+ serialize: function () {
+ return {
+ url: this.model.get('origins').split(',')[this.index]
+ };
+ },
+
+ onEditDomain: function () {
+
+ // show the editable field & save button
+ this.$('.js-url').addClass('hide');
+ this.$('.js-edit-domain').removeClass('hide');
+
+ // change the edit icon to a cancel icon
+ this.$('.js-edit').removeClass('js-edit fonticon-pencil')
+ .addClass('js-cancel-edit fonticon-cancel').attr("title", "Click to cancel");
+ this.$('.js-edit-domain input').select();
+ },
+
+ onCancelEditDomain: function () {
+ this.$('.js-url').removeClass('hide');
+ this.$('.js-edit-domain').addClass('hide');
+ this.$('.js-cancel-edit').removeClass('js-cancel-edit fonticon-cancel')
+ .addClass('js-edit fonticon-pencil').attr("title", "Click to edit");
+ },
+
+ onSaveDomain: function () {
+ var newDomain = this.$('.js-edit-domain input').val();
+
+ if (!CORS.validateCORSDomain(newDomain)) {
+ FauxtonAPI.addNotification({
+ msg: "Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).",
+ type: "error",
+ clear: true
+ });
+ return;
+ }
+
+ var domains = this.model.get('origins').split(',');
+ domains[this.index] = newDomain;
+ this.saveOrigins(domains);
+ },
+
+ // remove the domain from the list
+ onDeleteDomain: function () {
+
+ // remove the domain from the list. Anyone monitoring the object will hear that it's changed (e.g.
+ // the main table, which will know to re-render)
+ var domains = this.model.get('origins').split(',');
+ domains.splice(this.index, 1);
+
+ this.saveOrigins(domains);
+ },
+
+ saveOrigins: function (origins) {
+ var originDomains = origins.toString();
+
+ var allowOrigins = new CORS.ConfigModel({
+ section: 'cors',
+ attribute: 'origins',
+ value: originDomains
+ });
+
+ allowOrigins.save().then(function (response) {
+ var notification = FauxtonAPI.addNotification({
+ msg: 'Your origin domains have been updated.',
+ type: 'success',
+ clear: true
+ });
+ },
+ function (response, errorCode, errorMsg) {
+ var notification = FauxtonAPI.addNotification({
+ msg: 'Something went wrong.',
+ type: 'error',
+ clear: true
+ });
+ });
+ }
+ });
+
+ CORS.Views = Views;
+
+ return CORS;
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/a9636bdc/settings.json.default
----------------------------------------------------------------------
diff --git a/settings.json.default b/settings.json.default
index 8e88c0e..1e6aa50 100644
--- a/settings.json.default
+++ b/settings.json.default
@@ -6,6 +6,7 @@
{ "name": "activetasks" },
{ "name": "config" },
{ "name": "replication" },
+ { "name": "cors" },
{ "name": "plugins" },
{ "name": "permissions" },
{ "name": "compaction" },
[2/2] fauxton commit: updated refs/heads/master to ddef3d5
Posted by ga...@apache.org.
CORS Support
Fixes COUCHDB-2483
Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/ddef3d54
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/ddef3d54
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/ddef3d54
Branch: refs/heads/master
Commit: ddef3d54ccbe08a9767977e0cf349e7bc652fb5f
Parents: a9636bd
Author: Garren Smith <ga...@gmail.com>
Authored: Tue Jan 20 10:02:34 2015 +0200
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Feb 2 09:31:14 2015 +0200
----------------------------------------------------------------------
.gitignore | 1 +
app/addons/config/resources.js | 1 -
app/addons/config/routes.js | 12 +-
app/addons/cors/actions.js | 151 +++++++++
app/addons/cors/actiontypes.js | 24 ++
app/addons/cors/assets/less/cors.less | 34 +-
app/addons/cors/base.js | 2 +-
app/addons/cors/components.react.jsx | 328 +++++++++++++++++++
app/addons/cors/resources.js | 56 +++-
app/addons/cors/routes.js | 21 --
app/addons/cors/stores.js | 162 +++++++++
app/addons/cors/templates/cors.html | 50 ---
.../cors/templates/origin_domain_row.html | 33 --
.../cors/templates/origin_domain_table.html | 17 -
app/addons/cors/tests/actionsSpecs.js | 153 +++++++++
app/addons/cors/tests/componentsSpec.react.jsx | 220 +++++++++++++
app/addons/cors/tests/resourcesSpec.js | 41 +++
app/addons/cors/tests/storesSpec.js | 104 ++++++
app/addons/cors/views.js | 324 ++----------------
app/templates/layouts/with_tabs_sidebar.html | 2 +-
assets/less/templates.less | 2 +-
21 files changed, 1269 insertions(+), 469 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index db58376..7d5a71f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ app/addons/*
!app/addons/databases
!app/addons/documents
!app/addons/styletests
+!app/addons/cors
settings.json*
!settings.json.default
!assets/js/plugins/zeroclipboard/ZeroClipboard.swf
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/config/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/config/resources.js b/app/addons/config/resources.js
index 895e4d8..b1b4080 100644
--- a/app/addons/config/resources.js
+++ b/app/addons/config/resources.js
@@ -45,7 +45,6 @@ function (app, FauxtonAPI) {
} else {
params.type = 'PUT';
}
-
return $.ajax(params);
}
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/config/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js
index 32dc4cc..85b8ff6 100644
--- a/app/addons/config/routes.js
+++ b/app/addons/config/routes.js
@@ -25,8 +25,9 @@ function(app, FauxtonAPI, Config, Views, CORS) {
initialize: function () {
this.configs = new Config.Collection();
- this.cors = new CORS.config();
-
+ this.cors = new CORS.Config();
+ this.httpd = new CORS.Httpd();
+
this.sidebar = this.setView("#sidebar-content", new Views.Tabs({
sidebarItems: [
{
@@ -64,10 +65,13 @@ function(app, FauxtonAPI, Config, Views, CORS) {
this.setView('#dashboard-content', new Views.Table({ collection: this.configs }));
this.sidebar.setSelectedTab("main");
},
-
+
configCORS: function() {
this.removeView('#right-header');
- this.newSection = this.setView('#dashboard-content', new CORS.Views.CORSMain({ model: this.cors }));
+ this.newSection = this.setView('#dashboard-content', new CORS.Views.CORSWrapper({
+ cors: this.cors,
+ httpd: this.httpd
+ }));
this.sidebar.setSelectedTab("cors");
},
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/actions.js b/app/addons/cors/actions.js
new file mode 100644
index 0000000..bb9e25b
--- /dev/null
+++ b/app/addons/cors/actions.js
@@ -0,0 +1,151 @@
+// 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.
+define([
+ 'api',
+ 'addons/cors/actiontypes',
+ 'addons/cors/resources'
+ ], function (FauxtonAPI, ActionTypes, Resources) {
+ return {
+ editCors: function (options) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.EDIT_CORS,
+ options: options
+ });
+ },
+
+ toggleEnableCors: function () {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.TOGGLE_ENABLE_CORS
+ });
+ },
+
+ addOrigin: function (origin) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.CORS_ADD_ORIGIN,
+ origin: origin
+ });
+ },
+
+ originChange: function (isAllOrigins) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.CORS_IS_ALL_ORIGINS,
+ isAllOrigins: isAllOrigins
+ });
+ },
+
+ deleteOrigin: function (origin) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.CORS_DELETE_ORIGIN,
+ origin: origin
+ });
+ },
+
+ updateOrigin: function (updatedOrigin, originalOrigin) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.CORS_UPDATE_ORIGIN,
+ updatedOrigin: updatedOrigin,
+ originalOrigin: originalOrigin
+ });
+ },
+ methodChange: function (httpMethod) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.CORS_METHOD_CHANGE,
+ httpMethod: httpMethod
+ });
+ },
+
+ saveEnableCorsToHttpd: function (enableCors) {
+ var enableOption = new Resources.ConfigModel({
+ section: 'httpd',
+ attribute: 'enable_cors',
+ value: enableCors.toString()
+ });
+
+ return enableOption.save();
+ },
+
+ saveCorsOrigins: function (origins) {
+ var allowOrigins = new Resources.ConfigModel({
+ section: 'cors',
+ attribute: 'origins',
+ value: origins
+ });
+
+ return allowOrigins.save();
+ },
+
+ saveCorsCredentials: function () {
+ var allowCredentials = new Resources.ConfigModel({
+ section: 'cors',
+ attribute: 'credentials',
+ value: "true"
+ });
+
+ return allowCredentials.save();
+ },
+
+ saveCorsHeaders: function () {
+ var corsHeaders = new Resources.ConfigModel({
+ section: 'cors',
+ attribute: 'headers',
+ value: 'accept, authorization, content-type, origin, referer'
+ });
+
+ return corsHeaders.save();
+ },
+
+ saveCorsMethods: function () {
+ var corsMethods = new Resources.ConfigModel({
+ section: 'cors',
+ attribute: 'methods',
+ value: 'GET, PUT, POST, HEAD, DELETE'
+ });
+
+ return corsMethods.save();
+ },
+
+ sanitizeOrigins: function (origins) {
+ if(_.isEmpty(origins)) {
+ return '';
+ }
+
+ return origins.join(',');
+ },
+
+ saveCors: function (options) {
+ var promises = [];
+ promises.push(this.saveEnableCorsToHttpd(options.enableCors));
+
+ if(options.enableCors) {
+ promises.push(this.saveCorsOrigins(this.sanitizeOrigins(options.origins)));
+ promises.push(this.saveCorsCredentials());
+ promises.push(this.saveCorsHeaders());
+ promises.push(this.saveCorsMethods());
+ }
+
+ FauxtonAPI.when(promises).then(function () {
+ FauxtonAPI.addNotification({
+ msg: 'Cors settings updated',
+ type: 'success',
+ clear: true
+ });
+
+ }, function () {
+ FauxtonAPI.addNotification({
+ msg: 'Error! Could not save your CORS settings. Please try again.',
+ type: 'error',
+ clear: true
+ });
+ });
+ }
+ };
+ });
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/actiontypes.js b/app/addons/cors/actiontypes.js
new file mode 100644
index 0000000..7e4916b
--- /dev/null
+++ b/app/addons/cors/actiontypes.js
@@ -0,0 +1,24 @@
+// 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.
+define([], function () {
+ return {
+ TOGGLE_ENABLE_CORS: 'TOGGLE_ENABLE_CORS',
+ EDIT_CORS: 'EDIT_CORS',
+ CORS_ADD_ORIGIN: 'CORS_ADD_ORIGIN',
+ CORS_IS_ALL_ORIGINS: 'CORS_IS_ALL_ORIGINS',
+ CORS_DELETE_ORIGIN: 'CORS_DELETE_ORIGIN',
+ CORS_UPDATE_ORIGIN: 'CORS_UPDATE_ORIGIN',
+ CORS_METHOD_CHANGE: 'CORS_METHOD_CHANGE',
+ CORS_SAVING: 'CORS_SAVING',
+ CORS_SAVED: 'CORS_SAVED'
+ };
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/assets/less/cors.less
----------------------------------------------------------------------
diff --git a/app/addons/cors/assets/less/cors.less b/app/addons/cors/assets/less/cors.less
index 4e06032..ce6dc7e 100644
--- a/app/addons/cors/assets/less/cors.less
+++ b/app/addons/cors/assets/less/cors.less
@@ -48,23 +48,19 @@
border-bottom: 1px solid #ccc;
}
#origin-domains-container {
- display: none;
-
- .new-origin-domain {
- width: 100%;
- border-radius: 6px;
+ button.add-domain {
+ height: 46px;
}
}
#collapsing-container {
border-top: 1px solid #fff;
border-bottom: 1px solid #ccc;
padding: 18px 30px;
- display: none;
}
.origin-domains {
margin: 0;
padding: 18px 0px;
-
+
p {
line-height: 36px;
}
@@ -82,6 +78,14 @@
margin-top: 0;
border-top: 1px solid #fff;
}
+
+ .input-append {
+ width: 90%;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ }
}
#origin-domain-table {
@@ -96,26 +100,14 @@
}
.edit-domain-section {
- position: relative;
- margin-right: 20px;
-
- .field-wrapper {
- margin-right: 78px;
- }
- input {
- width: 100%;
- padding: 9px;
- margin: 0 0 0 4px;
+ .btn {
+ height: 46px;
}
}
.url-display {
padding: 10px;
}
- .hide {
- display: none;
- }
-
.update-domain-btn {
position: absolute;
right: -2px;
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/base.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/base.js b/app/addons/cors/base.js
index b5824e3..300b5b7 100644
--- a/app/addons/cors/base.js
+++ b/app/addons/cors/base.js
@@ -13,7 +13,7 @@
define([
"app",
"api",
- "addons/cors/routes"
+ "addons/cors/views"
],
function (app, FauxtonAPI, CORS) {
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/cors/components.react.jsx b/app/addons/cors/components.react.jsx
new file mode 100644
index 0000000..1de76c1
--- /dev/null
+++ b/app/addons/cors/components.react.jsx
@@ -0,0 +1,328 @@
+// 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.
+
+define([
+ "api",
+ "react",
+ "addons/cors/stores",
+ "addons/cors/resources",
+ "addons/cors/actions"
+], function (FauxtonAPI, React, Stores, Resources, Actions) {
+ var corsStore = Stores.corsStore;
+
+ var validateOrigin = function (origin) {
+ if (!Resources.validateCORSDomain(origin)) {
+ FauxtonAPI.addNotification({
+ msg: 'Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).',
+ type: 'error',
+ clear: true
+ });
+
+ return false;
+ }
+
+ return true;
+ };
+
+ var OriginRow = React.createClass({
+
+ getInitialState: function () {
+ return {
+ edit: false,
+ updatedOrigin: this.props.origin
+ };
+ },
+
+ editOrigin: function (event) {
+ event.preventDefault();
+ this.setState({edit: !this.state.edit});
+ },
+
+ updateOrigin: function (event) {
+ event.preventDefault();
+ if (!validateOrigin(this.state.updatedOrigin)) {
+ return;
+ }
+ this.props.updateOrigin(this.state.updatedOrigin, this.props.origin);
+ this.setState({edit: false});
+ },
+
+ deleteOrigin: function (event) {
+ event.preventDefault();
+ if (!window.confirm('Are you sure you want to delete ' + this.props.origin)) {
+ return;
+ }
+
+ this.props.deleteOrigin(this.props.origin);
+ },
+
+ onInputChange: function (event) {
+ this.setState({updatedOrigin: event.target.value});
+ },
+
+ createOriginDisplay: function () {
+ if (this.state.edit) {
+ return (
+ <div className="input-append edit-domain-section">
+ <input type="text" name="update_origin_domain" onChange={this.onInputChange} value={this.state.updatedOrigin} />
+ <button onClick={this.updateOrigin} className="btn btn-primary update-origin"> Update </button>
+ </div>
+ );
+ }
+
+ return <div className="js-url url-display">{this.props.origin}</div>;
+ },
+
+ render: function () {
+ var display = this.createOriginDisplay();
+ return (
+ <tr>
+ <td>
+ { display }
+ </td>
+ <td width="30">
+ <span className="fonticon-pencil" onClick={this.editOrigin} title="Click to edit"></span>
+ </td>
+ <td width="30">
+ <span className="fonticon-trash" onClick={this.deleteOrigin} title="Click to delete"></span>
+ </td>
+ </tr>
+ );
+ }
+
+ });
+
+ var OriginTable = React.createClass({
+
+ createRows: function () {
+ return _.map(this.props.origins, function (origin, i) {
+ return <OriginRow
+ updateOrigin={this.props.updateOrigin}
+ deleteOrigin={this.props.deleteOrigin}
+ key={i} origin={origin}
+ />;
+ }, this);
+ },
+
+ render: function () {
+ if (!this.props.isVisible || this.props.origins.length === 0) {
+ return null;
+ }
+
+ var origins = this.createRows();
+
+ return (
+ <table id="origin-domain-table" className="table table-striped">
+ <tbody>
+ {origins}
+ </tbody>
+ </table>
+ );
+ }
+
+ });
+
+ var OriginInput = React.createClass({
+ getInitialState: function () {
+ return {
+ origin: ''
+ };
+ },
+
+ onInputChange: function (e) {
+ this.setState({origin: e.target.value});
+ },
+
+ addOrigin: function (event) {
+ event.preventDefault();
+ if (!validateOrigin(this.state.origin)) {
+ return;
+ }
+
+ this.props.addOrigin(this.state.origin);
+ this.setState({origin: ''});
+ },
+
+ render: function () {
+ if (!this.props.isVisible) {
+ return null;
+ }
+
+ return (
+ <div id= "origin-domains-container">
+ <div className= "origin-domains">
+ <div className="input-append">
+ <input type="text" name="new_origin_domain" onChange={this.onInputChange} value={this.state.origin} placeholder="e.g., https://site.com"/>
+ <button onClick={this.addOrigin} className="btn btn-primary add-domain"> Add </button>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ });
+
+ var Origins = React.createClass({
+
+ onOriginChange: function (event) {
+ this.props.originChange(event.target.value === 'all');
+ },
+
+ render: function () {
+
+ if (!this.props.corsEnabled) {
+ return null;
+ }
+
+ return (
+ <div>
+ <p><strong> Origin Domains </strong> </p>
+ <p>Databases will accept requests from these domains: </p>
+ <label className="radio">
+ <input type="radio" checked={this.props.isAllOrigins} value="all" onChange={this.onOriginChange} name="all-domains"/> All origin domains ( * )
+ </label>
+ <label className="radio">
+ <input type="radio" checked={!this.props.isAllOrigins} value="selected" onChange={this.onOriginChange} name="selected-domains"/> Restrict to specific origin domains
+ </label>
+ </div>
+ );
+ }
+ });
+
+ var CORSController = React.createClass({
+
+ getStoreState: function () {
+ return {
+ corsEnabled: corsStore.isEnabled(),
+ origins: corsStore.getOrigins(),
+ isAllOrigins: corsStore.isAllOrigins(),
+ configChanged: corsStore.hasConfigChanged(),
+ savingStatus: corsStore.getSavingStatus()
+ };
+ },
+
+ getInitialState: function () {
+ return this.getStoreState();
+ },
+
+ componentDidMount: function () {
+ corsStore.on('change', this.onChange, this);
+ },
+
+ componentWillUnmount: function() {
+ corsStore.off('change', this.onChange);
+ },
+
+ componentDidUpdate: function () {
+ this.save();
+ },
+
+ onChange: function () {
+ this.setState(this.getStoreState());
+ },
+
+ enableCorsChange: function (event) {
+ if (this.state.corsEnabled && !_.isEmpty(this.state.origins) && !this.state.isAllOrigins) {
+ var result = window.confirm('Are you sure? Disabling CORS will overwrite your specific origin domains.');
+ if (!result) { return; }
+ }
+
+ Actions.toggleEnableCors();
+ },
+
+ save: function (event) {
+ Actions.saveCors({
+ enableCors: this.state.corsEnabled,
+ origins: this.state.origins
+ });
+ },
+
+ deleteOrigin: function (origin) {
+ Actions.deleteOrigin(origin);
+ },
+
+ originChange: function (isAllOrigins) {
+ if (isAllOrigins && !_.isEmpty(this.state.origins)) {
+ var result = window.confirm('Are you sure? Switching to all origin domains will overwrite your specific origin domains.');
+ if (!result) { return; }
+ }
+
+ Actions.originChange(isAllOrigins);
+ },
+
+ addOrigin: function (origin) {
+ Actions.addOrigin(origin);
+ },
+
+ updateOrigin: function (updatedOrigin, originalOrigin) {
+ Actions.updateOrigin(updatedOrigin, originalOrigin);
+ },
+
+ methodChange: function (httpMethod) {
+ Actions.methodChange(httpMethod);
+ },
+
+ getCorsNotice: function () {
+ var msg = FauxtonAPI.getExtensions('cors:notice');
+ if (_.isUndefined(msg)) {
+ return 'Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.';
+ }
+
+ return msg;
+ },
+
+ render: function () {
+ var isVisible = _.all([this.state.corsEnabled, !this.state.isAllOrigins]);
+ var className = this.state.corsEnabled ? 'collapsing-container' : '';
+
+ return (
+ <div className="cors-page">
+ <header id="cors-header">
+ <p> {this.getCorsNotice()}</p>
+ </header>
+
+ <form id="corsForm" onSubmit={this.save}>
+ <div className="cors-enable">
+ <label className="checkbox">
+ <input type="checkbox" checked={this.state.corsEnabled} onChange={this.enableCorsChange} /> Enable CORS
+ </label>
+ </div>
+ <div id={className}>
+ <Origins corsEnabled={this.state.corsEnabled} originChange={this.originChange} isAllOrigins={this.state.isAllOrigins}/>
+ <OriginTable updateOrigin={this.updateOrigin} deleteOrigin={this.deleteOrigin} isVisible={isVisible} origins={this.state.origins} />
+ <OriginInput addOrigin={this.addOrigin} isVisible={isVisible} />
+ </div>
+
+ <div className="form-actions">
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+ });
+
+ return {
+ renderCORS: function (el) {
+ React.render(<CORSController />, el);
+ },
+
+ removeCORS: function (el) {
+ React.unmountComponentAtNode(el);
+ },
+ CORSController: CORSController,
+ OriginInput: OriginInput,
+ Origins: Origins,
+ OriginTable: OriginTable,
+ OriginRow: OriginRow
+ };
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/resources.js b/app/addons/cors/resources.js
index bd3809e..4cf4f10 100644
--- a/app/addons/cors/resources.js
+++ b/app/addons/cors/resources.js
@@ -1,39 +1,65 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// 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
+// 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.
define([
- "app",
- "api"
+ 'app',
+ 'api'
],
-function (app, FauxtonAPI) {
+function (app, FauxtonAPI) {
var CORS = FauxtonAPI.addon();
- CORS.config = FauxtonAPI.Model.extend({
+ CORS.Config = FauxtonAPI.Model.extend({
url: function() {
- return app.host+"/_config/cors";
+ return app.host + '/_config/cors';
+ },
+
+ getOrigins: function () {
+ var origins = this.get('origins');
+ if (_.isUndefined(origins)) {
+ return [];
+ }
+
+ return origins.split(',');
+ }
+ });
+
+ CORS.Httpd = FauxtonAPI.Model.extend({
+ url: function() {
+ return app.host + '/_config/httpd';
+ },
+
+ corsEnabled: function () {
+ var enabledCors = this.get('enable_cors');
+
+ if (_.isUndefined(enabledCors)) {
+ return false;
+ }
+
+ return enabledCors === 'true';
}
+
});
-
+
CORS.ConfigModel = Backbone.Model.extend({
- documentation: "cors",
-
+ documentation: 'cors',
+
url: function () {
- return app.host + '/_config/' + encodeURIComponent(this.get("section")) + '/' + encodeURIComponent(this.get("attribute"));
+ return app.host + '/_config/' + encodeURIComponent(this.get('section')) + '/' + encodeURIComponent(this.get('attribute'));
},
-
+
isNew: function () { return false; },
-
+
sync: function (method, model, options) {
var params = {
@@ -51,9 +77,9 @@ function (app, FauxtonAPI) {
return $.ajax(params);
}
-
+
});
-
+
// simple helper function to validate the user entered a valid domain starting with http(s) and
// not including any subfolder
CORS.validateCORSDomain = function (str) {
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/routes.js b/app/addons/cors/routes.js
deleted file mode 100644
index 017211b..0000000
--- a/app/addons/cors/routes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// 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.
-
-define([
- 'app',
- 'api',
- 'addons/cors/views'
-],
-
-function (app, FauxtonAPI, CORS) {
- return CORS;
-});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/stores.js b/app/addons/cors/stores.js
new file mode 100644
index 0000000..dc70cbd
--- /dev/null
+++ b/app/addons/cors/stores.js
@@ -0,0 +1,162 @@
+// 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.
+
+define([
+ "api",
+ "addons/cors/actiontypes"
+], function (FauxtonAPI, ActionTypes) {
+
+ var CorsStore = FauxtonAPI.Store.extend({
+
+ editCors: function (options) {
+ this._isEnabled = options.isEnabled;
+ this._origins = options.origins;
+ this._configChanged = false;
+ this.savingDone();
+ },
+
+ saving: function () {
+ this._savingStatus = 'Saving';
+ },
+
+ savingDone: function () {
+ this._savingStatus = 'Save';
+ },
+
+ getSavingStatus: function () {
+ return this._savingStatus;
+ },
+
+ hasConfigChanged: function () {
+ return this._configChanged;
+ },
+
+ setConfigChanged: function () {
+ this._configChanged = true;
+ },
+
+ setConfigSaved: function () {
+ this._configChanged = false;
+ },
+
+ isEnabled: function () {
+ return this._isEnabled;
+ },
+
+ addOrigin: function (origin) {
+ this._origins.push(origin);
+ },
+
+ deleteOrigin: function (origin) {
+ var index = _.indexOf(this._origins, origin);
+
+ if (index === -1) { return; }
+
+ this._origins.splice(index, 1);
+ },
+
+ originChange: function (isAllOrigins) {
+ if (isAllOrigins) {
+ this._origins = ['*'];
+ return;
+ }
+
+ this._origins = [];
+ },
+
+ getOrigins: function () {
+ return this._origins;
+ },
+
+ isAllOrigins: function () {
+ var origins = this.getOrigins();
+ if (_.include(origins, '*')) {
+ return true;
+ }
+
+ return false;
+ },
+
+ toggleEnableCors: function () {
+ this._isEnabled = !this._isEnabled;
+ },
+
+ updateOrigin: function (updatedOrigin, originalOrigin) {
+ this.deleteOrigin(originalOrigin);
+ this.addOrigin(updatedOrigin);
+ },
+
+ dispatch: function (action) {
+
+ switch (action.type) {
+ case ActionTypes.EDIT_CORS:
+ this.editCors(action.options);
+ this.triggerChange();
+ break;
+
+ case ActionTypes.TOGGLE_ENABLE_CORS:
+ this.toggleEnableCors();
+ this.setConfigChanged();
+ this.triggerChange();
+ break;
+
+ case ActionTypes.CORS_ADD_ORIGIN:
+ this.addOrigin(action.origin);
+ this.setConfigChanged();
+ this.triggerChange();
+ break;
+
+ case ActionTypes.CORS_IS_ALL_ORIGINS:
+ this.originChange(action.isAllOrigins);
+ this.setConfigChanged();
+ this.triggerChange();
+ break;
+
+ case ActionTypes.CORS_DELETE_ORIGIN:
+ this.deleteOrigin(action.origin);
+ this.setConfigChanged();
+ this.triggerChange();
+ break;
+
+ case ActionTypes.CORS_UPDATE_ORIGIN:
+ this.updateOrigin(action.updatedOrigin, action.originalOrigin);
+ this.setConfigChanged();
+ this.triggerChange();
+ break;
+
+ case ActionTypes.CORS_SAVING:
+ this.saving();
+ this.triggerChange();
+ break;
+
+ case ActionTypes.CORS_SAVED:
+ this.setConfigSaved();
+ this.savingDone();
+ this.triggerChange();
+ break;
+
+ default:
+ return;
+ }
+ }
+
+ });
+
+
+ var corsStore = new CorsStore();
+
+ corsStore.dispatchToken = FauxtonAPI.dispatcher.register(corsStore.dispatch.bind(corsStore));
+
+ return {
+ corsStore: corsStore
+ };
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/templates/cors.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/cors.html b/app/addons/cors/templates/cors.html
deleted file mode 100644
index 8db6a60..0000000
--- a/app/addons/cors/templates/cors.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!--
-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.
--->
-
-<header id="cors-header">
- <p>Cross-Origin Resource Sharing (CORS) lets you connect to remote servers directly from the browser, so you can host browser-based apps on static pages and talk directly with CouchDB to load your data.</p>
-</header>
-
-<form id="corsForm">
-
- <div class="cors-enable">
- <label class="checkbox">
- <input type="checkbox" class="js-enable-cors" name="enable_cors" <% if (typeof enableCors !== 'undefined' && enableCors === 'true') { %> checked="checked" <% } %>> Enable CORS
- </label>
- </div>
-
- <div id="collapsing-container">
- <p><strong>Origin Domains</strong></p>
-
- <p>Databases will accept requests from these domains:</p>
-
- <label class="checkbox"><input type="checkbox" class="js-all-origin-domains" name="all_origin_domains"> All origin domains ( * )</label></li>
-
- <label class="checkbox"><input type="checkbox" class="js-restrict-origin-domains" name="restrict_origin_domains"> Restrict to specific origin domains</label>
-
- <div id="origin-domains-container">
- <div class="origin-domains"></div>
- <input type="text" class="new-origin-domain" name="new_origin_domain" value="" placeholder="e.g., https://site.com" />
- </div>
-
- </div>
-
- </div>
-
- <div class="form-actions">
- <input class="btn btn-success" type="submit" value="Save" />
- </div>
-
-</form>
-
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/templates/origin_domain_row.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_row.html b/app/addons/cors/templates/origin_domain_row.html
deleted file mode 100644
index 19f6954..0000000
--- a/app/addons/cors/templates/origin_domain_row.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-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.
--->
-
-<td>
- <div class="js-url url-display"><%-url%></div>
-
- <div class="js-edit-domain edit-domain-section hide">
- <div class="field-wrapper">
- <input type="text" value="<%-url%>" />
- </div>
- <a class="js-save-domain btn update-domain-btn">
- <i class="fonticon-save"></i>
- Update
- </a>
- </div>
-</td>
-<td width="30">
- <span class="js-edit fonticon-pencil" title="Click to edit"></span>
-</td>
-<td width="30">
- <span class="js-delete fonticon-trash" title="Click to delete"></span>
-</td>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/templates/origin_domain_table.html
----------------------------------------------------------------------
diff --git a/app/addons/cors/templates/origin_domain_table.html b/app/addons/cors/templates/origin_domain_table.html
deleted file mode 100644
index 7192bea..0000000
--- a/app/addons/cors/templates/origin_domain_table.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!--
-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.
--->
-
-<table id="origin-domain-table" class="table table-striped">
- <tbody></tbody>
-</table>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/actionsSpecs.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/actionsSpecs.js b/app/addons/cors/tests/actionsSpecs.js
new file mode 100644
index 0000000..9b74634
--- /dev/null
+++ b/app/addons/cors/tests/actionsSpecs.js
@@ -0,0 +1,153 @@
+// 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.
+define([
+ 'app',
+ 'testUtils',
+ 'api',
+ 'addons/cors/actions',
+], function (app, testUtils, FauxtonAPI, Actions) {
+ var assert = testUtils.assert;
+
+ describe('CORS actions', function () {
+
+ describe('save', function () {
+
+ afterEach(function () {
+ Actions.saveCorsOrigins.restore && Actions.saveCorsOrigins.restore();
+ });
+
+ it('should save cors enabled to httpd', function () {
+ var spy = sinon.spy(Actions, 'saveEnableCorsToHttpd');
+
+ Actions.saveCors({
+ enableCors: false
+ });
+
+ assert.ok(spy.calledWith(false));
+ });
+
+ it('does not save cors origins if cors not enabled', function () {
+ var spy = sinon.spy(Actions, 'saveCorsOrigins');
+
+ Actions.saveCors({
+ enableCors: false,
+ origins: ['*']
+ });
+
+ assert.notOk(spy.calledOnce);
+ });
+
+ it('saves cors origins', function () {
+ var spy = sinon.spy(Actions, 'saveCorsOrigins');
+
+ Actions.saveCors({
+ enableCors: true,
+ origins: ['*']
+ });
+
+ assert.ok(spy.calledWith('*'));
+ });
+
+ it('saves cors allow credentials', function () {
+ var spy = sinon.spy(Actions, 'saveCorsCredentials');
+
+ Actions.saveCors({
+ enableCors: true,
+ origins: ['https://testdomain.com']
+ });
+
+ assert.ok(spy.calledOnce);
+ });
+
+ it('saves cors headers', function () {
+ var spy = sinon.spy(Actions, 'saveCorsHeaders');
+
+ Actions.saveCors({
+ enableCors: true,
+ origins: ['https://testdomain.com']
+ });
+
+ assert.ok(spy.calledOnce);
+ });
+
+ it('saves cors methods', function () {
+ var spy = sinon.spy(Actions, 'saveCorsMethods');
+
+ Actions.saveCors({
+ enableCors: true,
+ origins: ['https://testdomain.com']
+ });
+
+ assert.ok(spy.calledOnce);
+
+ });
+
+ it('shows notification on successful save', function () {
+ var stub = sinon.stub(FauxtonAPI, 'when');
+ var spy = sinon.spy(FauxtonAPI, 'addNotification');
+ var promise = FauxtonAPI.Deferred();
+ promise.resolve();
+ stub.returns(promise);
+
+ Actions.saveCors({
+ enableCors: true,
+ origins: ['https://testdomain.com']
+ });
+
+ assert.ok(spy.calledOnce);
+ FauxtonAPI.when.restore();
+ FauxtonAPI.addNotification.restore();
+ });
+
+ it('dispatches CORS_SAVED', function () {
+ var stub = sinon.stub(FauxtonAPI, 'when');
+ var called = false;
+ var promise = FauxtonAPI.Deferred();
+ promise.resolve();
+ stub.returns(promise);
+
+ FauxtonAPI.dispatcher.register(function (actions) {
+
+ if (actions.type === 'CORS_SAVED') {
+ called = true;
+ }
+
+ });
+
+ Actions.saveCors({
+ enableCors: true,
+ origins: ['https://testdomain.com']
+ });
+
+ assert.ok(called);
+ FauxtonAPI.when.restore();
+ });
+
+ });
+
+ describe('Sanitize origins', function () {
+
+ it('joins array into string', function () {
+ var origins = ['https://hello.com', 'https://hello2.com'];
+
+ assert.deepEqual(Actions.sanitizeOrigins(origins), origins.join(','));
+ });
+
+ it('returns empty string for no origins', function () {
+ var origins = [];
+
+ assert.deepEqual(Actions.sanitizeOrigins(origins), '');
+ });
+ });
+ });
+
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/componentsSpec.react.jsx b/app/addons/cors/tests/componentsSpec.react.jsx
new file mode 100644
index 0000000..5880257
--- /dev/null
+++ b/app/addons/cors/tests/componentsSpec.react.jsx
@@ -0,0 +1,220 @@
+// 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.
+define([
+ 'api',
+ 'addons/cors/components.react',
+ 'addons/cors/actions',
+ 'addons/cors/resources',
+ 'addons/cors/stores',
+ 'testUtils',
+ "react"
+], function (FauxtonAPI, Views, Actions, Resources, Stores, utils, React) {
+
+ FauxtonAPI.router = new FauxtonAPI.Router([]);
+ var assert = utils.assert;
+ var TestUtils = React.addons.TestUtils;
+ var corsStore = Stores.corsStore;
+
+ describe('CORS Components', function () {
+
+
+ describe('CorsController', function () {
+ var container, corsEl, saveStub;
+
+ beforeEach(function () {
+ container = document.createElement('div');
+ corsStore._origins = ['http://hello.com'];
+ corsStore._isEnabled = true;
+ corsStore._configChanged = true;
+ corsEl = TestUtils.renderIntoDocument(<Views.CORSController />, container);
+ //stub this out so it doesn't keep trying to save cors and crash phantomjs
+ saveStub = sinon.stub(corsEl, 'save');
+ });
+
+ afterEach(function () {
+ corsEl.save.restore();
+ React.unmountComponentAtNode(container);
+ window.confirm.restore && window.confirm.restore();
+ });
+
+ it('confirms user change from restricted origin to disabled cors', function () {
+ var spy = sinon.stub(window, 'confirm');
+ spy.returns(false);
+ corsEl.state.isAllOrigins = false;
+ corsEl.state.corsEnabled = true;
+ corsEl.enableCorsChange();
+ assert.ok(spy.calledOnce);
+ });
+
+ it('does not confirm for selected origins are emtpy for disabled cors change', function () {
+ var spy = sinon.stub(window, 'confirm');
+ spy.returns(false);
+ corsEl.state.corsEnabled = true;
+ corsEl.state.isAllOrigins = false;
+ corsEl.state.origins = [];
+ corsEl.enableCorsChange();
+ assert.notOk(spy.calledOnce);
+ });
+
+ it('confirms user change when moving from selected origins to all origins', function () {
+ var spy = sinon.stub(window, 'confirm');
+ spy.returns(false);
+ corsEl.state.corsEnabled = true;
+ corsEl.state.isAllOrigins = false;
+ corsEl.originChange(true);
+ assert.ok(spy.calledOnce);
+ });
+
+ it('does not confirm all origins change if selected origins are emtpy', function () {
+ var spy = sinon.stub(window, 'confirm');
+ spy.returns(false);
+ corsEl.state.corsEnabled = true;
+ corsEl.state.isAllOrigins = false;
+ corsEl.state.origins = [];
+ corsEl.originChange(true);
+ assert.notOk(spy.calledOnce);
+ });
+ });
+
+ describe('OriginInput', function () {
+ var container, inputEl, addOrigin;
+ var newOrigin = 'http://new-site.com';
+
+ beforeEach(function () {
+ addOrigin = sinon.spy();
+ container = document.createElement('div');
+ inputEl = TestUtils.renderIntoDocument(<Views.OriginInput isVisible={true} addOrigin={addOrigin}/>, container);
+ });
+
+ afterEach(function () {
+ Resources.validateCORSDomain.restore && Resources.validateCORSDomain.restore();
+ React.unmountComponentAtNode(container);
+ FauxtonAPI.addNotification.restore && FauxtonAPI.addNotification.restore();
+ });
+
+ it('calls validates each domain', function () {
+ var spy = sinon.spy(Resources, 'validateCORSDomain');
+ TestUtils.Simulate.change($(inputEl.getDOMNode()).find('input')[0],{target: {value: newOrigin}});
+ TestUtils.Simulate.click($(inputEl.getDOMNode()).find('.btn')[0]);
+ assert.ok(spy.calledWith(newOrigin));
+ });
+
+ it('calls addOrogin AddOrigin on add click with valid domain', function () {
+ TestUtils.Simulate.change($(inputEl.getDOMNode()).find('input')[0],{target: {value: newOrigin}});
+ TestUtils.Simulate.click($(inputEl.getDOMNode()).find('.btn')[0]);
+ assert.ok(addOrigin.calledWith(newOrigin));
+ });
+
+ it('shows notification if origin is not valid', function () {
+ var spy = sinon.spy(FauxtonAPI, 'addNotification');
+ TestUtils.Simulate.change($(inputEl.getDOMNode()).find('input')[0],{target: {value: 'badOrigin'}});
+ TestUtils.Simulate.click($(inputEl.getDOMNode()).find('.btn')[0]);
+ assert.ok(spy.calledOnce);
+ });
+ });
+
+ describe('Origins', function () {
+ var container, originEl, changeOrigin;
+
+ beforeEach(function () {
+ changeOrigin = sinon.spy();
+ container = document.createElement('div');
+ originEl = TestUtils.renderIntoDocument(<Views.Origins corsEnabled={true} isAllOrigins={false} originChange={changeOrigin}/>, container);
+ });
+
+ afterEach(function () {
+ React.unmountComponentAtNode(container);
+ });
+
+ it('calls change Origin on all origins selected', function () {
+ TestUtils.Simulate.change($(originEl.getDOMNode()).find('input[value="all"]')[0]);
+ assert.ok(changeOrigin.calledWith(true));
+ });
+
+ it('calls change Origin on selected origins selected', function () {
+ TestUtils.Simulate.change($(originEl.getDOMNode()).find('input[value="selected"]')[0]);
+ assert.ok(changeOrigin.calledWith(false));
+ });
+ });
+
+ describe('OriginRow', function () {
+ var container, originTableEl, origin, deleteOrigin, updateOrigin;
+
+ beforeEach(function () {
+ deleteOrigin = sinon.spy();
+ updateOrigin = sinon.spy();
+ container = document.createElement('div');
+ origin = 'https://hello.com';
+ //because OriginRow is inside a table have to render the whole table to test
+ originTableEl = TestUtils.renderIntoDocument(<Views.OriginTable updateOrigin={updateOrigin} deleteOrigin={deleteOrigin} isVisible={true} origins={[origin]}/>, container);
+ });
+
+ afterEach(function () {
+ window.confirm.restore && window.confirm.restore();
+ Actions.deleteOrigin.restore && Actions.deleteOrigin.restore();
+ React.unmountComponentAtNode(container);
+ });
+
+ it('should confirm on delete', function () {
+ var spy = sinon.spy(window, 'confirm');
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-trash')[0]);
+ assert.ok(spy.calledOnce);
+ });
+
+ it('should deleteOrigin on confirm true', function () {
+ var stub = sinon.stub(window, 'confirm');
+ stub.returns(true);
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-trash')[0]);
+ assert.ok(deleteOrigin.calledWith(origin));
+ });
+
+ it('should not deleteOrigin on confirm false', function () {
+ var stub = sinon.stub(window, 'confirm');
+ stub.returns(false);
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-trash')[0]);
+ assert.notOk(deleteOrigin.calledOnce);
+ });
+
+ it('should change origin to input on edit click', function () {
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-pencil')[0]);
+ assert.ok($(originTableEl.getDOMNode()).find('input').length === 1);
+ });
+
+ it('should update origin on update clicked', function () {
+ var updatedOrigin = 'https://updated-origin.com';
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-pencil')[0]);
+ TestUtils.Simulate.change($(originTableEl.getDOMNode()).find('input')[0], {
+ target: {
+ value: updatedOrigin
+ }
+ });
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.btn')[0]);
+ assert.ok(updateOrigin.calledWith(updatedOrigin));
+ });
+
+ it('should not update origin on update clicked with bad origin', function () {
+ var updatedOrigin = 'updated-origin';
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.fonticon-pencil')[0]);
+ TestUtils.Simulate.change($(originTableEl.getDOMNode()).find('input')[0], {
+ target: {
+ value: updatedOrigin
+ }
+ });
+ TestUtils.Simulate.click($(originTableEl.getDOMNode()).find('.btn')[0]);
+ assert.notOk(updateOrigin.calledOnce);
+ });
+
+ });
+
+ });
+
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/resourcesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/resourcesSpec.js b/app/addons/cors/tests/resourcesSpec.js
new file mode 100644
index 0000000..2d2c8d0
--- /dev/null
+++ b/app/addons/cors/tests/resourcesSpec.js
@@ -0,0 +1,41 @@
+// 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.
+define([
+ 'app',
+ 'testUtils',
+ 'addons/cors/resources',
+], function (app, testUtils, CORS) {
+ var assert = testUtils.assert;
+
+ describe('Cors Config Model', function () {
+ var cors;
+
+ beforeEach(function () {
+ cors = new CORS.Config({});
+ });
+
+ it('Splits up origins into array', function () {
+ var origins = ['http://hello.com', 'http://another.co.a'];
+ cors.set({origins: origins.join(',')});
+
+ assert.deepEqual(cors.getOrigins(), origins);
+ });
+
+ it('returns empty array for undefined', function () {
+
+ assert.deepEqual(cors.getOrigins(), []);
+ });
+
+
+ });
+
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/tests/storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/tests/storesSpec.js b/app/addons/cors/tests/storesSpec.js
new file mode 100644
index 0000000..48a1897
--- /dev/null
+++ b/app/addons/cors/tests/storesSpec.js
@@ -0,0 +1,104 @@
+// 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.
+define([
+ 'app',
+ 'testUtils',
+ 'api',
+ 'addons/cors/stores',
+], function (app, testUtils, FauxtonAPI, Stores) {
+ var assert = testUtils.assert;
+ var store = Stores.corsStore;
+
+ describe('CORS store', function () {
+
+ describe('isAllOrigins', function () {
+
+ it('returns true for all origins', function () {
+ store._origins = ['*'];
+
+ assert.ok(store.isAllOrigins());
+ });
+
+ it('returns false for specific origins', function () {
+ store._origins = ['https://hello.com', 'http://another.com'];
+ assert.notOk(store.isAllOrigins());
+ });
+
+ it('returns false for empty array', function () {
+ store._origins = [];
+ assert.notOk(store.isAllOrigins());
+ });
+ });
+
+ describe('addOrigin', function () {
+
+ it('adds Origin to list', function () {
+ var origin = 'http://hello.com';
+ store._origins = [];
+ store.addOrigin(origin);
+
+ assert.ok(_.include(store.getOrigins(), origin));
+ });
+
+ });
+
+ describe('originChange', function () {
+
+ it('sets origins to * for true', function () {
+ store.originChange(true);
+
+ assert.deepEqual(store.getOrigins(), ['*']);
+ });
+
+ it('sets origins to [] for true', function () {
+ store.originChange(false);
+
+ assert.deepEqual(store.getOrigins(), []);
+ });
+
+ });
+
+ describe('deleteOrigin', function () {
+
+ it('removes origin', function () {
+ store._origins = ['http://first.com', 'http://hello.com', 'http://second.com'];
+ store.deleteOrigin('http://hello.com');
+
+ assert.deepEqual(store.getOrigins(), ['http://first.com', 'http://second.com']);
+
+ });
+
+ });
+
+ describe('update origin', function () {
+
+ it('removes old origin', function () {
+ store._origins = ['http://first.com', 'http://hello.com', 'http://second.com'];
+ store.updateOrigin('http://hello123.com', 'http://hello.com');
+
+ assert.notOk(_.include(store.getOrigins(), 'http://hello.com'));
+
+ });
+
+ it('adds new origin', function () {
+ store._origins = ['http://first.com', 'http://hello.com', 'http://second.com'];
+ store.updateOrigin('http://hello123.com', 'http://hello.com');
+
+ assert.ok(_.include(store.getOrigins(), 'http://hello123.com'));
+
+ });
+
+ });
+
+ });
+
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/addons/cors/views.js
----------------------------------------------------------------------
diff --git a/app/addons/cors/views.js b/app/addons/cors/views.js
index c390d5a..341b427 100644
--- a/app/addons/cors/views.js
+++ b/app/addons/cors/views.js
@@ -13,324 +13,40 @@
define([
"app",
"api",
- "addons/cors/resources"
+ "addons/cors/resources",
+ "addons/cors/components.react",
+ "addons/cors/actions"
],
-function (app, FauxtonAPI, CORS) {
+function (app, FauxtonAPI, CORS, Components, Actions) {
var Views= {};
-
- Views.CORSMain = FauxtonAPI.View.extend({
- className: 'cors-page',
- template: 'addons/cors/templates/cors',
- events: {
- 'submit form#corsForm': 'submit',
- 'click .js-enable-cors': 'corsClick',
- 'click .js-restrict-origin-domains': 'restrictOrigins',
- 'click .js-all-origin-domains': 'allOrigins'
- },
-
- initialize: function () {
- this.originDomainTable = this.setView('.origin-domains', new Views.OriginDomainTable({
- model: this.model
- }));
- },
-
- serialize: function () {
- return {
- enableCors: this.model.get('credentials')
- };
- },
-
- establish: function(){
- return [this.model.fetch()];
- },
-
- afterRender: function () {
- var corsEnabled = this.$('.js-enable-cors').is(':checked');
- this.$('#collapsing-container').toggle(corsEnabled);
- this.setupOrigins();
- },
-
- corsClick: function (e) {
- var isChecked = this.$(e.target).prop('checked');
- this.$('#collapsing-container').toggle(isChecked);
- this.setupOrigins();
- },
-
-
- setupOrigins: function() {
- var storedOrigins = this.model.get('origins');
- if (storedOrigins && storedOrigins != '*') {
- this.restrictOrigins();
- } else {
- this.allOrigins();
- }
- },
-
- allOrigins: function() {
- this.$('.js-all-origin-domains').prop('checked', true);
- this.$('.js-restrict-origin-domains').prop('checked', false);
- this.$('#origin-domains-container').hide();
- },
- restrictOrigins: function() {
- this.$('.js-restrict-origin-domains').prop('checked', true);
- this.$('.js-all-origin-domains').prop('checked', false);
- this.$('#origin-domains-container').show();
+ Views.CORSWrapper = FauxtonAPI.View.extend({
+ className: 'list',
+ initialize: function (options) {
+ this.cors = options.cors;
+ this.httpd = options.httpd;
},
-
- formToJSON: function(formSelector){
- var formObject = $(formSelector).serializeArray(),
- formJSON={};
- _.map(formObject, function(field){
- formJSON[field.name]=field.value;
- });
- return formJSON;
- },
-
- submit: function(e){
- e.preventDefault();
- var data = this.formToJSON(e.currentTarget);
-
- if (data.enable_cors === 'on') {
- // CORS checked, save data
- if (data.restrict_origin_domains === 'on') {
- var storedOrigins = this.model.get('origins').split(',');
- var newDomain = $.trim(data.new_origin_domain);
-
- // if a new domain has been entered, check it's valid
- if (!_.isEmpty(newDomain) && !CORS.validateCORSDomain(newDomain)) {
- FauxtonAPI.addNotification({
- msg: 'Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).',
- type: 'error',
- clear: true
- });
- return;
- }
-
- // check that the user has entered at least one new origin domain
- if (storedOrigins && storedOrigins.length > 0 && storedOrigins !== '*') {
- this.originData = storedOrigins.concat(newDomain).toString();
- } else {
- if (_.isEmpty(newDomain)) {
- FauxtonAPI.addNotification({
- msg: 'Please enter a new origin domain.',
- type: 'error',
- clear: true
- });
- this.$('.new-origin-domain').focus();
- return;
- }
- this.originData = data.new_origin_domain;
- }
-
- } else {
- this.originData = "*";
- }
-
-
- var enableOption = new CORS.ConfigModel({
- section: 'httpd',
- attribute: 'enable_cors',
- value: 'true'
- });
-
- var enableCreds = new CORS.ConfigModel({
- section: 'cors',
- attribute: 'credentials',
- value: 'true'
- });
-
- var allowOrigins = new CORS.ConfigModel({
- section: 'cors',
- attribute: 'origins',
- value: this.originData
- });
-
- enableOption.save().then(function (response) {
- var notification = FauxtonAPI.addNotification({
- msg: 'Your settings have been saved.',
- type: 'success',
- clear: true
- });
- },
- function (response, errorCode, errorMsg) {
- var notification = FauxtonAPI.addNotification({
- msg: 'Sorry! There was an error. Code ' + errorCode + '.',
- type: 'error',
- clear: true
- });
- });
-
- enableCreds.save();
- allowOrigins.save();
- this.$('.new-origin-domain').val('');
-
- } else {
-
- // Disable CORS
- var disableOption = new CORS.ConfigModel({
- section: 'httpd',
- attribute: 'enable_cors',
- value: 'false'
- });
-
- var disableCreds = new CORS.ConfigModel({
- section: 'cors',
- attribute: 'credentials',
- value: 'false'
- });
-
- var disableOrigins = new CORS.ConfigModel({
- section: 'cors',
- attribute: 'origins',
- value: ''
- });
-
- disableOption.save().then(function (response) {
- var notification = FauxtonAPI.addNotification({
- msg: 'Your settings have been saved.',
- type: 'success',
- clear: true
- });
- },
- function (response, errorCode, errorMsg) {
- var notification = FauxtonAPI.addNotification({
- msg: 'Sorry! There was an error. Code ' + errorCode + '.',
- type: 'error',
- clear: true
- });
- });
-
- disableCreds.save();
- disableOrigins.save();
- }
- }
- });
-
- Views.OriginDomainTable = FauxtonAPI.View.extend({
- template: 'addons/cors/templates/origin_domain_table',
-
- initialize: function () {
- // listen for any server-side changes to the object (i.e. saves/deletes). Only then, re-render the table
- this.listenTo(this.model, 'sync', this.render);
+ establish: function () {
+ return [this.cors.fetch(), this.httpd.fetch()];
},
- beforeRender: function () {
- var origins = this.model.get('origins');
-
- // if the stored origins are set to '*' or nothing's defined, show nothing
- if (_.isEmpty(origins) || origins === '*') {
- return;
- }
- this.showRows();
+ afterRender: function () {
+ Actions.editCors({
+ origins: this.cors.getOrigins(),
+ isEnabled: this.httpd.corsEnabled()
+ });
+ Components.renderCORS(this.el);
},
- showRows: function () {
- var originsArray = this.model.get('origins').split(',');
- _.each(originsArray, function (url, index) {
- this.insertView('#origin-domain-table tbody', new Views.OriginDomainRow({
- model: this.model,
- index: index
- }));
- }, this);
+ cleanup: function () {
+ Components.removeCORS(this.el);
}
- });
-
-
- // this gets passed the entire model so it can manipulate it directly (add/update the row)
- Views.OriginDomainRow = FauxtonAPI.View.extend({
- template: 'addons/cors/templates/origin_domain_row',
- tagName: 'tr',
-
- events: {
- 'click .js-edit': 'onEditDomain',
- 'click .js-cancel-edit': 'onCancelEditDomain',
- 'click .js-delete': 'onDeleteDomain',
- 'click .js-save-domain': 'onSaveDomain'
- },
-
- serialize: function () {
- return {
- url: this.model.get('origins').split(',')[this.index]
- };
- },
-
- onEditDomain: function () {
-
- // show the editable field & save button
- this.$('.js-url').addClass('hide');
- this.$('.js-edit-domain').removeClass('hide');
-
- // change the edit icon to a cancel icon
- this.$('.js-edit').removeClass('js-edit fonticon-pencil')
- .addClass('js-cancel-edit fonticon-cancel').attr("title", "Click to cancel");
- this.$('.js-edit-domain input').select();
- },
-
- onCancelEditDomain: function () {
- this.$('.js-url').removeClass('hide');
- this.$('.js-edit-domain').addClass('hide');
- this.$('.js-cancel-edit').removeClass('js-cancel-edit fonticon-cancel')
- .addClass('js-edit fonticon-pencil').attr("title", "Click to edit");
- },
-
- onSaveDomain: function () {
- var newDomain = this.$('.js-edit-domain input').val();
-
- if (!CORS.validateCORSDomain(newDomain)) {
- FauxtonAPI.addNotification({
- msg: "Please enter a valid domain, starting with http/https and only containing the domain (not a subfolder).",
- type: "error",
- clear: true
- });
- return;
- }
- var domains = this.model.get('origins').split(',');
- domains[this.index] = newDomain;
- this.saveOrigins(domains);
- },
-
- // remove the domain from the list
- onDeleteDomain: function () {
-
- // remove the domain from the list. Anyone monitoring the object will hear that it's changed (e.g.
- // the main table, which will know to re-render)
- var domains = this.model.get('origins').split(',');
- domains.splice(this.index, 1);
-
- this.saveOrigins(domains);
- },
-
- saveOrigins: function (origins) {
- var originDomains = origins.toString();
-
- var allowOrigins = new CORS.ConfigModel({
- section: 'cors',
- attribute: 'origins',
- value: originDomains
- });
-
- allowOrigins.save().then(function (response) {
- var notification = FauxtonAPI.addNotification({
- msg: 'Your origin domains have been updated.',
- type: 'success',
- clear: true
- });
- },
- function (response, errorCode, errorMsg) {
- var notification = FauxtonAPI.addNotification({
- msg: 'Something went wrong.',
- type: 'error',
- clear: true
- });
- });
- }
});
-
+
CORS.Views = Views;
return CORS;
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/app/templates/layouts/with_tabs_sidebar.html
----------------------------------------------------------------------
diff --git a/app/templates/layouts/with_tabs_sidebar.html b/app/templates/layouts/with_tabs_sidebar.html
index e6acfa3..f58184d 100644
--- a/app/templates/layouts/with_tabs_sidebar.html
+++ b/app/templates/layouts/with_tabs_sidebar.html
@@ -26,7 +26,7 @@ the License.
<aside id="sidebar-content" class="scrollable"></aside>
- <section id="dashboard-content" class="list">
+ <section id="dashboard-content" class="list scrollable">
<div class="scrollable">
<div class="inner">
<div id="dashboard-upper-content"></div>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ddef3d54/assets/less/templates.less
----------------------------------------------------------------------
diff --git a/assets/less/templates.less b/assets/less/templates.less
index c0b6515..1e017ef 100644
--- a/assets/less/templates.less
+++ b/assets/less/templates.less
@@ -417,7 +417,7 @@ with_tabs_sidebar.html
bottom: 0px;
top: @collapsedNavWidth;
position: fixed;
- overflow: hidden;
+ overflow-x: hidden;
left: @sidebarWidth+@navWidth;
right: 0;
.box-sizing(border-box);