wikiwiki 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc13d22d549b9398dc6573d2731c7c1169fe70a1626f80d9f9b650116deb162c
4
- data.tar.gz: 1495f98f015f7494f7355a76f377f42d082a72e92a4679438b2d8822f6294f91
3
+ metadata.gz: 637f781de0cde55bb67c79d67624df9cc4255bac9e35e6b144fc6170d34b1d5d
4
+ data.tar.gz: 3155651131252d9e84260584fb3f7dcd6c21ffd52ac48fe16da2b605ac1948ae
5
5
  SHA512:
6
- metadata.gz: ad6ac01ea174e040f151f74d04504314bb4885abb0e66cc915debddee70f5d6ad3de402def7c693ae52c3ba8f1f3dd9fe8497befbb3c736ae5e79d86e79ed56a
7
- data.tar.gz: 0351abf501fed5484cc1b9606610d3ed3fbc789064a11384470ee7c7abd07115c27f315568d42a4ab4ead63f5be05473bef832cfdc350afdfa04c90e76774a68
6
+ metadata.gz: e5a8ce9a48d65aa95d7334289e5966a72228f9da9adc22799ac7ef63de802f463e882e5aec90e784d35f8411688ec24af6ddd9b9b14702a6988c61fc4d7a1357
7
+ data.tar.gz: 432bc7bfc745d96b4fcb00e296dbb3fb96ebe34a8bd9d6b06042054e633ca12a7863ce6a1d77bb638f81232258b5400102ed04fd02d65a8693735576bd0cea6c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.1] - 2025-11-03
4
+
5
+ ### Documentation
6
+
7
+ - Added security notes to README about path traversal risks when using API-provided names in bulk download automation scripts
8
+
9
+ ## [0.7.0] - 2025-11-03
10
+
11
+ ### Added
12
+
13
+ - Page deletion support
14
+ - `Wiki#delete_page` method to delete pages
15
+ - `wikiwiki page delete` command in CLI
16
+ - Validation in `Wiki#update_page` and `wikiwiki page put` to prevent accidental deletion with empty content
17
+ - `ConflictError` exception class for HTTP 409 Conflict responses
18
+
19
+ ### Changed
20
+
21
+ - `attachment put --force` behavior: attempts upload first, then deletes and retries on conflict (409 error)
22
+ - Previously: checked existence first, then deleted and uploaded
23
+ - Now: optimistic upload pattern eliminates Time-of-Check-Time-of-Use (TOCTOU) race conditions
24
+
3
25
  ## [0.6.0] - 2025-11-02
4
26
 
5
27
  ### Added
data/README.ja.md CHANGED
@@ -87,6 +87,9 @@ wikiwiki page get FrontPage frontpage.txt
87
87
  wikiwiki page put TestPage < content.txt
88
88
  wikiwiki page put TestPage content.txt
89
89
 
90
+ # ページを削除
91
+ wikiwiki page delete TestPage
92
+
90
93
  # 添付ファイル一覧
91
94
  wikiwiki attachment list FrontPage
92
95
 
@@ -109,8 +112,9 @@ wikiwiki page get FrontPage existing.txt --force
109
112
  wikiwiki attachment put FrontPage logo.png --force
110
113
 
111
114
  # 注意: --force による添付ファイルの上書きはアトミックではありません。
112
- # 既存の添付ファイルを削除してから新しいファイルをアップロードします。
113
- # アップロードに失敗した場合、添付ファイルは失われます。
115
+ # まずアップロードを試み、競合エラー(ファイルが既に存在)が発生した場合、
116
+ # 既存ファイルを削除してアップロードをリトライします。
117
+ # リトライに失敗した場合、添付ファイルは失われます。
114
118
 
115
119
  # コマンドラインで認証情報を指定(環境変数より優先)
116
120
  wikiwiki --wiki-id=your-wiki-id --password=your-password page list
@@ -126,6 +130,23 @@ wikiwiki page list --verbose
126
130
  wikiwiki page list --debug
