Памятка, если вас посылают на войну против вашей воли (обновлено 23/09)Помогите завершить войну — поддержите армию Украины
(в том числе анонимно криптовалютой) -- адм. toriningen
(в том числе анонимно криптовалютой) -- адм. toriningen
MediaWiki:Modules/signed-embed.js
Материал из Мракопедии
Замечание. Возможно, после сохранения вам придётся очистить кэш своего браузера, чтобы увидеть изменения.
- Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl-F5 или Ctrl-R (⌘-R на Mac)
- Google Chrome: Нажмите Ctrl-Shift-R (⌘-Shift-R на Mac)
- Internet Explorer: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl-F5
- Opera: Перейдите в Menu → Настройки (Opera → Настройки на Mac), а затем Безопасность → Очистить историю посещений → Кэшированные изображения и файлы
/******************************************************************************** * Этот модуль занимается тем, что обрабатывает подписанные включения. * * == Зачем это нужно? * У тех, кто имеет возможность редактировать эту страницу (т.е. администраторов), * теперь есть еще и бонусная возможность встраивать HTML-код, JavaScript и CSS * без ограничений прямо в историях. * * Т.е. теперь можно сделать отдельно взятую историю более интерактивной, и не * городить велосипеды в Common.js, и не замусоривать Common.css стилями для * отдельной истории. * * == Как этим пользоваться? * Если нужно добавить JavaScript, в историю нужно добавить тэг следующего вида: * <pre data-type="text/javascript"> * alert('Привет, я жабоскрипт!'); * $('body').html('Эта история говно, поэтому мы ее больше не показываем'); * </pre> * * Если нужно добавить HTML, то тэг приобретает следующий вид: * <pre data-type="text/html"> * <button id="knopa">Ого, да я же самая настоящая кнопка!</button> * <script> * $('#knopa').click(function() { * alert('Тут еще и обработчики! Вот это да!'); * }); * </script> * </pre> * * Если нужно добавить CSS, то тэг приобретает следующий вид: * <pre data-type="text/css" data-id="my-style-tag"> * body { * background: url(http://...); * } * </pre> * * Отступы приведены исключительно для красоты оформления и ни на что не влияют. * * Затем, когда тэг добавлен, его нужно подписать. Для этого нужно нажать на * красную кнопку в виде восковой печати, и все волшебство случится автоматически. * * Вот, в принципе, и все. Неподписанные (или плохо подписанные) включения * заменяются на сообщение об ошибке в стиле Mediawiki с объяснением, что пошло * не так. Если будут вопросы — обращайстесь к toriningen-у, он эту херь породил. ********************************************************************************/ ~function() { waitFor('deps.esShim && deps.elliptic', function() { setupSignedEmbed({ // ДОПИСЫВАЙТЕ СЮДА СВОИ ПУБЛИЧНЫЕ КЛЮЧИ "toriningen;1464505906296": "A6r5aaUrzRVMkscRd9Tb7DLChwE9pFEsqj/eknsETZp5", // НАД ЭТОЙ СТРОКОЙ }); setupSignedEmbedToolbar(); }); // Обработчики различных типов включений, можно добавлять новые. var mimeHandlers = { 'text/html': function($embed, source) { $embed.replaceWith(source); }, 'text/javascript': function($embed, source) { function run() { try { eval(source); } catch(e) { console.error(e); } } setTimeout(function() { var pred = $embed.data('waitfor'); if (pred != null) { waitFor(pred, run); } else { run(); } }, 0); $embed.remove(); }, 'text/css': function($embed, source) { $('<style>') .html(source) .attr('id', $embed.data('id')) .appendTo($('head')); $embed.remove(); }, }; var pubkeys; function setupSignedEmbed(b64pubkeys) { pubkeys = parseKeys(b64pubkeys); var selector = 'pre[data-type],pre[data-signature],pre[data-signee]'; $(selector).each(function() { decodeEmbed(this); }); waitFor('deps.insq', function() { insertionQ(selector).every(function(el) { decodeEmbed(el); }); }); } function setupSignedEmbedToolbar() { if (!window.localStorage) { return; } function extend() { mw.toolbar.addButton({ imageFile: '//mrakopedia.org/w/images/5/5f/Sign-embed.png', speedTip: 'Переподписать все включения с неверной подписью', imageId: 'button-sign-embed', tagOpen: '', tagClose: '', sampleText: '', }); $('#button-sign-embed').css({ 'margin-left': '0.5em', }).click(function() { mw.loader.using('jquery.color', function() { toolbarSign(); }); }); mw.toolbar.addButton({ imageFile: '//mrakopedia.org/w/images/f/f1/Sign-embed-settings.png', speedTip: 'Настройки криптоподписи', imageId: 'button-sign-embed-settings', tagOpen: '', tagClose: '', sampleText: '', }); $('#button-sign-embed-settings').click(function() { toolbarConfigure(); }); } if (mw.toolbar == null) { return; } mw.loader.using('user.options', function() { if (mw.user.options.get('showtoolbar') == 1) { // Показывать кнопки только для администраторов и желающих if (+localStorage.enableSignedEmbedToolbar || $.inArray('sysop', mw.config.get('wgUserGroups')) !== -1) { waitFor('deps.elliptic && deps.toolbar', function() { extend(); }); } } }); } function validateEmbed($embed) { var mime = $embed.data('type'); if (mime == null) { return 'mime-missing'; } if (mimeHandlers[mime] == null) { return 'mime-unknown'; } var signee = $embed.data('signee'); if (signee == null) { return 'signee-missing'; } var pubkey = pubkeys.get(signee); if (pubkey == null) { return 'signee-unknown'; } var b64signature = $embed.data('signature'); if (b64signature == null) { return 'signature-missing'; } try { var signature = elliptic.bitcoin.ECSignature.fromDER(new elliptic.buffer.Buffer(b64signature, 'base64')); } catch(e) { console.error('signature decode', $embed[0], e); return 'signature-malformed'; } var source = $embed.text(); var hash = hashEmbed($embed, source, false); try { if (!pubkey.verify(hash, signature)) { return 'signature-broken'; } } catch(e) { console.error('signature verify', $embed[0], e); return 'signature-broken'; } return null; } function decodeEmbed(embed) { var $embed = $(embed); $embed.css({'display': 'none'}); var err = validateEmbed($embed); if (err) { var msg = { 'mime-missing': 'У включения не указан тип: добавьте аттрибут data-type', 'mime-unknown': 'У включения указан неизвестный тип: поддерживаются <code>' + Object.keys(mimeHandlers).join(', ') + '</code>', 'signee-missing': 'У включения не указан автор: добавьте аттрибут data-signee', 'signee-unknown': 'Неизвестный автор включения: добавьте ваш открытый ключ в <a href="//mrakopedia.org/wiki/MediaWiki:Modules/signed-embed.js">MediaWiki:Modules/signed-embed.js</a>', 'signature-missing': 'У включения нет подписи: для пересоздания нажмите на кнопку с восковой печатью', 'signature-malformed': 'Повреждена подпись: для пересоздания нажмите на кнопку с восковой печатью', 'signature-broken': 'У включения неправильная подпись: для пересоздания нажмите на кнопку с восковой печатью', }[err]; $embed.replaceWith($('<b class="error">').html(msg + '<br>')) return; } var source = $embed.text(); var mime = $embed.data('type'); mimeHandlers[mime]($embed, source); } function parseKeys(b64pubkeys) { var signees = Object.keys(b64pubkeys); var pubkeys = new Map(); for (var i = 0; i < signees.length; i++) { var signee = signees[i]; var pubkeyBuf = new elliptic.buffer.Buffer(b64pubkeys[signee], 'base64'); var pubkey = elliptic.bitcoin.ECPair.fromPublicKeyBuffer(pubkeyBuf); pubkeys.set(signee, pubkey); } return pubkeys; } function hashEmbed($embed, source, trimLeadingNewline) { if (trimLeadingNewline && (source.charAt(0) === '\n')) { // MediaWiki содержит "улучшение" - если сразу за открытием тэга идет перенос строки, то он не учитывается при рендеринге. Мне приходится учитывать еще и чужие костыли :с source = source.substr(1); } var mime = JSON.stringify($embed.data('type')); var id = JSON.stringify($embed.data('id')); var waitfor = JSON.stringify($embed.data('waitfor')); var pack = [ mime, id, waitfor, source, ].join('\n'); return elliptic.bitcoin.crypto.sha256(pack); } function toolbarSign() { var rx = /(<pre .*?>)([\s\S]*?)<\/pre>/ig; var content = $('#wpTextbox1').val(); var match; var key = readStoredKey(); if (key === 'missing') { alert('А у вас докУментов нету. Зайдите в настройки, тут рядом - первая кнопка направо.'); return; } if (key === 'broken') { alert('У вас ключ отвалился. Зайдите в настройки, тут рядом - первая кнопка направо, и загляните в экспорт.'); return; } $('#wpTextbox1').val(content.replace(rx, function(full, tag, source) { var $embed = $(tag); var mime = $embed.data('type'); $embed.html(source); var err = validateEmbed($embed); if (err == null) { // переподписывать не нужно, оно и так работает return full; } else if (err === 'mime-missing') { // без типа - подписывать нельзя return full; } else { var hash = hashEmbed($embed, source, true); var signature = key.privkey.sign(hash); var b64signature = signature.toDER().toString('base64'); $embed .attr('data-signature', b64signature) .attr('data-signee', key.id) .text(''); // HAAAAAX tag = $embed.wrapAll('<p>').parent().html().slice(0, -6); // cut off closing tag var rep = tag + source + '</' + 'pre>'; return rep; } })); $('#wpTextbox1') .css("background-color", "#9CFF9C") .animate({ backgroundColor: "#FFFFFF"}, 1000); } function menu(opts) { var msg; var keys = {}; var defaultOpt = ''; var catchall = menuIamStupid; msg = (opts.title || '') + '\n\n'; if (opts.prompt) { msg += opts.prompt + '\n'; } for (var i = 0; i < opts.options.length; i++) { var opt = opts.options[i]; if (opt.default) { defaultOpt = opt.key; } if (opt.catchall) { catchall = opt.action; } msg += opt.key + ') ' + (opt.title || '') + '\n'; keys[opt.key] = opt.action; } while (true) { var choice = prompt(msg, defaultOpt); if (choice == null) { throw new Error('fallthrough exit'); } var rv; if (keys[choice]) { rv = keys[choice](); } else { rv = catchall(); } if (rv) { break; } } } function menuMain() { return menu({ title: '=== Настройки подписи включений ===', prompt: 'Чего изволите, любезнейший?', options: [ { key: '1', title: 'Расскажите, что это за хрень?', action: menuAbout, }, { key: '2', title: 'Создайте новый ключ', action: menuCreate, }, { key: '3', title: 'Мне снова нужен публичный ключ для той странички', action: menuExport, }, { key: '4', title: 'Удалите сохраненный ключ', action: menuClear, }, { key: '0', default: true, title: 'Всего доброго, хорошего настроения и здоровья', action: menuBye, }, ] }); } function menuIamStupid() { alert('Введите цифру, которая соответствует вашему выбору.'); } function menuAbout() { alert([ 'Здравствуйте. Я, Кирилл. Хотел чтобы вы сделали модуль, суть токова. Администратор может добавлять к любым статьям жабоскрипт, CSS-стили без ограничений и сырой HTML-код, и для этого не нужно править Common.js/.css и изобретать велосипеды по отфильтровке адреса текущей статьи. Можно грабить корованы.', '', 'Так как эта функциональность должна использоваться только администраторами, во избежание набигания злодеев и деревянных домиков, все подобные включения сопровождаются криптоподписью ECDSA secp256k1. Несмотря на то, что создать ключ и подписать вложения может каждый желающий, внести свой ключ в список доверенных могут только администраторы.', '', 'Инструкция по использованию вскоре появится, если ленивая жопа toriningen ее напишет. Пока ориентируйтесь на живые образцы или спросите меня лично.', ].join('\n')); } function menuCreate() { var storedKey = readStoredKey(); if (storedKey === 'missing') { return menuCreateProceed(); } if (storedKey === 'broken') { return menuCreateExistsBroken(); } return menuCreateExists(storedKey); } function menuCreateExistsBroken() { return menu({ title: 'Ой, а у вас уже есть какой-то. Только он не читается.', prompt: 'Ваши действия?', options: [ { key: '1', title: 'Игнорируем! Пишите поверх!', action: menuCreateProceed, }, { key: '2', title: 'Вы знаете, я передумал.', action: menuCreateAbort, }, ] }); } function menuCreateExists(storedKey) { return menu({ title: 'Ой, а у вас уже есть такой. Называется ' + JSON.stringify(storedKey.id) + '.', prompt: 'Ваши действия?', options: [ { key: '1', title: 'Игнорируем! Пишите поверх!', action: menuCreateProceed, }, { key: '2', title: 'Вы знаете, я передумал.', action: menuCreateAbort, }, ] }); } function menuCreateProceed() { var defaultId = mw.user.getName() + ';' + (+new Date()); var id; var msg = 'Как назовем? В принципе, можно смело соглашаться с вариантом по-умолчанию.'; while (true) { id = prompt(msg, defaultId).trim(); if (!id) { msg = 'Совсем без названия нельзя. Давайте еще раз?'; continue; } else if (pubkeys.get(id)) { msg = 'У кого-то уже есть ключ с таким названием. Давайте еще раз?'; continue; } break; } try { var privkey = elliptic.bitcoin.ECPair.makeRandom(); var wif = privkey.toWIF(); } catch(e) { showErrorReport(e); throw e; } var key = { id: id, wif: wif }; localStorage.signedEmbedKey = JSON.stringify(key); var pubkey = privkey.getPublicKeyBuffer().toString('base64'); var pubkeyStr = ' ' + JSON.stringify(id) + ': ' + JSON.stringify(pubkey) + ','; prompt([ 'Отлично! Ваш новый ключ создан и готов к работе.', '', 'Последний штрих — скопируйте строчку ниже, и вставьте ее в указанное место на странице, которая сейчас откроется. Таким образом вы добавите свой открытый ключ в доверенные.' ].join('\n'), pubkeyStr); window.open('https://mrakopedia.org/w/index.php?title=MediaWiki:Modules/signed-embed.js&action=edit'); return true; } function menuCreateAbort() { return true; } function menuExport() { var storedKey = readStoredKey(); if (storedKey === 'missing') { return menu({ title: 'А у вас пока ключей нет, нечего экспортировать :(', prompt: 'Хотите, создадим новый?', options: [ { key: '1', title: 'Ну конечно, вы еще спрашиваете!', action: menuCreateProceed, }, { key: '2', title: 'Вы знаете, я передумал.', action: menuCreateAbort, }, ] }); } if (storedKey === 'broken') { prompt([ 'Увы!', '', 'У вас вместо ключа — нечитающаяся фигня. Вот она, в строчке ниже, можете скопировать и посмотреть сами.' ].join('\n'), '!!! ЭТО НЕ КЛЮЧ !!! ' + localStorage.signedEmbedKey); } var pubkey = storedKey.privkey.getPublicKeyBuffer().toString('base64'); var pubkeyStr = ' ' + JSON.stringify(storedKey.id) + ': ' + JSON.stringify(pubkey) + ','; prompt([ 'Держите, вот ваш публичный ключ.', '', 'Скопируйте строчку ниже, и вставьте ее в указанное место на странице, которая сейчас откроется.', ].join('\n'), pubkeyStr); window.open('https://mrakopedia.org/w/index.php?title=MediaWiki:Modules/signed-embed.js&action=edit'); return true; } function menuClear() { return menu({ title: 'Тоооочно?', options: [ { key: 'дя', title: 'Да, я хочу все удалить', action: function() { localStorage.removeItem('signedEmbedKey'); alert('Готово!'); return true; }, }, { key: 'неть', default: true, title: 'Вы знаете, я передумал.', action: function () { return true; }, }, ] }); } function menuBye() { alert('Держитесь там!'); return true; } function readStoredKey() { var storedKey = localStorage.signedEmbedKey; if (!storedKey) { return 'missing'; } try { storedKey = JSON.parse(storedKey); } catch(e) { console.error(e); return 'broken'; } try { storedKey.privkey = elliptic.bitcoin.ECPair.fromWIF(storedKey.wif); } catch(e) { console.error(e); return 'broken'; } return storedKey; } function showErrorReport(e) { console.error(e); var errorReport = JSON.stringify({ str: e.toString && e.toString(), src: e.toSource && e.toSource(), columnNumber: e.columnNumber, fileName: e.fileName, lineNumber: e.lineNumber, message: e.message, name: e.name, stack: e.stack, }); prompt([ 'Ой!', '', 'Что-то пошло не так, и у нас ничего не вышло :(', '', 'Давайте сделаем вот что — скопируйте вот эту строчку ниже и отправьте ее toriningen-у, а он посмотрит и попробует исправить. Может, там не все так страшно.', ].join('\n'), errorReport); } function toolbarConfigure() { try { while (menuMain()) {} } catch(e) { if (e.msg !== 'fallthrough exit') { throw e; } } } }();