「MediaWiki:Forum.js」の版間の差分

編集の要約なし
編集の要約なし
7行目: 7行目:
         // 設定がない場合のデフォルト値
         // 設定がない場合のデフォルト値
         mw.forum = mw.forum || {};
         mw.forum = mw.forum || {};
        mw.forum.sticky = mw.forum.sticky || [];
         mw.forum.toppage = mw.forum.toppage || 'KenryoBBS1';
         mw.forum.toppage = mw.forum.toppage || 'KenryoBBS1';
         mw.forum.threads = mw.forum.threads || 20;
         mw.forum.threads = mw.forum.threads || 20;
        mw.forum.sticky = mw.forum.sticky || [];
          
          
         const fedit = mw.util.getParamValue('fedit');
         const fedit = mw.util.getParamValue('fedit');
          
          
         // ページ判定(掲示板ページでなければ終了)
         // ページ判定(掲示板ページでなければ終了)
        // バッククォート ( ` ) に注意してください
         if(!new RegExp(`^${mw.forum.toppage}(|/.*)$`).test(mw.config.get('wgPageName')) || mw.config.get('wgAction') != 'view'){
         if(!new RegExp(`^${mw.forum.toppage}(|/.*)$`).test(mw.config.get('wgPageName')) || mw.config.get('wgAction') != 'view'){
             return;
             return;
41行目: 40行目:
             const isAdmin = mw.config.get('wgUserGroups').includes('sysop');
             const isAdmin = mw.config.get('wgUserGroups').includes('sysop');


             // ユーザー名の決定
             // ユーザー名(IPなど)をハッシュ化して動物インデックスに変換
             const stringToHash = (str) => {
             const stringToHash = (str) => {
                 let hash = 0;
                 let hash = 0;
52行目: 51行目:
                 return Math.abs(hash);
                 return Math.abs(hash);
             };
             };
            // ========== トリップ機能 ==========
            // SHA-256を使ってトリップコードを生成する
            // 入力: 秘密のキー文字列
            // 出力: "◆" + Base64の先頭10文字
            const computeTrip = async (key) => {
                const encoder = new TextEncoder();
                const data = encoder.encode(key);
                const hashBuffer = await crypto.subtle.digest('SHA-256', data);
                const bytes = new Uint8Array(hashBuffer);
                // バイト列をBase64文字列に変換
                let binary = '';
                for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
                const base64 = btoa(binary);
                return '◆' + base64.slice(0, 10);
            };
            // localStorageから匿名ユーザーの名前設定を読み込む
            const loadAnonSettings = () => {
                try {
                    const saved = localStorage.getItem('forum_anon_settings');
                    return saved ? JSON.parse(saved) : null;
                } catch(e) { return null; }
            };
            // 匿名ユーザーの名前設定をlocalStorageに保存する
            const saveAnonSettings = (name, tripKey) => {
                try {
                    localStorage.setItem('forum_anon_settings', JSON.stringify({ name, tripKey }));
                } catch(e) {}
            };
            // ======================================


             (async()=>{
             (async()=>{
                 if (isAnon) {
                 if (isAnon) {
                    // デフォルトの匿名名(IPハッシュベース)
                     const identifier = mw.user.getName() || mw.user.sessionId();
                     const identifier = mw.user.getName() || mw.user.sessionId();
                     const index = stringToHash(identifier) % animals.length;
                     const index = stringToHash(identifier) % animals.length;
                     mw.forum.username = '匿名' + animals[index];
                     const defaultName = '匿名' + animals[index];
 
                    // 保存済み設定があればそれを使う
                    const saved = loadAnonSettings();
                    mw.forum.anonDefaultName = defaultName;
                    mw.forum.anonName = (saved && saved.name) ? saved.name : defaultName;
                    mw.forum.anonTripKey = (saved && saved.tripKey) ? saved.tripKey : '';
 
                    // トリップがあれば計算してusernameに反映
                    if (mw.forum.anonTripKey) {
                        const trip = await computeTrip(mw.forum.anonTripKey);
                        mw.forum.username = mw.forum.anonName + trip;
                    } else {
                        mw.forum.username = mw.forum.anonName;
                    }
                 } else {
                 } else {
                     mw.forum.username = mw.config.get('wgUserName');
                     mw.forum.username = mw.config.get('wgUserName');
                 }
                 }
                 if(!mw.forum.username) {
                 if(!mw.forum.username) {
                     // フォールバック
                     // フォールバック
77行目: 124行目:
             // メイン処理関数
             // メイン処理関数
             function startForum() {
             function startForum() {
                // ========== 名前・トリップ設定UIのHTML ==========
                // 匿名ユーザーのみ投稿フォームの上部に表示される
                const anonSettingsHtml = isAnon ? `<details id="f-name-settings" style="margin-bottom:.8em;border:1px solid #ccc;padding:.5em;border-radius:3px;">
<summary style="cursor:pointer;font-weight:bold;user-select:none;">▼ 名前・トリップの設定</summary>
<div style="margin-top:.5em;">
  <p style="margin:.2em 0 .5em;font-size:.9em;color:#555;">
    トリップキーを設定すると、投稿名の後ろに <b>◆記号+固有コード</b> が付き、なりすましを防げます。<br>
    キーは他人に見えませんが、同じキーからは常に同じコードが生成されます。
  </p>
  <label style="display:block;margin-bottom:.4em;">
    名前:
    <input type="text" id="f-anon-name" class="mw-ui-input" placeholder="例: 名無しさん" style="width:200px;display:inline-block;margin-left:.5em;">
  </label>
  <label style="display:block;margin-bottom:.4em;">
    トリップキー:
    <input type="password" id="f-anon-trip" class="mw-ui-input" placeholder="秘密のキー(省略可)" style="width:200px;display:inline-block;margin-left:.5em;" autocomplete="new-password">
  </label>
  <span id="f-trip-preview" style="font-size:.9em;color:#555;display:block;margin-bottom:.4em;"></span>
  <input type="button" id="f-save-name" value="設定を保存" class="mw-ui-button">
</div>
</details>` : '';
                // =================================================
                 const msg = Object.assign({
                 const msg = Object.assign({
                     loading: '<p>読み込み中...</p>',
                     loading: '<p>読み込み中...</p>',
                     postform: `<div id="f-form"><h2>投稿${fedit ? `の編集 (<a href="#post-${fedit}")>#${fedit}</a>)` : ''}</h2><div class="mw-ui-checkbox" style="margin-bottom:.3em;${fedit ? 'display:none;' : ''}"><input type="checkbox" class="mw-ui-checkbox" id="f-reply-cb"><label for="f-reply-cb" style="user-select:none;">返信する</label></div><input type="number" id="f-reply" class="mw-ui-input" style="widht:50%;margin-bottom:.5em;"><textarea accesskey="," id="wpTextbox1" cols="80" rows="25" class="mw-editfont-monospace"></textarea><input type="button" id="f-post" value="投稿" class="mw-ui-button mw-ui-progressive" style="margin-top:.5em;"><input type="button" id="f-preview" value="プレビュー" class="mw-ui-button" style="margin: .5em 0 0 .5em;"><fieldset hidden><legend>プレビュー</legend><div id="f-preview-content"></div></fieldset><style>.mw-ui-checkbox:has(#f-reply-cb:checked)+input{display:block;}#f-reply{display:none;}</style></div>`,
                     postform: `<div id="f-form"><h2>投稿${fedit ? `の編集 (<a href="#post-${fedit}")>#${fedit}</a>)` : ''}</h2>${anonSettingsHtml}<div class="mw-ui-checkbox" style="margin-bottom:.3em;${fedit ? 'display:none;' : ''}"><input type="checkbox" class="mw-ui-checkbox" id="f-reply-cb"><label for="f-reply-cb" style="user-select:none;">返信する</label></div><input type="number" id="f-reply" class="mw-ui-input" style="widht:50%;margin-bottom:.5em;"><textarea accesskey="," id="wpTextbox1" cols="80" rows="25" class="mw-editfont-monospace"></textarea><input type="button" id="f-post" value="投稿" class="mw-ui-button mw-ui-progressive" style="margin-top:.5em;"><input type="button" id="f-preview" value="プレビュー" class="mw-ui-button" style="margin: .5em 0 0 .5em;"><fieldset hidden><legend>プレビュー</legend><div id="f-preview-content"></div></fieldset><style>.mw-ui-checkbox:has(#f-reply-cb:checked)+input{display:block;}#f-reply{display:none;}</style></div>`,
                     postsummary: 'post',
                     postsummary: 'post',
                     replysummary: 'reply to',
                     replysummary: 'reply to',
151行目: 222行目:
                             mw.notify(msg.deleteerror);
                             mw.notify(msg.deleteerror);
                         });
                         });
                    },
                    // ========== 名前設定UIの初期化 ==========
                    initAnonSettings: () => {
                        if (!isAnon) return;
                        const nameInput = document.querySelector('#f-anon-name');
                        const tripInput = document.querySelector('#f-anon-trip');
                        const preview  = document.querySelector('#f-trip-preview');
                        const saveBtn  = document.querySelector('#f-save-name');
                        if (!nameInput || !tripInput) return;
                        // 保存済み値をフォームに反映
                        nameInput.value = mw.forum.anonName;
                        tripInput.value = mw.forum.anonTripKey;
                        // プレビュー更新関数
                        const updatePreview = async () => {
                            const name = nameInput.value.trim() || mw.forum.anonDefaultName;
                            const key  = tripInput.value;
                            if (key) {
                                const trip = await computeTrip(key);
                                preview.textContent = `投稿名プレビュー: ${name}${trip}`;
                            } else {
                                preview.textContent = `投稿名プレビュー: ${name}`;
                            }
                        };
                        nameInput.addEventListener('input', updatePreview);
                        tripInput.addEventListener('input', updatePreview);
                        updatePreview(); // 初期表示
                        // 保存ボタン
                        saveBtn.onclick = async () => {
                            const newName  = nameInput.value.trim() || mw.forum.anonDefaultName;
                            const newTripKey = tripInput.value;
                            saveAnonSettings(newName, newTripKey);
                            mw.forum.anonName    = newName;
                            mw.forum.anonTripKey = newTripKey;
                            if (newTripKey) {
                                const trip = await computeTrip(newTripKey);
                                mw.forum.username = newName + trip;
                            } else {
                                mw.forum.username = newName;
                            }
                            mw.notify('名前設定を保存しました。次の投稿から反映されます。');
                        };
                     }
                     }
                    // =========================================
                 };
                 };


                 const mc = document.querySelector('#mw-content-text>.mw-parser-output');
                 const mc = document.querySelector('#mw-content-text>.mw-parser-output');
                 if(!mc) return; // コンテンツエリアが見つからない場合は終了
                 if(!mc) return;


                 if(mw.config.get('wgPageName') == mw.forum.toppage){
                 if(mw.config.get('wgPageName') == mw.forum.toppage){
168行目: 284行目:
                                 content += `{{post|System|0|{{subst:#timel:Y/m/d H:i}}|4=${mw.forum.zeroTemplate}}}\n`;
                                 content += `{{post|System|0|{{subst:#timel:Y/m/d H:i}}|4=${mw.forum.zeroTemplate}}}\n`;
                             }
                             }
                             content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}}}`;
                            // スレッド作成者がログイン済みかどうかで anon フラグを付与
                            const anonParam = isAnon ? '|anon=1' : '';
                             content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}${anonParam}}}`;
                             new mw.Api().postWithToken('csrf', {
                             new mw.Api().postWithToken('csrf', {
                                 action: 'edit',
                                 action: 'edit',
214行目: 332行目:
                     const indicators = document.querySelector('.mw-indicators');
                     const indicators = document.querySelector('.mw-indicators');
                     if(indicators) indicators.innerHTML = msg.gotoform;
                     if(indicators) indicators.innerHTML = msg.gotoform;
                    // 名前設定UIを初期化(匿名ユーザーのみ)
                    func.initAnonSettings();
                      
                      
                     document.querySelector('#f-post').onclick = (async() => {
                     document.querySelector('#f-post').onclick = (async() => {
228行目: 349行目:
                             summary = `${msg.postsummary} [[${mw.config.get('wgPageName')}#post-${lp}|#${lp}]]`;
                             summary = `${msg.postsummary} [[${mw.config.get('wgPageName')}#post-${lp}|#${lp}]]`;
                         }
                         }
                        // 匿名ユーザーには |anon=1 を付与してテンプレートでリンクを非表示にする
                        const anonParam = isAnon ? '|anon=1' : '';
                         new mw.Api().postWithToken('csrf', {
                         new mw.Api().postWithToken('csrf', {
                             action: 'edit',
                             action: 'edit',
                             title: mw.config.get('wgPageName'),
                             title: mw.config.get('wgPageName'),
                             [fedit ? 'text' : 'appendtext']: fedit ? source.replace(new RegExp(`\\{\\{post\\|(.*?)\\|${fedit}\\|(.*?)\\|4=((.|\n)*?)}}`), `{{post|$1|${fedit}|$2|4=${document.querySelector('#wpTextbox1').value}}}`) :`\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re='+document.querySelector('#f-reply').value : ''}}}`,
                             [fedit ? 'text' : 'appendtext']: fedit
                                ? source.replace(new RegExp(`\\{\\{post\\|(.*?)\\|${fedit}\\|(.*?)\\|4=((.|\n)*?)}}`), `{{post|$1|${fedit}|$2|4=${document.querySelector('#wpTextbox1').value}}}`)
                                : `\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re='+document.querySelector('#f-reply').value : ''}${anonParam}}}`,
                             summary: summary,
                             summary: summary,
                             format: 'json'
                             format: 'json'
281行目: 408行目:
     };
     };
      
      
    // 確実に読み込まれるように少し待つか、即時実行
     if (window.mw && window.mw.loader) {
     if (window.mw && window.mw.loader) {
         checkConfig();
         checkConfig();
     } else {
     } else {
        // 万が一ロード前ならイベントを待つ
         window.addEventListener('load', checkConfig);
         window.addEventListener('load', checkConfig);
     }
     }
})();
})();
// </nowiki>
// </nowiki>