127
131
  ```
128
132
 
133
+ **一括ダウンロード時のセキュリティ注意:**
134
+
135
+ APIから取得したページ名や添付ファイル名を使用してページや添付ファイルを一括ダウンロードする自動化処理を行う場合、これらの名前にパストラバーサルシーケンス(例:`../../../etc/passwd`)が含まれている可能性があることに注意してください。ファイルパスとして使用する前に、必ず検証またはサニタイズを行ってください:
136
+
137
+ ```bash
138
+ # 悪い例: シェルスクリプトでAPIから取得した名前を直接使用
139
+ for name in $(wikiwiki page list --json | jq -r '.[]'); do
140
+ wikiwiki page get "$name" "$name.txt" # 安全でない: nameに../が含まれる可能性
141
+ done
142
+
143
+ # 良い例: 自動化スクリプト内で名前をサニタイズ
144
+ for name in $(wikiwiki page list --json | jq -r '.[]'); do
145
+ safe_name=$(basename "$name") # ディレクトリ成分を削除
146
+ wikiwiki page get "$name" "$safe_name.txt"
147
+ done
148
+ ```
149
+
129
150
  ### Rubyライブラリ
130
151
 
131
152
  ライブラリを使用する基本的な例:
@@ -160,6 +181,9 @@ wiki.update_page(page_name: "TestPage", source: <<~SOURCE)
160
181
  # Hello World
161
182
  SOURCE
162
183
 
184
+ # ページを削除
185
+ wiki.delete_page(page_name: "TestPage")
186
+
163
187
  # 添付ファイル名の一覧を取得
164
188
  attachment_names = wiki.attachment_names(page_name: "FrontPage")
165
189
 
data/README.md CHANGED
@@ -87,6 +87,9 @@ wikiwiki page get FrontPage frontpage.txt
87
87
  wikiwiki page put TestPage < content.txt
88
88
  wikiwiki page put TestPage content.txt
89
89
 
90
+ # Delete page
91
+ wikiwiki page delete TestPage
92
+
90
93
  # List attachments
91
94
  wikiwiki attachment list FrontPage
92
95
 
@@ -109,8 +112,9 @@ wikiwiki page get FrontPage existing.txt --force
109
112
  wikiwiki attachment put FrontPage logo.png --force
110
113
 
111
114
  # Note: Attachment overwrite with --force is not atomic.
112
- # The existing attachment is deleted before uploading the new one.
113
- # If the upload fails, the attachment will be lost.
115
+ # First attempts to upload; if it fails due to conflict (file exists),
116
+ # deletes the existing file and retries the upload.
117
+ # If retry fails, the attachment will be lost.
114
118
 
115
119
  # Authentication via command line (overrides environment variables)
116
120
  wikiwiki --wiki-id=your-wiki-id --password=your-password page list
@@ -126,6 +130,23 @@ wikiwiki page list --verbose
126
130
  wikiwiki page list --debug
127
131
  ```
128
132
 
133
+ **Security Note for Bulk Downloads:**
134
+
135
+ When automating bulk downloads of pages or attachments using page/attachment names from the API, be aware that these names may contain path traversal sequences (e.g., `../../../etc/passwd`). Always validate or sanitize names before using them as file paths:
136
+
137
+ ```bash
138
+ # Bad: Direct use of API-provided names in shell scripts
139
+ for name in $(wikiwiki page list --json | jq -r '.[]'); do
140
+ wikiwiki page get "$name" "$name.txt" # UNSAFE if name contains ../
141
+ done
142
+
143
+ # Good: Sanitize names in your automation script
144
+ for name in $(wikiwiki page list --json | jq -r '.[]'); do
145
+ safe_name=$(basename "$name") # Remove directory components
146
+ wikiwiki page get "$name" "$safe_name.txt"
147
+ done
148
+ ```
149
+
129
150
  ### Ruby Library
130
151
 
131
152
  Basic example using the library:
@@ -160,6 +181,9 @@ wiki.update_page(page_name: "TestPage", source: <<~SOURCE)
160
181
  # Hello World
161
182
  SOURCE
162
183
 
184
+ # Delete a page
185
+ wiki.delete_page(page_name: "TestPage")
186
+
163
187
  # List attachment names
164
188
  attachment_names = wiki.attachment_names(page_name: "FrontPage")
165
189
 
data/lib/wikiwiki/api.rb CHANGED
@@ -62,6 +62,7 @@ module Wikiwiki
62
62
  # @param source [String] the page source content
63
63
  # @return [void]
64
64
  # @raise [Error] if request fails
65
+ # @note Passing an empty string as source will delete the page
65
66
  def put_page(encoded_page_name:, source:)
66
67
  uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}"
67
68
  response = request(:put, uri, body: {"source" => source})
@@ -171,6 +172,7 @@ module Wikiwiki
171
172
  # @return [Hash] parsed response body
172
173
  # @raise [AuthenticationError] if authentication fails (401)
173
174
  # @raise [ResourceNotFoundError] if resource not found (404)
175
+ # @raise [ConflictError] if conflict occurs (409)
174
176
  # @raise [ServerError] if server error (5xx)
175
177
  # @raise [APIError] if other API request fails
176
178
  private def parse_json_response(response)
@@ -181,6 +183,8 @@ module Wikiwiki
181
183
  raise AuthenticationError, message
182
184
  when 404
183
185
  raise ResourceNotFoundError, message
186
+ when 409
187
+ raise ConflictError, message
184
188
  when 500..599
185
189
  raise ServerError, message
186
190
  else
@@ -21,24 +21,10 @@ module Wikiwiki
21
21
  def call(page_name:, file_name:, out: $stdout, err: $stderr, **)
