form.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. const Mn = require('backbone.marionette');
  2. const App = require('../../main');
  3. const ProxyHostModel = require('../../../models/proxy-host');
  4. const ProxyLocationModel = require('../../../models/proxy-host-location');
  5. const template = require('./form.ejs');
  6. const certListItemTemplate = require('../certificates-list-item.ejs');
  7. const accessListItemTemplate = require('./access-list-item.ejs');
  8. const CustomLocation = require('./location');
  9. const Helpers = require('../../../lib/helpers');
  10. const i18n = require('../../i18n');
  11. const dns_providers = require('../../../../../global/certbot-dns-plugins');
  12. require('jquery-serializejson');
  13. require('selectize');
  14. module.exports = Mn.View.extend({
  15. template: template,
  16. className: 'modal-dialog',
  17. locationsCollection: new ProxyLocationModel.Collection(),
  18. ui: {
  19. form: 'form',
  20. domain_names: 'input[name="domain_names"]',
  21. forward_host: 'input[name="forward_host"]',
  22. buttons: '.modal-footer button',
  23. cancel: 'button.cancel',
  24. save: 'button.save',
  25. add_location_btn: 'button.add_location',
  26. locations_container: '.locations_container',
  27. le_error_info: '#le-error-info',
  28. certificate_select: 'select[name="certificate_id"]',
  29. access_list_select: 'select[name="access_list_id"]',
  30. ssl_forced: 'input[name="ssl_forced"]',
  31. hsts_enabled: 'input[name="hsts_enabled"]',
  32. hsts_subdomains: 'input[name="hsts_subdomains"]',
  33. http2_support: 'input[name="http2_support"]',
  34. dns_challenge_switch: 'input[name="meta[dns_challenge]"]',
  35. dns_challenge_content: '.dns-challenge',
  36. dns_provider: 'select[name="meta[dns_provider]"]',
  37. credentials_file_content: '.credentials-file-content',
  38. dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]',
  39. propagation_seconds: 'input[name="meta[propagation_seconds]"]',
  40. forward_scheme: 'select[name="forward_scheme"]',
  41. letsencrypt: '.letsencrypt'
  42. },
  43. regions: {
  44. locations_regions: '@ui.locations_container'
  45. },
  46. events: {
  47. 'change @ui.certificate_select': function () {
  48. let id = this.ui.certificate_select.val();
  49. if (id === 'new') {
  50. this.ui.letsencrypt.show().find('input').prop('disabled', false);
  51. this.ui.dns_challenge_content.hide();
  52. } else {
  53. this.ui.letsencrypt.hide().find('input').prop('disabled', true);
  54. }
  55. let enabled = id === 'new' || parseInt(id, 10) > 0;
  56. let inputs = this.ui.ssl_forced.add(this.ui.http2_support);
  57. inputs
  58. .prop('disabled', !enabled)
  59. .parents('.form-group')
  60. .css('opacity', enabled ? 1 : 0.5);
  61. if (!enabled) {
  62. inputs.prop('checked', false);
  63. }
  64. inputs.trigger('change');
  65. },
  66. 'change @ui.ssl_forced': function () {
  67. let checked = this.ui.ssl_forced.prop('checked');
  68. this.ui.hsts_enabled
  69. .prop('disabled', !checked)
  70. .parents('.form-group')
  71. .css('opacity', checked ? 1 : 0.5);
  72. if (!checked) {
  73. this.ui.hsts_enabled.prop('checked', false);
  74. }
  75. this.ui.hsts_enabled.trigger('change');
  76. },
  77. 'change @ui.hsts_enabled': function () {
  78. let checked = this.ui.hsts_enabled.prop('checked');
  79. this.ui.hsts_subdomains
  80. .prop('disabled', !checked)
  81. .parents('.form-group')
  82. .css('opacity', checked ? 1 : 0.5);
  83. if (!checked) {
  84. this.ui.hsts_subdomains.prop('checked', false);
  85. }
  86. },
  87. 'change @ui.dns_challenge_switch': function () {
  88. const checked = this.ui.dns_challenge_switch.prop('checked');
  89. if (checked) {
  90. this.ui.dns_provider.prop('required', 'required');
  91. const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
  92. if(selected_provider != '' && dns_providers[selected_provider].credentials !== false){
  93. this.ui.dns_provider_credentials.prop('required', 'required');
  94. }
  95. this.ui.dns_challenge_content.show();
  96. } else {
  97. this.ui.dns_provider.prop('required', false);
  98. this.ui.dns_provider_credentials.prop('required', false);
  99. this.ui.dns_challenge_content.hide();
  100. }
  101. },
  102. 'change @ui.dns_provider': function () {
  103. const selected_provider = this.ui.dns_provider[0].options[this.ui.dns_provider[0].selectedIndex].value;
  104. if (selected_provider != '' && dns_providers[selected_provider].credentials !== false) {
  105. this.ui.dns_provider_credentials.prop('required', 'required');
  106. this.ui.dns_provider_credentials[0].value = dns_providers[selected_provider].credentials;
  107. this.ui.credentials_file_content.show();
  108. } else {
  109. this.ui.dns_provider_credentials.prop('required', false);
  110. this.ui.credentials_file_content.hide();
  111. }
  112. },
  113. 'click @ui.add_location_btn': function (e) {
  114. e.preventDefault();
  115. const model = new ProxyLocationModel.Model();
  116. this.locationsCollection.add(model);
  117. },
  118. 'click @ui.save': function (e) {
  119. e.preventDefault();
  120. this.ui.le_error_info.hide();
  121. if (!this.ui.form[0].checkValidity()) {
  122. $('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
  123. return;
  124. }
  125. let view = this;
  126. let data = this.ui.form.serializeJSON();
  127. // Add locations
  128. data.locations = [];
  129. this.locationsCollection.models.forEach((location) => {
  130. data.locations.push(location.toJSON());
  131. });
  132. // Serialize collects path from custom locations
  133. // This field must be removed from root object
  134. delete data.path;
  135. // Manipulate
  136. data.forward_port = parseInt(data.forward_port, 10);
  137. data.block_exploits = !!data.block_exploits;
  138. data.caching_enabled = !!data.caching_enabled;
  139. data.allow_websocket_upgrade = !!data.allow_websocket_upgrade;
  140. data.http2_support = !!data.http2_support;
  141. data.hsts_enabled = !!data.hsts_enabled;
  142. data.hsts_subdomains = !!data.hsts_subdomains;
  143. data.ssl_forced = !!data.ssl_forced;
  144. if (typeof data.meta === 'undefined') data.meta = {};
  145. data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1;
  146. data.meta.dns_challenge = data.meta.dns_challenge == 1;
  147. if(!data.meta.dns_challenge){
  148. data.meta.dns_provider = undefined;
  149. data.meta.dns_provider_credentials = undefined;
  150. data.meta.propagation_seconds = undefined;
  151. } else {
  152. if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined;
  153. }
  154. if (typeof data.domain_names === 'string' && data.domain_names) {
  155. data.domain_names = data.domain_names.split(',');
  156. }
  157. // Check for any domain names containing wildcards, which are not allowed with letsencrypt
  158. if (data.certificate_id === 'new') {
  159. let domain_err = false;
  160. if (!data.meta.dns_challenge) {
  161. data.domain_names.map(function (name) {
  162. if (name.match(/\*/im)) {
  163. domain_err = true;
  164. }
  165. });
  166. }
  167. if (domain_err) {
  168. alert(i18n('ssl', 'no-wildcard-without-dns'));
  169. return;
  170. }
  171. } else {
  172. data.certificate_id = parseInt(data.certificate_id, 10);
  173. }
  174. let method = App.Api.Nginx.ProxyHosts.create;
  175. let is_new = true;
  176. if (this.model.get('id')) {
  177. // edit
  178. is_new = false;
  179. method = App.Api.Nginx.ProxyHosts.update;
  180. data.id = this.model.get('id');
  181. }
  182. this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
  183. this.ui.save.addClass('btn-loading');
  184. method(data)
  185. .then(result => {
  186. view.model.set(result);
  187. App.UI.closeModal(function () {
  188. if (is_new) {
  189. App.Controller.showNginxProxy();
  190. }
  191. });
  192. })
  193. .catch(err => {
  194. let more_info = '';
  195. if(err.code === 500 && err.debug){
  196. try{
  197. more_info = JSON.parse(err.debug).debug.stack.join("\n");
  198. } catch(e) {}
  199. }
  200. this.ui.le_error_info[0].innerHTML = `${err.message}${more_info !== '' ? `<pre class="mt-3">${more_info}</pre>`:''}`;
  201. this.ui.le_error_info.show();
  202. this.ui.le_error_info[0].scrollIntoView();
  203. this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
  204. this.ui.save.removeClass('btn-loading');
  205. });
  206. }
  207. },
  208. templateContext: {
  209. getLetsencryptEmail: function () {
  210. return App.Cache.User.get('email');
  211. },
  212. getUseDnsChallenge: function () {
  213. return typeof this.meta.dns_challenge !== 'undefined' ? this.meta.dns_challenge : false;
  214. },
  215. getDnsProvider: function () {
  216. return typeof this.meta.dns_provider !== 'undefined' && this.meta.dns_provider != '' ? this.meta.dns_provider : null;
  217. },
  218. getDnsProviderCredentials: function () {
  219. return typeof this.meta.dns_provider_credentials !== 'undefined' ? this.meta.dns_provider_credentials : '';
  220. },
  221. getPropagationSeconds: function () {
  222. return typeof this.meta.propagation_seconds !== 'undefined' ? this.meta.propagation_seconds : '';
  223. },
  224. dns_plugins: dns_providers,
  225. },
  226. onRender: function () {
  227. let view = this;
  228. this.ui.ssl_forced.trigger('change');
  229. this.ui.hsts_enabled.trigger('change');
  230. // Domain names
  231. this.ui.domain_names.selectize({
  232. delimiter: ',',
  233. persist: false,
  234. maxOptions: 15,
  235. create: function (input) {
  236. return {
  237. value: input,
  238. text: input
  239. };
  240. },
  241. createFilter: /^(?:\.)?(?:[^.*]+\.?)+[^.]$/
  242. });
  243. // Access Lists
  244. this.ui.access_list_select.selectize({
  245. valueField: 'id',
  246. labelField: 'name',
  247. searchField: ['name'],
  248. create: false,
  249. preload: true,
  250. allowEmptyOption: true,
  251. render: {
  252. option: function (item) {
  253. item.i18n = App.i18n;
  254. item.formatDbDate = Helpers.formatDbDate;
  255. return accessListItemTemplate(item);
  256. }
  257. },
  258. load: function (query, callback) {
  259. App.Api.Nginx.AccessLists.getAll(['items', 'clients'])
  260. .then(rows => {
  261. callback(rows);
  262. })
  263. .catch(err => {
  264. console.error(err);
  265. callback();
  266. });
  267. },
  268. onLoad: function () {
  269. view.ui.access_list_select[0].selectize.setValue(view.model.get('access_list_id'));
  270. }
  271. });
  272. // Certificates
  273. this.ui.le_error_info.hide();
  274. this.ui.dns_challenge_content.hide();
  275. this.ui.credentials_file_content.hide();
  276. this.ui.letsencrypt.hide();
  277. this.ui.certificate_select.selectize({
  278. valueField: 'id',
  279. labelField: 'nice_name',
  280. searchField: ['nice_name', 'domain_names'],
  281. create: false,
  282. preload: true,
  283. allowEmptyOption: true,
  284. render: {
  285. option: function (item) {
  286. item.i18n = App.i18n;
  287. item.formatDbDate = Helpers.formatDbDate;
  288. return certListItemTemplate(item);
  289. }
  290. },
  291. load: function (query, callback) {
  292. App.Api.Nginx.Certificates.getAll()
  293. .then(rows => {
  294. callback(rows);
  295. })
  296. .catch(err => {
  297. console.error(err);
  298. callback();
  299. });
  300. },
  301. onLoad: function () {
  302. view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
  303. }
  304. });
  305. },
  306. initialize: function (options) {
  307. if (typeof options.model === 'undefined' || !options.model) {
  308. this.model = new ProxyHostModel.Model();
  309. }
  310. this.locationsCollection = new ProxyLocationModel.Collection();
  311. // Custom locations
  312. this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({
  313. collection: this.locationsCollection
  314. }));
  315. // Check wether there are any location defined
  316. if (options.model && Array.isArray(options.model.attributes.locations)) {
  317. options.model.attributes.locations.forEach((location) => {
  318. let m = new ProxyLocationModel.Model(location);
  319. this.locationsCollection.add(m);
  320. });
  321. }
  322. }
  323. });