m365_active_storage 1.1.0 → 1.1.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 +4 -4
- data/CHANGELOG.md +24 -2
- data/lib/active_storage/service/sharepoint_service.rb +20 -25
- data/lib/m365_active_storage/controllers/blobs_controller.rb +2 -0
- data/lib/m365_active_storage/files.rb +13 -2
- data/lib/m365_active_storage/railtie.rb +18 -0
- data/lib/m365_active_storage/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72492311b32ca0b097c13fdec2e254d43006191dfeaa4a6d2245640ac7cd0b97
|
|
4
|
+
data.tar.gz: 281c7e93c835fb9c8bab29d29f0fc9efdb066c44457327ddd4be5eb71592cf63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ecc16539d70ec002cb69f6aa1dde824e52ad6e51ec2ba2ea76ff0e9cc54de1aa714098f3ac301784489db932d3390cd93e158f6c5f38898592877a3350f1c55
|
|
7
|
+
data.tar.gz: 8d73cd54cec9435e1db2b3d7ac7583430d3fe450c83856dccf67fef018d91fcb162516f60cb15071f462855918d86c28c6d891d26e8ef46f564107908d46655b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [1.1.1] - 2026-04-10
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- Fix controller class discovery in `M365ActiveStorage::Files` so only controllers from the active gem installation are loaded, avoiding invalid constantization and inconsistent behavior when multiple gem copies or versions are present on the load path.
|
|
6
|
+
- Fix credential-gated test skipping in `test_helper.rb` to avoid constant lookup errors by comparing class names instead of class constants, making skip behavior stable regardless of test load order.
|
|
7
|
+
- Fix SharePoint path encoding to produce valid Graph URLs by encoding spaces as `%20` (not `+`), ensuring file and folder paths with spaces are resolved correctly.
|
|
8
|
+
- Fix SharePoint ID not being persisted when blob is created with custom metadata (e.g., `sharepoint_folder`). ID persistence is now deferred to an `after_commit` hook to prevent being overwritten by Active Storage's blob save.
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- Add explicit CSRF protection (`protect_from_forgery`) to `M365ActiveStorage::BlobsController` to address Brakeman security warning and enforce forgery protection posture.
|
|
13
|
+
|
|
14
|
+
## [1.1.0] - 2026-03-25
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- Add support for organizing files in nested SharePoint folders via `sharepoint_folder` metadata on blob attachments.
|
|
19
|
+
- Allow configurable storage key to use either blob key or filename for file storage organization in SharePoint.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- Patch Stored XSS vulnerability in `action_text-trix` dependency.
|
|
24
|
+
|
|
25
|
+
## [1.0.0] - 2026-03-16
|
|
4
26
|
|
|
5
27
|
- Initial release
|
|
@@ -129,6 +129,8 @@ module ActiveStorage
|
|
|
129
129
|
#
|
|
130
130
|
# Uploads file content to the configured SharePoint drive.
|
|
131
131
|
# The file is stored with the blob's filename for better organization in SharePoint.
|
|
132
|
+
# The SharePoint item ID is persisted asynchronously in an after_commit hook
|
|
133
|
+
# to avoid being overwritten by Active Storage's blob.save.
|
|
132
134
|
#
|
|
133
135
|
# @param [String] key The Active Storage blob key (ignored, filename used instead)
|
|
134
136
|
# @param [IO] io The file content as an IO object
|
|
@@ -150,7 +152,10 @@ module ActiveStorage
|
|
|
150
152
|
storage_path = build_storage_path(get_storage_name(key), folder_path)
|
|
151
153
|
upload_url = "#{drive_url}/root:/#{encode_storage_path(storage_path)}:/content"
|
|
152
154
|
response = http.put(upload_url, io.read, { "Content-Type": "application/octet-stream" })
|
|
153
|
-
|
|
155
|
+
raise "Failed to upload file to SharePoint" unless [201, 200].include?(response.code.to_i)
|
|
156
|
+
|
|
157
|
+
# Store response body payload in thread-local storage for later retrieval in after_commit hook
|
|
158
|
+
Thread.current[:sharepoint_upload_response] = response
|
|
154
159
|
end
|
|
155
160
|
|
|
156
161
|
# Download a file from SharePoint
|
|
@@ -355,38 +360,27 @@ module ActiveStorage
|
|
|
355
360
|
"#{config.ms_graph_endpoint}/sites/#{config.site_id}/drives/#{config.drive_id}"
|
|
356
361
|
end
|
|
357
362
|
|
|
358
|
-
# Handle the response from an upload request
|
|
359
|
-
#
|
|
360
|
-
# Validates that the upload succeeded (201 Created or 200 OK).
|
|
361
|
-
# Raises an error for any other status code.
|
|
362
|
-
#
|
|
363
|
-
# @param [String] key The Active Storage blob key associated with the upload
|
|
364
|
-
# @param [Net::HTTPResponse] response The HTTP response from the upload
|
|
365
|
-
# @return [void]
|
|
366
|
-
#
|
|
367
|
-
# @raise [StandardError] if upload failed
|
|
368
|
-
def handle_upload_response(key, response)
|
|
369
|
-
if [201, 200].include?(response.code.to_i)
|
|
370
|
-
persist_sharepoint_id(key, response)
|
|
371
|
-
return
|
|
372
|
-
end
|
|
373
363
|
|
|
374
|
-
raise "Failed to upload file to SharePoint"
|
|
375
|
-
end
|
|
376
364
|
|
|
377
|
-
# Persist the SharePoint item id in blob metadata
|
|
365
|
+
# Persist the SharePoint item id in blob metadata from a stored upload response.
|
|
366
|
+
#
|
|
367
|
+
# This is called by the Railtie's after_commit hook to store the SharePoint ID
|
|
368
|
+
# after the blob has been fully saved to the database, preventing the ID from
|
|
369
|
+
# being overwritten by Active Storage's metadata save.
|
|
378
370
|
#
|
|
379
371
|
# Stored keys:
|
|
380
372
|
# * metadata["sharepoint_id"] - convenience flat key for quick access
|
|
381
373
|
# * metadata["sharepoint"]["id"] - namespaced SharePoint metadata
|
|
382
374
|
#
|
|
383
375
|
# @param [String] key The Active Storage blob key
|
|
384
|
-
# @param [Net::HTTPResponse] response The
|
|
376
|
+
# @param [Net::HTTPResponse, String] response The upload response or JSON payload string
|
|
385
377
|
# @return [void]
|
|
386
|
-
def persist_sharepoint_id(key, response)
|
|
387
|
-
return if response.body.to_s.strip.empty?
|
|
388
378
|
|
|
389
|
-
|
|
379
|
+
def persist_sharepoint_id_from_response(key, response)
|
|
380
|
+
body = response.is_a?(String) ? response : response.body.to_s
|
|
381
|
+
return if body.to_s.strip.empty?
|
|
382
|
+
|
|
383
|
+
payload = JSON.parse(body)
|
|
390
384
|
sharepoint_id = payload["id"]
|
|
391
385
|
return if sharepoint_id.to_s.empty?
|
|
392
386
|
|
|
@@ -525,11 +519,12 @@ module ActiveStorage
|
|
|
525
519
|
#
|
|
526
520
|
# Each path segment is CGI-encoded individually so that "/" separators
|
|
527
521
|
# are preserved and not encoded as "%2F".
|
|
522
|
+
# Spaces are encoded as "%20" (not "+") to ensure proper URL path handling.
|
|
528
523
|
#
|
|
529
524
|
# @param [String] path The path to encode (e.g. "my folder/sub folder/file.pdf")
|
|
530
|
-
# @return [String] URL-safe encoded path (e.g. "my
|
|
525
|
+
# @return [String] URL-safe encoded path (e.g. "my%20folder/sub%20folder/file.pdf")
|
|
531
526
|
def encode_storage_path(path)
|
|
532
|
-
path.split("/").map { |s| CGI.escape(s) }.join("/")
|
|
527
|
+
path.split("/").map { |s| CGI.escape(s).gsub("+", "%20") }.join("/")
|
|
533
528
|
end
|
|
534
529
|
|
|
535
530
|
# Ensure the full folder hierarchy exists in SharePoint.
|
|
@@ -43,6 +43,8 @@ module M365ActiveStorage
|
|
|
43
43
|
# @see ActiveStorage::Blob
|
|
44
44
|
# @see ActiveStorage::Service::SharepointService
|
|
45
45
|
class BlobsController < ActionController::Base
|
|
46
|
+
protect_from_forgery with: :exception
|
|
47
|
+
|
|
46
48
|
# Display/download a blob
|
|
47
49
|
#
|
|
48
50
|
# Retrieves a blob by its signed ID and sends it to the client with appropriate
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
3
5
|
module M365ActiveStorage
|
|
4
6
|
# == File Discovery and Loading
|
|
5
7
|
#
|
|
@@ -52,8 +54,17 @@ module M365ActiveStorage
|
|
|
52
54
|
# @see #controller_files
|
|
53
55
|
def self.controller_classes
|
|
54
56
|
controller_files.map do |path|
|
|
55
|
-
path
|
|
56
|
-
|
|
57
|
+
# Extract the relative path from the gem root, then convert to class name
|
|
58
|
+
root_path = Pathname.new("#{root}/lib")
|
|
59
|
+
file_path = Pathname.new(path)
|
|
60
|
+
|
|
61
|
+
# Skip files that are not within the gem's lib directory (e.g., from other gem installations)
|
|
62
|
+
next nil if file_path.relative_path_from(root_path).to_s.start_with?("..")
|
|
63
|
+
|
|
64
|
+
relative_path = file_path.relative_path_from(root_path).to_s
|
|
65
|
+
class_name = relative_path.remove("controllers/").remove(".rb").camelize.constantize
|
|
66
|
+
class_name
|
|
67
|
+
end.compact
|
|
57
68
|
end
|
|
58
69
|
end
|
|
59
70
|
end
|
|
@@ -42,8 +42,10 @@ module M365ActiveStorage
|
|
|
42
42
|
# Hook into Rails initialization to set up gem components
|
|
43
43
|
config.after_initialize do
|
|
44
44
|
# Add before_destroy callback to ActiveStorage::Blob to capture deletion identifiers
|
|
45
|
+
# Add after_commit callback to persist SharePoint ID after blob is fully saved
|
|
45
46
|
::ActiveStorage::Blob.class_eval do
|
|
46
47
|
before_destroy :store_filename_for_deletion, if: proc { |blob| blob.service.is_a?(::ActiveStorage::Service::SharepointService) }
|
|
48
|
+
after_commit :persist_sharepoint_id_from_thread_storage, on: [:create], if: proc { |blob| blob.service.is_a?(::ActiveStorage::Service::SharepointService) }
|
|
47
49
|
|
|
48
50
|
private
|
|
49
51
|
|
|
@@ -66,6 +68,22 @@ module M365ActiveStorage
|
|
|
66
68
|
}
|
|
67
69
|
M365ActiveStorage::PendingDelete.store(key, pending_delete_data)
|
|
68
70
|
end
|
|
71
|
+
|
|
72
|
+
# Persist the SharePoint ID from the upload response after blob is saved
|
|
73
|
+
#
|
|
74
|
+
# This callback fires after the blob has been successfully committed to the database
|
|
75
|
+
# via the after_commit hook. It retrieves the upload response stored in thread-local
|
|
76
|
+
# storage by the service.upload method and persists the SharePoint ID to avoid
|
|
77
|
+
# being overwritten by Active Storage's blob.save.
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
80
|
+
def persist_sharepoint_id_from_thread_storage
|
|
81
|
+
upload_response = Thread.current[:sharepoint_upload_response]
|
|
82
|
+
return unless upload_response.present?
|
|
83
|
+
|
|
84
|
+
Thread.current[:sharepoint_upload_response] = nil # Clean up thread storage
|
|
85
|
+
service.send(:persist_sharepoint_id_from_response, key, upload_response)
|
|
86
|
+
end
|
|
69
87
|
end
|
|
70
88
|
end
|
|
71
89
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: m365_active_storage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Óscar León
|
|
@@ -51,7 +51,7 @@ metadata:
|
|
|
51
51
|
allowed_push_host: https://rubygems.org
|
|
52
52
|
homepage_uri: https://github.com/oei-int/m365-active-storage
|
|
53
53
|
source_code_uri: https://github.com/oei-int/m365-active-storage
|
|
54
|
-
changelog_uri: https://github.com/oei-int/m365-active-storage/CHANGELOG.md
|
|
54
|
+
changelog_uri: https://github.com/oei-int/m365-active-storage/blob/main/CHANGELOG.md
|
|
55
55
|
documentation_uri: https://rubydoc.info/gems/m365_active_storage
|
|
56
56
|
bug_tracker_uri: https://github.com/oei-int/m365-active-storage/issues
|
|
57
57
|
rdoc_options:
|