22
22
  wiki = create_wiki(out:, err:, **)
23
23
 
24
- # Check if page exists first
25
- unless page_exists?(wiki, page_name:)
26
- raise ArgumentError, "Page '#{page_name}' does not exist"
27
- end
28
-
29
- # Check if attachment exists
30
- unless attachment_exists?(wiki, page_name:, attachment_name: file_name)
31
- raise ArgumentError, "Attachment '#{file_name}' does not exist on page '#{page_name}'"
32
- end
33
-
34
24
  wiki.delete_attachment(page_name:, attachment_name: file_name)
35
25
 
36
26
  say("Attachment '#{file_name}' deleted from page '#{page_name}'", out:, **)
37
27
  end
38
-
39
- private def page_exists?(wiki, page_name:) = wiki.page_names.include?(page_name)
40
-
41
- private def attachment_exists?(wiki, page_name:, attachment_name:) = wiki.attachment_names(page_name:).include?(attachment_name)
42
28
  end
43
29
  end
44
30
 
@@ -35,22 +35,17 @@ module Wikiwiki
35
35
  raise ArgumentError, "File size (#{content.bytesize} bytes) exceeds maximum allowed size (#{MAX_ATTACHMENT_SIZE} bytes / 512 KiB)"
36
36
  end
37
37
 
38
- if attachment_exists?(wiki, page_name:, attachment_name:)
38
+ begin
39
+ wiki.add_attachment(page_name:, attachment_name:, content:)
40
+ rescue ConflictError
39
41
  raise ArgumentError, "Attachment '#{attachment_name}' already exists. Use --force to overwrite." unless force
40
42
 
41
- begin
42
- wiki.delete_attachment(page_name:, attachment_name:)
43
- rescue ResourceNotFoundError
44
- # Already deleted by another process, continue
45
- end
43
+ wiki.delete_attachment(page_name:, attachment_name:)
44
+ retry
46
45
  end
47
46
 
48
- wiki.add_attachment(page_name:, attachment_name:, content:)
49
-
50
47
  say("Attachment '#{attachment_name}' uploaded to page '#{page_name}'", out:, **)
51
48
  end
52
-
53
- private def attachment_exists?(wiki, page_name:, attachment_name:) = wiki.attachment_names(page_name:).include?(attachment_name)
54
49
  end
55
50
  end
56
51
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ module Page
7
+ # Delete a page
8
+ class Delete < Base
9
+ desc "Delete a page"
10
+
11
+ argument :page_name, required: true, desc: "Page name"
12
+
13
+ # Execute the delete command
14
+ #
15
+ # @param page_name [String] name of the page to delete
16
+ # @param out [IO] output stream
17
+ # @param err [IO] error stream
18
+ # @return [void]
19
+ def call(page_name:, out: $stdout, err: $stderr, **)
20
+ wiki = create_wiki(out:, err:, **)
21
+
22
+ wiki.delete_page(page_name:)
23
+
24
+ say("Page '#{page_name}' deleted successfully", out:, **)
25
+ end
26
+ end
27
+ end
28
+
29
+ register "page delete", Page::Delete
30
+ end
31
+ end
32
+ end
@@ -18,6 +18,8 @@ module Wikiwiki
18
18
  # @param out [IO] output stream
19
19
  # @param err [IO] error stream
20
20
  # @return [void]
21
+ # @raise [ArgumentError] if source content is empty
22
+ # @note To delete a page, use `wikiwiki page delete` instead
21
23
  def call(page_name:, input_file: nil, out: $stdout, err: $stderr, **)
22
24
  wiki = create_wiki(out:, err:, **)
23
25
 
@@ -27,6 +29,8 @@ module Wikiwiki
27
29
  $stdin.read
28
30
  end
29
31
 
32
+ raise ArgumentError, "Page source must not be empty. Use 'wikiwiki page delete' to delete a page." if source.empty?
33
+
30
34
  wiki.update_page(page_name:, source:)
31
35
 
32
36
  say("Page '#{page_name}' updated successfully", out:, **)
data/lib/wikiwiki/cli.rb CHANGED
@@ -9,12 +9,10 @@ module Wikiwiki
9
9
  # Provides commands for managing wiki pages and attachments through the terminal.
10
10
  # Supports authentication via API key or password, with configurable logging and output modes.
11
11
  class CLI
12
- # Run the CLI with given arguments
12
+ # Initialize the CLI with output streams
13
13
  #
14
- # @param argv [Array<String>] command-line arguments
15
14
  # @param out [IO] standard output (defaults to $stdout)
16
15
  # @param err [IO] error output (defaults to $stderr)
17
- # @return [Integer] exit code (0 for success, 1 for error)
18
16
  def initialize(out: $stdout, err: $stderr)
19
17
  @out = out
