diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..fd4282a --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,22 @@ +name: Validate legacy HTA wiki + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Validate HTA JavaScript and documentation + run: python Tests/validate.py diff --git a/README.md b/README.md index a6e6ae5..11024e2 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,121 @@ # MarkdownLocalWiki -MarkDown記法が使える、ディスクトップ上で動くwiki -個人ブログで管理していたものをGitHubに展開 -http://kazu-s-diary-2.cocolog-nifty.com/blog/2016/07/htajquerymark-1.html +WindowsのHTML Application(HTA)として動作し、MarkdownファイルをローカルPC上で閲覧・編集する個人用Wikiです。Webサーバーやデータベースは不要で、Wikiページはリポジトリ直下の`.md`ファイルとして保存されます。 -・サーバ不要、WindowsOSであれば利用可能 +> [!CAUTION] +> このリポジトリはHTA/`mshta.exe`を使うレガシー版です。HTAは通常のWebページより強い権限でActiveXやローカルファイルを操作できます。組織のセキュリティ設定で実行を禁止されている場合があり、新規利用にはPowerShellローカルサーバー版の[`MarkDownPSLocalServerWiki`](https://github.com/papanda925/MarkDownPSLocalServerWiki)を推奨します。 -・Markdown記法が使える。 +## 主な機能 - いまさらかもしれないが、以前から興味があったMarkdown記法の変換ルールを採用した。 - そのために以下のjsファイル+cssを移植。 +- Markdownページの表示と編集 +- 新規ページ作成、名前変更、削除 +- ページ一覧 +- ファイル名と本文の文字列検索 +- 表示履歴 +- 文字サイズの拡大・縮小 +- HTTP/HTTPSリンクを外部ブラウザーで開く機能 +- WindowsのファイルパスやUNCパスを関連付けられたアプリで開く機能 +- ページを同じフォルダーの`.md`ファイルとして保存 - ・marked.js(一部改造) - ・github-markdown.css - ・jquery-1.12.0.js - -・テキストファイルの保存機能 +## 動作環境 - marked.jsを修正し、リンクのルール「( aaaa )[ aaaa ] 」の中でURL等以外は、テキストファイル(aaaa.md)保存するように改造している。 +- Windows +- `mshta.exe`/HTAの実行が許可されている環境 +- リポジトリのフォルダーへ書き込めること -テキストファイル(aaaa.md)は、リンクのルール「( aaaa )[ aaaa ] 」によりファイルリンクされるようにしている。 +Edge、Chrome、Firefoxの拡張機能として動くものではありません。`index.hta`はWindowsのHTML Application Hostで実行されます。 -・テキストファイル以外は、ブラウザ等の外部アプリを利用するように修正。 +## 起動方法 -・いわゆるファイルサーバ(\\コンピュータ名)でもリンクされるように機能追加。 +1. リポジトリをダウンロードまたはクローンします。 +2. `index.hta`、`core`フォルダー、`.md`ファイルを同じ構成のまま置きます。 +3. `index.hta`をダブルクリックします。 +4. 「トップページ」が表示されることを確認します。 -追伸: -まだまだ改良の余地あるツールで、作者も勉強途中ですが。 -このツール(手法)に興味を持っていただき、さらに改良にチャレンジされた方がいらっしゃり、とてもうれしい。 -こちらにリンクを張っておきます。 +実行前に`index.hta`の入手元と内容を確認してください。メールや不明なWebサイトから取得したHTAファイルは実行しないでください。 -https://github.com/ma34s/MarkdownLocalWiki +## ページデータ -https://qiita.com/hachimitu22/items/5bba11ab89b556b810b4 +| 場所 | 内容 | +| --- | --- | +| `トップページ.md` | 最初に表示するページ | +| `*.md` | 作成したWikiページ | +| `index.hta` | 画面、Markdown処理、ファイル操作 | +| `core\marked.js` | MarkdownからHTMLへの変換 | +| `core\github-markdown.css` | Markdown表示用CSS | +| `core\jquery-1.12.4.js` | 同梱されている旧版jQuery | + +バックアップする場合は、リポジトリ直下の`.md`ファイルをすべてコピーしてください。更新、名前変更、削除の前には別フォルダーへバックアップすることを推奨します。 + +## ページ名の扱い + +ページ名はWindowsのファイル名として使われます。現在のコードは次を拒否します。 + +- 空のページ名 +- `.`または`..` +- `\ / : * ? " < > |`など、Windowsのファイル名に使用できない文字 +- 制御文字 + +すべてのページパスは`index.hta`があるフォルダーを基準に組み立てます。これにより、新規作成、保存、名前変更、削除でWikiフォルダー外を直接指定できないようにしています。 + +## セキュリティ上の注意 + +- HTAは現在のブラウザーのサンドボックス内では動作しません。 +- `Scripting.FileSystemObject`でファイルを作成・変更・削除します。 +- `Shell.Application`でローカルファイルやフォルダーを開きます。 +- 信頼できないMarkdownには、HTMLや不正なリンクが含まれる可能性があります。 +- このWikiフォルダーへ、信頼できない`.hta`、JavaScript、Markdownを追加しないでください。 +- 組織で`mshta.exe`が禁止されている場合、設定を回避せずPowerShellローカルサーバー版を使用してください。 + +`mshta.exe`は正規のWindows機能ですが、任意のスクリプト実行に悪用される例があるため、セキュリティ製品や組織ポリシーで制限されることがあります。参考:[MITRE ATT&CK - Mshta](https://attack.mitre.org/techniques/T1218/005/) + +## PowerShellローカルサーバー版への移行 + +1. [`MarkDownPSLocalServerWiki`](https://github.com/papanda925/MarkDownPSLocalServerWiki)を別フォルダーへ準備します。 +2. HTA版を終了します。 +3. このリポジトリ直下の`.md`ファイルを、移行先の`doc`フォルダーへコピーします。 +4. `トップページ.md`が`doc`フォルダーにあることを確認します。 +5. 移行先の`startWiki.bat`または`StartWiki.ps1`を起動します。 +6. 表示、編集、検索、リンクを確認してから旧フォルダーを保管します。 + +最初は移動ではなくコピーにし、元データを残してください。 + +## 制限事項 + +- HTA/Internet Explorer系のMSHTML実行環境に依存 +- 現在のWeb標準やブラウザー機能を前提にしたコードではない +- 複数利用者、ログイン、権限管理、同時編集に非対応 +- 自動バックアップ、世代管理、ごみ箱はなし +- 同梱JavaScriptライブラリは旧版であり、無条件の最新版置換は互換性を壊す可能性あり +- Markdown内の信頼できないHTMLを安全に無害化する機能はなし + +## トラブルシューティング + +### `index.hta`を開けない + +組織ポリシー、EDR、ウイルス対策ソフト、ファイルの関連付けでHTAが禁止されている可能性があります。設定の回避は行わず、PowerShellローカルサーバー版への移行を検討してください。 + +### トップページが見つからない + +`index.hta`と同じフォルダーに`トップページ.md`があるか確認してください。 + +### 保存できない + +Wikiフォルダーへの書き込み権限を確認してください。読み取り専用の場所、ZIPファイル内、保護されたフォルダーでは保存できません。 + +### 文字化けする + +`index.hta`はShift_JISを前提としています。ソースやMarkdownの文字コードを変更する場合は、必ずコピーで試し、既存ページの表示と再保存を確認してください。 + +## 由来と関連プロジェクト + +このリポジトリは、個人ブログで公開していたHTA/jQuery/marked.jsによるMarkdown WikiをGitHubへ移したものです。 + +- [元になったブログ記事](http://kazu-s-diary-2.cocolog-nifty.com/blog/2016/07/htajquerymark-1.html) +- [ma34s/MarkdownLocalWiki](https://github.com/ma34s/MarkdownLocalWiki) +- [Qiitaでの関連紹介](https://qiita.com/hachimitu22/items/5bba11ab89b556b810b4) +- [後継:MarkDownPSLocalServerWiki](https://github.com/papanda925/MarkDownPSLocalServerWiki) + +## English summary + +Legacy Windows HTA-based personal Markdown wiki. It can read and write local files with the current user's permissions. New users should prefer the localhost-only PowerShell server successor. diff --git a/Tests/validate.py b/Tests/validate.py new file mode 100644 index 0000000..2f9f585 --- /dev/null +++ b/Tests/validate.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import re +import subprocess +import tempfile +from pathlib import Path + + +repository_root = Path(__file__).resolve().parent.parent +hta_path = repository_root / "index.hta" +readme_path = repository_root / "README.md" + +hta = hta_path.read_text(encoding="cp932") +readme = readme_path.read_text(encoding="utf-8") + +inline_scripts = re.findall( + r"]*)?>(.*?)", + hta, + flags=re.IGNORECASE | re.DOTALL, +) +javascript = "\n".join(script for script in inline_scripts if script.strip()) + +with tempfile.NamedTemporaryFile("w", suffix=".js", encoding="utf-8", delete=False) as temp_file: + temp_file.write(javascript) + temp_path = Path(temp_file.name) + +try: + subprocess.run(["node", "--check", str(temp_path)], check=True) +finally: + temp_path.unlink(missing_ok=True) + +required_source_patterns = { + "page-name validation": r"function\s+validatePageName\s*\(", + "HTA-relative base folder": r"window\.location\.pathname", + "textarea value saving": r"var\s+content\s*=\s*id\('textarea'\)\.value", + "scoped current page": r"var\s+pagename\s*=\s*id\('headerH1'\)\.innerText", + "protected top page": r"pagename\s*===\s*'トップページ'", +} + +for label, pattern in required_source_patterns.items(): + if not re.search(pattern, hta): + raise AssertionError(f"Missing {label}: {pattern}") + +for heading in ( + "## 起動方法", + "## セキュリティ上の注意", + "## PowerShellローカルサーバー版への移行", + "## 制限事項", + "## トラブルシューティング", +): + if heading not in readme: + raise AssertionError(f"README is missing {heading}") + +print("HTA JavaScript and repository conventions validated.") diff --git a/index.hta b/index.hta index 3f99da5..0f93362 100644 --- a/index.hta +++ b/index.hta @@ -23,7 +23,24 @@ function smaller() { x /= 1.2; document.getElementById('content').style.fontSize //HTMLt@CuĂtH_̃pX擾 function getBaseFolder(){ - return fso.GetFolder("."); + var currentPath = decodeURI(window.location.pathname).replace(/\//g, "\\"); + if (/^\\[A-Za-z]:\\/.test(currentPath)) { + currentPath = currentPath.substring(1); + } + return fso.GetFolder(fso.GetParentFolderName(currentPath)); +} + +//y[Wt@CƂĈSɎg邩mFB +//WikitH_[Owu..vAWindowsŎgpłȂۂB +function validatePageName(pagename) { + var normalizedPageName = String(pagename || "").replace(/^\s+|\s+$/g, ""); + var invalidCharacters = /[\\\/:*?"<>|\x00-\x1F]/; + + if (!normalizedPageName || normalizedPageName === "." || normalizedPageName === ".." || invalidCharacters.test(normalizedPageName)) { + throw new Error("y[WɎgpłȂ܂܂Ă܂B"); + } + + return normalizedPageName; } @@ -188,16 +205,14 @@ function edit(pagename){ //܌Ăy[WҏW function editCurrentPage(){ - pagename = id('headerH1').innerText; + var pagename = id('headerH1').innerText; edit(pagename); } //tH[̃eLXgGA̒gAw肵y[W̃RecƂĕۑ function save(pagename){ -//ҏW@̏C textareaւ̊i[̓tH[textareaփeLXg -// var content = id('textarea').value; - var content = id('textarea').innerText; + var content = id('textarea').value; var filepath = getFilePath(pagename); var file = fso.createTextFile(filepath); @@ -258,7 +273,7 @@ function openIndexPage(){ //y[Ww肵āAY}[N_Ei.md)̃pX擾 function getFilePath(pagename){ - var filename = pagename + ".md"; + var filename = validatePageName(pagename) + ".md"; return oBaseFolder.Path + "\\" + filename; } @@ -386,18 +401,28 @@ function openAndSerch(pagename) { //VK쐬 function NewPage() { - var pagename = Text2.value; -// window.confirm(pagename); - if (pagename) { + try { + var pagename = validatePageName(Text2.value); open(pagename); } + catch (error) { + window.alert(error.message); + } Text2.value = ''; } //l[ function ReNamePage() { var srcPagename = getPageName(); - var dstPagename = Text2.value; + var dstPagename; + + try { + dstPagename = validatePageName(Text2.value); + } + catch (error) { + window.alert(error.message); + return; + } // uOKv̏Jn { mF_CAO̕\ if (window.confirm(dstPagename + ' Ƀl[܂H')) @@ -405,6 +430,11 @@ function ReNamePage() { var srcFilepath = getFilePath(srcPagename); var dstFilepath = getFilePath(dstPagename); + + if (fso.FileExists(dstFilepath)) { + window.alert(dstPagename + ' ͊ɑ݂܂B'); + return; + } fso.MoveFile(srcFilepath, dstFilepath); window.alert(srcPagename + ' ' + dstPagename + ' Ƀl[܂B'); @@ -421,10 +451,14 @@ function DeletePage() { var pagename = getPageName(); + if (pagename === 'gbvy[W') { + window.alert('gbvy[W͍폜ł܂B'); + return; + } + // uOKv̏Jn { mF_CAO̕\ if (window.confirm(pagename + ' 폜܂H')) { - var pagename = getPageName(); var filepath = getFilePath(pagename); fso.DeleteFile(filepath);