luci-mod-system: flash.js: fix flash erase modal
[oweals/luci.git] / modules / luci-mod-system / htdocs / luci-static / resources / view / system / flash.js
1 'use strict';
2 'require form';
3 'require rpc';
4 'require fs';
5
6 var callSystemValidateFirmwareImage = rpc.declare({
7         object: 'system',
8         method: 'validate_firmware_image',
9         params: [ 'path' ],
10         expect: { '': { valid: false, forcable: true } }
11 });
12
13 function findStorageSize(procmtd, procpart) {
14         var kernsize = 0, rootsize = 0, wholesize = 0;
15
16         procmtd.split(/\n/).forEach(function(ln) {
17                 var match = ln.match(/^mtd\d+: ([0-9a-f]+) [0-9a-f]+ "(.+)"$/),
18                     size = match ? parseInt(match[1], 16) : 0;
19
20                 switch (match ? match[2] : '') {
21                 case 'linux':
22                 case 'firmware':
23                         if (size > wholesize)
24                                 wholesize = size;
25                         break;
26
27                 case 'kernel':
28                 case 'kernel0':
29                         kernsize = size;
30                         break;
31
32                 case 'rootfs':
33                 case 'rootfs0':
34                 case 'ubi':
35                 case 'ubi0':
36                         rootsize = size;
37                         break;
38                 }
39         });
40
41         if (wholesize > 0)
42                 return wholesize;
43         else if (kernsize > 0 && rootsize > kernsize)
44                 return kernsize + rootsize;
45
46         procpart.split(/\n/).forEach(function(ln) {
47                 var match = ln.match(/^\s*\d+\s+\d+\s+(\d+)\s+(\S+)$/);
48                 if (match) {
49                         var size = parseInt(match[1], 10);
50
51                         if (!match[2].match(/\d/) && size > 2048 && wholesize == 0)
52                                 wholesize = size * 1024;
53                 }
54         });
55
56         return wholesize;
57 }
58
59
60 var mapdata = { actions: {}, config: {} };
61
62 return L.view.extend({
63         load: function() {
64                 var tasks = [
65                         L.resolveDefault(fs.stat('/lib/upgrade/platform.sh'), {}),
66                         fs.trimmed('/proc/sys/kernel/hostname'),
67                         fs.trimmed('/proc/mtd'),
68                         fs.trimmed('/proc/partitions'),
69                         fs.trimmed('/proc/mounts')
70                 ];
71
72                 return Promise.all(tasks);
73         },
74
75         handleBackup: function(ev) {
76                 var form = E('form', {
77                         method: 'post',
78                         action: '/cgi-bin/cgi-backup',
79                         enctype: 'application/x-www-form-urlencoded'
80                 }, E('input', { type: 'hidden', name: 'sessionid', value: rpc.getSessionID() }));
81
82                 ev.currentTarget.parentNode.appendChild(form);
83
84                 form.submit();
85                 form.parentNode.removeChild(form);
86         },
87
88         handleFirstboot: function(ev) {
89                 if (!confirm(_('Do you really want to erase all settings?')))
90                         return;
91
92                 L.ui.showModal(_('Erasing...'), [
93                         E('p', { 'class': 'spinning' }, _('The system is erasing the configuration partition now and will reboot itself when finished.'))
94                 ]);
95
96                 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
97                 fs.exec('/sbin/firstboot', [ '-r', '-y' ]);
98
99                 L.ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
100         },
101
102         handleRestore: function(ev) {
103                 return L.ui.uploadFile('/tmp/backup.tar.gz', ev.target)
104                         .then(L.bind(function(btn, res) {
105                                 btn.firstChild.data = _('Checking archiveā€¦');
106                                 return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
107                         }, this, ev.target))
108                         .then(L.bind(function(btn, res) {
109                                 if (res.code != 0) {
110                                         L.ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
111                                         return fs.remove('/tmp/backup.tar.gz');
112                                 }
113
114                                 L.ui.showModal(_('Apply backup?'), [
115                                         E('p', _('The uploaded backup archive appears to be valid and contains the files listed below. Press "Continue" to restore the backup and reboot, or "Cancel" to abort the operation.')),
116                                         E('pre', {}, [ res.stdout ]),
117                                         E('div', { 'class': 'right' }, [
118                                                 E('button', {
119                                                         'class': 'btn',
120                                                         'click': L.ui.createHandlerFn(this, function(ev) {
121                                                                 return fs.remove('/tmp/backup.tar.gz').finally(L.ui.hideModal);
122                                                         })
123                                                 }, [ _('Cancel') ]), ' ',
124                                                 E('button', {
125                                                         'class': 'btn cbi-button-action important',
126                                                         'click': L.ui.createHandlerFn(this, 'handleRestoreConfirm', btn)
127                                                 }, [ _('Continue') ])
128                                         ])
129                                 ]);
130                         }, this, ev.target))
131                         .catch(function(e) { L.ui.addNotification(null, E('p', e.message)) })
132                         .finally(L.bind(function(btn, input) {
133                                 btn.firstChild.data = _('Upload archive...');
134                         }, this, ev.target));
135         },
136
137         handleRestoreConfirm: function(btn, ev) {
138                 return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
139                         .then(L.bind(function(btn, res) {
140                                 if (res.code != 0) {
141                                         L.ui.addNotification(null, [
142                                                 E('p', _('The restore command failed with code %d').format(res.code)),
143                                                 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
144                                         ]);
145                                         L.raise('Error', 'Unpack failed');
146                                 }
147
148                                 btn.firstChild.data = _('Rebootingā€¦');
149                                 return fs.exec('/sbin/reboot');
150                         }, this, ev.target))
151                         .then(L.bind(function(res) {
152                                 if (res.code != 0) {
153                                         L.ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
154                                         L.raise('Error', 'Reboot failed');
155                                 }
156
157                                 L.ui.showModal(_('Rebootingā€¦'), [
158                                         E('p', { 'class': 'spinning' }, _('The system is rebooting now. If the restored configuration changed the current LAN IP address, you might need to reconnect manually.'))
159                                 ]);
160
161                                 L.ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
162                         }, this))
163                         .catch(function(e) { L.ui.addNotification(null, E('p', e.message)) })
164                         .finally(function() { btn.firstChild.data = _('Upload archive...') });
165         },
166
167         handleBlock: function(hostname, ev) {
168                 var mtdblock = L.dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select').value;
169                 var form = E('form', {
170                         'method': 'post',
171                         'action': '/cgi-bin/cgi-download',
172                         'enctype': 'application/x-www-form-urlencoded'
173                 }, [
174                         E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
175                         E('input', { 'type': 'hidden', 'name': 'path',      'value': '/dev/mtdblock%d'.format(mtdblock) }),
176                         E('input', { 'type': 'hidden', 'name': 'filename',  'value': '%s.mtd%d.bin'.format(hostname, mtdblock) })
177                 ]);
178
179                 ev.currentTarget.parentNode.appendChild(form);
180
181                 form.submit();
182                 form.parentNode.removeChild(form);
183         },
184
185         handleSysupgrade: function(storage_size, ev) {
186                 return L.ui.uploadFile('/tmp/firmware.bin', ev.target.firstChild)
187                         .then(L.bind(function(btn, reply) {
188                                 btn.firstChild.data = _('Checking imageā€¦');
189
190                                 L.ui.showModal(_('Checking imageā€¦'), [
191                                         E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.'))
192                                 ]);
193
194                                 return callSystemValidateFirmwareImage('/tmp/firmware.bin')
195                                         .then(function(res) { return [ reply, res ]; });
196                         }, this, ev.target))
197                         .then(L.bind(function(btn, reply) {
198                                 return fs.exec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ])
199                                         .then(function(res) { reply.push(res); return reply; });
200                         }, this, ev.target))
201                         .then(L.bind(function(btn, res) {
202                                 var keep = E('input', { type: 'checkbox' }),
203                                     force = E('input', { type: 'checkbox' }),
204                                     is_valid = res[1].valid,
205                                     is_forceable = res[1].forceable,
206                                     allow_backup = res[1].allow_backup,
207                                     is_too_big = (storage_size > 0 && res[0].size > storage_size),
208                                     body = [];
209
210                                 body.push(E('p', _('The flash image was uploaded. Below is the checksum and file size listed, compare them with the original file to ensure data integrity. <br /> Click "Proceed" below to start the flash procedure.')));
211                                 body.push(E('ul', {}, [
212                                         res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '',
213                                         res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '',
214                                         res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : ''
215                                 ]));
216
217                                 body.push(E('p', {}, E('label', { 'class': 'btn' }, [
218                                         keep, ' ', _('Keep settings and retain the current configuration')
219                                 ])));
220
221                                 if (!is_valid || is_too_big)
222                                         body.push(E('hr'));
223
224                                 if (is_too_big)
225                                         body.push(E('p', { 'class': 'alert-message' }, [
226                                                 _('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!')
227                                         ]));
228
229                                 if (!is_valid)
230                                         body.push(E('p', { 'class': 'alert-message' }, [
231                                                 res[2].stderr ? res[2].stderr : '',
232                                                 res[2].stderr ? E('br') : '',
233                                                 res[2].stderr ? E('br') : '',
234                                                 _('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.')
235                                         ]));
236
237                                 if (!allow_backup)
238                                         body.push(E('p', { 'class': 'alert-message' }, [
239                                                 _('The uploaded firmware does not allow keeping current configuration.')
240                                         ]));
241
242                                 if (allow_backup)
243                                         keep.checked = true;
244                                 else
245                                         keep.disabled = true;
246
247
248                                 if ((!is_valid || is_too_big) && is_forceable)
249                                         body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [
250                                                 force, ' ', _('Force upgrade'),
251                                                 E('br'), E('br'),
252                                                 _('Select \'Force upgrade\' to flash the image even if the image format check fails. Use only if you are sure that the firmware is correct and meant for your device!')
253                                         ])));
254
255                                 var cntbtn = E('button', {
256                                         'class': 'btn cbi-button-action important',
257                                         'click': L.ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, keep, force),
258                                         'disabled': (!is_valid || is_too_big) ? true : null
259                                 }, [ _('Continue') ]);
260
261                                 body.push(E('div', { 'class': 'right' }, [
262                                         E('button', {
263                                                 'class': 'btn',
264                                                 'click': L.ui.createHandlerFn(this, function(ev) {
265                                                         return fs.remove('/tmp/firmware.bin').finally(L.ui.hideModal);
266                                                 })
267                                         }, [ _('Cancel') ]), ' ', cntbtn
268                                 ]));
269
270                                 force.addEventListener('change', function(ev) {
271                                         cntbtn.disabled = !ev.target.checked;
272                                 });
273
274                                 L.ui.showModal(_('Flash image?'), body);
275                         }, this, ev.target))
276                         .catch(function(e) { L.ui.addNotification(null, E('p', e.message)) })
277                         .finally(L.bind(function(btn) {
278                                 btn.firstChild.data = _('Flash image...');
279                         }, this, ev.target));
280         },
281
282         handleSysupgradeConfirm: function(btn, keep, force, ev) {
283                 btn.firstChild.data = _('Flashingā€¦');
284
285                 L.ui.showModal(_('Flashingā€¦'), [
286                         E('p', { 'class': 'spinning' }, _('The system is flashing now.<br /> DO NOT POWER OFF THE DEVICE!<br /> Wait a few minutes before you try to reconnect. It might be necessary to renew the address of your computer to reach the device again, depending on your settings.'))
287                 ]);
288
289                 var opts = [];
290
291                 if (!keep.checked)
292                         opts.push('-n');
293
294                 if (force.checked)
295                         opts.push('--force');
296
297                 opts.push('/tmp/firmware.bin');
298
299                 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
300                 fs.exec('/sbin/sysupgrade', opts);
301
302                 L.ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
303         },
304
305         handleBackupList: function(ev) {
306                 return fs.exec('/sbin/sysupgrade', [ '--list-backup' ]).then(function(res) {
307                         if (res.code != 0) {
308                                 L.ui.addNotification(null, [
309                                         E('p', _('The sysupgrade command failed with code %d').format(res.code)),
310                                         res.stderr ? E('pre', {}, [ res.stderr ]) : ''
311                                 ]);
312                                 L.raise('Error', 'Sysupgrade failed');
313                         }
314
315                         L.ui.showModal(_('Backup file list'), [
316                                 E('p', _('Below is the determined list of files to backup. It consists of changed configuration files marked by opkg, essential base files and the user defined backup patterns.')),
317                                 E('ul', {}, (res.stdout || '').trim().split(/\n/).map(function(ln) { return E('li', {}, ln) })),
318                                 E('div', { 'class': 'right' }, [
319                                         E('button', {
320                                                 'class': 'btn',
321                                                 'click': L.ui.hideModal
322                                         }, [ _('Dismiss') ])
323                                 ])
324                         ], 'cbi-modal');
325                 });
326         },
327
328         handleBackupSave: function(m, ev) {
329                 return m.save(function() {
330                         return fs.write('/etc/sysupgrade.conf', mapdata.config.editlist.trim().replace(/\r\n/g, '\n') + '\n');
331                 }).then(function() {
332                         L.ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
333                 }).catch(function(e) {
334                         L.ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e)));
335                 });
336         },
337
338         render: function(rpc_replies) {
339                 var has_sysupgrade = (rpc_replies[0].type == 'file'),
340                     hostname = rpc_replies[1],
341                     procmtd = rpc_replies[2],
342                     procpart = rpc_replies[3],
343                     procmounts = rpc_replies[4],
344                     has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:\/overlay \/ ") != null),
345                     storage_size = findStorageSize(procmtd, procpart),
346                     m, s, o, ss;
347
348                 m = new form.JSONMap(mapdata, _('Flash operations'));
349                 m.tabbed = true;
350
351                 s = m.section(form.NamedSection, 'actions', _('Actions'));
352
353
354                 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Backup'), _('Click "Generate archive" to download a tar archive of the current configuration files.'));
355                 ss = o.subsection;
356
357                 o = ss.option(form.Button, 'dl_backup', _('Download backup'));
358                 o.inputstyle = 'action important';
359                 o.inputtitle = _('Generate archive');
360                 o.onclick = this.handleBackup;
361
362
363                 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Restore'), _('To restore configuration files, you can upload a previously generated backup archive here. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).'));
364                 ss = o.subsection;
365
366                 if (has_rootfs_data) {
367                         o = ss.option(form.Button, 'reset', _('Reset to defaults'));
368                         o.inputstyle = 'negative important';
369                         o.inputtitle = _('Perform reset');
370                         o.onclick = this.handleFirstboot;
371                 }
372
373                 o = ss.option(form.Button, 'restore', _('Restore backup'), _('Custom files (certificates, scripts) may remain on the system. To prevent this, perform a factory-reset first.'));
374                 o.inputstyle = 'action important';
375                 o.inputtitle = _('Upload archive...');
376                 o.onclick = L.bind(this.handleRestore, this);
377
378
379                 if (procmtd.length) {
380                         o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Save mtdblock contents'), _('Click "Save mtdblock" to download specified mtdblock file. (NOTE: THIS FEATURE IS FOR PROFESSIONALS! )'));
381                         ss = o.subsection;
382
383                         o = ss.option(form.ListValue, 'mtdselect', _('Choose mtdblock'));
384                         procmtd.split(/\n/).forEach(function(ln) {
385                                 var match = ln.match(/^mtd(\d+): .+ "(.+?)"$/);
386                                 if (match)
387                                         o.value(match[1], match[2]);
388                         });
389
390                         o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
391                         o.inputstyle = 'action important';
392                         o.inputtitle = _('Save mtdblock');
393                         o.onclick = L.bind(this.handleBlock, this, hostname);
394                 }
395
396
397                 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
398                         has_sysupgrade
399                                 ? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
400                                 : _('Sorry, there is no sysupgrade support present; a new firmware image must be flashed manually. Please refer to the wiki for device specific install instructions.'));
401
402                 ss = o.subsection;
403
404                 if (has_sysupgrade) {
405                         o = ss.option(form.Button, 'sysupgrade', _('Image'));
406                         o.inputstyle = 'action important';
407                         o.inputtitle = _('Flash image...');
408                         o.onclick = L.bind(this.handleSysupgrade, this, storage_size);
409                 }
410
411
412                 s = m.section(form.NamedSection, 'config', 'config', _('Configuration'), _('This is a list of shell glob patterns for matching files and directories to include during sysupgrade. Modified files in /etc/config/ and certain other configurations are automatically preserved.'));
413                 s.render = L.bind(function(view /*, ... */) {
414                         return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
415                                 .then(L.bind(function(node) {
416                                         node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
417                                                 E('button', {
418                                                         'class': 'cbi-button cbi-button-save',
419                                                         'click': L.ui.createHandlerFn(view, 'handleBackupSave', this.map)
420                                                 }, [ _('Save') ])
421                                         ]));
422
423                                         return node;
424                                 }, this));
425                 }, s, this);
426
427                 o = s.option(form.Button, 'showlist', _('Show current backup file list'));
428                 o.inputstyle = 'action';
429                 o.inputtitle = _('Open list...');
430                 o.onclick = L.bind(this.handleBackupList, this);
431
432                 o = s.option(form.TextValue, 'editlist');
433                 o.forcewrite = true;
434                 o.rows = 30;
435                 o.load = function(section_id) {
436                         return L.resolveDefault(fs.read('/etc/sysupgrade.conf'), '');
437                 };
438
439
440                 return m.render();
441         },
442
443         handleSaveApply: null,
444         handleSave: null,
445         handleReset: null
446 });