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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d083b38ef3eacb7fa4d749c7c8a1de3c11fd3e8e86350d11c9533f9f295034e
4
- data.tar.gz: b8db90e6bd4d2f16c5664ae495cdf4efed60af27fe42bb6823dc4d68bd729e73
3
+ metadata.gz: 72492311b32ca0b097c13fdec2e254d43006191dfeaa4a6d2245640ac7cd0b97
4
+ data.tar.gz: 281c7e93c835fb9c8bab29d29f0fc9efdb066c44457327ddd4be5eb71592cf63
5
5
  SHA512:
6
- metadata.gz: d307d02eb5b143fd26c31cb9711d71e4d8f4668589c830334bc90158615b41138b151f1c125ce2ee8b0c5f05198c24befdcb0478cb176fd74f2935ae407cc010
7
- data.tar.gz: 771e399e44037c9a5637d82b16f9cb0ba3d23d42ea699dfcfc70deef533e9873aeec3f3a2cf64df9727917e9164b99615e0da99c8ca8840548f0b49645484055
6
+ metadata.gz: 0ecc16539d70ec002cb69f6aa1dde824e52ad6e51ec2ba2ea76ff0e9cc54de1aa714098f3ac301784489db932d3390cd93e158f6c5f38898592877a3350f1c55
7
+ data.tar.gz: 8d73cd54cec9435e1db2b3d7ac7583430d3fe450c83856dccf67fef018d91fcb162516f60cb15071f462855918d86c28c6d891d26e8ef46f564107908d46655b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
- ## [Unreleased]
1
+ ## [1.1.1] - 2026-04-10
2
2
 
3
- ## [0.1.0] - 2026-01-13
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
- handle_upload_response(key, response)
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 after successful upload.
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 successful upload response body
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
- payload = JSON.parse(response.body)
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+folder/sub+folder/file.pdf")
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.remove("#{root}/lib/").remove("controllers/").remove(".rb").camelize.constantize
56
- end
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module M365ActiveStorage
4
4
  # The version of the m365_active_storage gem
5
- VERSION = "1.1.0"
5
+ VERSION = "1.1.1"
6
6
  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.0
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: