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