20
18
  @err = err
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Wikiwiki
4
4
  # The gem version
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.1"
6
6
  public_constant :VERSION
7
7
  end
data/lib/wikiwiki/wiki.rb CHANGED
@@ -70,14 +70,28 @@ module Wikiwiki
70
70
  # Updates a page with new content
71
71
  #
72
72
  # @param page_name [String] the name of the page to update
73
- # @param source [String] the new page source content
73
+ # @param source [String] the new page source content (must not be empty)
74
74
  # @return [void]
75
+ # @raise [ArgumentError] if source is empty
75
76
  # @raise [Wikiwiki::Error] if the update fails
77
+ # @note To delete a page, use {#delete_page} instead
76
78
  def update_page(page_name:, source:)
79
+ raise ArgumentError, "Page source must not be empty. Use delete_page to delete a page." if source.empty?
80
+
77
81
  encoded_page_name = ERB::Util.url_encode(page_name)
78
82
  api.put_page(encoded_page_name:, source:)
79
83
  end
80
84
 
85
+ # Deletes a page
86
+ #
87
+ # @param page_name [String] the name of the page to delete
88
+ # @return [void]
89
+ # @raise [Wikiwiki::Error] if the deletion fails
90
+ def delete_page(page_name:)
91
+ encoded_page_name = ERB::Util.url_encode(page_name)
92
+ api.put_page(encoded_page_name:, source: "")
93
+ end
94
+
81
95
  # Retrieves attachment file names on a page
82
96
  #
83
97
  # @param page_name [String] the name of the page
data/lib/wikiwiki.rb CHANGED
@@ -32,6 +32,10 @@ module Wikiwiki
32
32
  # Raised when HTTP status is 404
33
33
  class ResourceNotFoundError < APIError; end
34
34
 
35
+ # Conflict error
36
+ # Raised when HTTP status is 409
37
+ class ConflictError < APIError; end
38
+
35
39
  # Server error
36
40
  # Raised when HTTP status is 5xx
37
41
  class ServerError < APIError; end
@@ -65,10 +65,6 @@ module Wikiwiki
65
65
  ?verbose: bool,
66
66
  ?debug: bool
67
67
  ) -> void
68
-
69
- private
70
-
71
- def attachment_exists?: (Wiki wiki, page_name: String, attachment_name: String) -> bool
72
68
  end
73
69
 
74
70
  class Delete < Base
@@ -84,11 +80,6 @@ module Wikiwiki
84
80
  ?verbose: bool,
85
81
  ?debug: bool
86
82
  ) -> void
87
-
88
- private
89
-
90
- def page_exists?: (Wiki wiki, page_name: String) -> bool
91
- def attachment_exists?: (Wiki wiki, page_name: String, attachment_name: String) -> bool
92
83
  end
93
84
  end
94
85
  end
@@ -61,6 +61,21 @@ module Wikiwiki
61
61
  ?debug: bool
62
62
  ) -> void
63
63
  end
64
+
65
+ class Delete < Base
66
+ def call: (
67
+ page_name: String,
68
+ ?out: IO,
69
+ ?err: IO,
70
+ ?wiki_id: String?,
71
+ ?api_key_id: String?,
72
+ ?secret: String?,
73
+ ?password: String?,
74
+ ?token: String?,
75
+ ?verbose: bool,
76
+ ?debug: bool
77
+ ) -> void
78
+ end
64
79
  end
65
80
  end
66
81
  end
@@ -14,6 +14,8 @@ module Wikiwiki
14
14
 
15
15
  def update_page: (page_name: String, source: String) -> void
16
16
 
17
+ def delete_page: (page_name: String) -> void
18
+
17
19
  def attachment_names: (page_name: String) -> Array[String]
18
20
 
19
21
  def attachment: (page_name: String, attachment_name: String, ?rev: String?) -> Attachment
data/sig/wikiwiki.rbs CHANGED
@@ -19,6 +19,9 @@ module Wikiwiki
19
19
  class ResourceNotFoundError < APIError
20
20
  end
21
21
 
22
+ class ConflictError < APIError
23
+ end
24
+
22
25
  class ServerError < APIError
23
26
  end
24
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wikiwiki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OZAWA Sakuro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-02 00:00:00.000000000 Z
11
+ date: 2025-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -95,6 +95,7 @@ files:
95
95
  - lib/wikiwiki/cli/commands/attachment/show.rb
96
96
  - lib/wikiwiki/cli/commands/auth.rb
97
97
  - lib/wikiwiki/cli/commands/base.rb
98
+ - lib/wikiwiki/cli/commands/page/delete.rb
98
99
  - lib/wikiwiki/cli/commands/page/get.rb
99
100
  - lib/wikiwiki/cli/commands/page/list.rb
100
101
  - lib/wikiwiki/cli/commands/page/put.rb