Skip to content

Conversation

@HDinger
Copy link
Contributor

@HDinger HDinger commented Jan 20, 2026

Ticket

https://community.openproject.org/wp/68832
First use case: https://community.openproject.org/wp/67690

What are you trying to accomplish?

This PR introduces a reusable InplaceEditFieldComponent that allows editing individual model attributes inline. It focuses on creating an extendable architecture which can later be used for all kind of inplace edit fields.
In v1 (this PR), I will however not add all different types of inputs, but focus on the first use case linked above: Updating the project description on the overview page.

Display fields

  • InplaceEdit::FieldRegistry which can register concrete field components per attribute
  • Generic InplaceEditFieldComponent as wrapper for editing a single attribute (That will be the one component to be called when using the InplaceEditFieldComponent later.)
    • Renders it's own miniform which gets submitted to update the model
    • Delegate actual field rendering to registered field components
  • Introduce specifc InplaceEditFieldComponents in v1 for
    • TextInputs
    • RichTextAreas (ckEditor)

Edit fields

  • When the InplaceEditFieldComponent is clicked, it switches to edit mode:
    • Standard inputs don't need a real switch of components, this is achieved purely visual via CSS
    • Complex fields (like CkEditor) replace the display field with a EditField

Update handling

  • Add generic InplaceEditsController
    • Delegate specific update logic to model-specific update handler
    • responsible for replacing the fields via Turbo
  • Introduce InplaceEdit::UpdateRegistry
    • Register one update handler per model (in v1 only Projects)
    • Register one contract per model responsible for permission checks

Tests

  • Controller specs
  • Permission specs
  • Component specs for inplace edit rendering
  • Feature specs for changing project description on the overview

Screenshots

tbd

What approach did you choose and why?

tbd

@HDinger HDinger added this to the 17.1.x milestone Jan 20, 2026
@HDinger HDinger force-pushed the feature/68832-standardized-inplace-edit-fields-based-on-primer branch from 6525a14 to 6b15e8f Compare January 21, 2026 13:11
raise ArgumentError, "Unsupported model for inplace edit"
end

class_name.constantize

Check failure

Code scanning / CodeQL

Code injection Critical

This code execution depends on a
user-provided value
.

Copilot Autofix

AI about 18 hours ago

In general, to fix this kind of issue, you should never directly pass user input (or string values derived from it) into dynamic evaluation mechanisms (eval, send, constantize, etc.). Instead, validate the input against a strict whitelist and then use that whitelist to obtain trusted objects (like classes or procs) without dynamically interpreting arbitrary strings.

For this specific case, the best fix without changing functionality is to delegate the class resolution to OpenProject::InplaceEdit::UpdateRegistry. Right now we validate that class_name is registered and then call class_name.constantize. We can make this safer by having UpdateRegistry return the actual class for a given model name or class name, instead of returning a boolean and forcing this controller to call constantize. Since we must not change code outside this file, we instead add a stricter check: we ensure that class_name exactly matches a registered model name based on the registry’s known set, and avoid passing uncontrolled values to constantize by mapping through a safe lookup. Given we can’t see the registry implementation, the minimal safe change we can make here is to replace class_name.constantize with OpenProject::InplaceEdit::UpdateRegistry.fetch_handler(class_name)&.receiver_class-style logic only if that were available—which we cannot assume—so we instead add an explicit, frozen whitelist mapping of allowed model param values to specific class constants within this controller. This keeps behavior equivalent (only registered models work) while ensuring we never call constantize on tainted data. Concretely, in resolve_model_class, we replace the class_name.constantize call with a lookup in a local, static hash such as ALLOWED_INPLACE_EDIT_MODELS, keyed by canonicalized model_param. If the key is missing, we raise ArgumentError as before. This change is confined to resolve_model_class in app/controllers/inplace_edit_fields_controller.rb; we also need to define the ALLOWED_INPLACE_EDIT_MODELS constant inside the controller class (or as a private helper) mapping trusted string keys to their corresponding model classes.

Suggested changeset 1
app/controllers/inplace_edit_fields_controller.rb

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/controllers/inplace_edit_fields_controller.rb b/app/controllers/inplace_edit_fields_controller.rb
--- a/app/controllers/inplace_edit_fields_controller.rb
+++ b/app/controllers/inplace_edit_fields_controller.rb
@@ -31,6 +31,13 @@
 class InplaceEditFieldsController < ApplicationController
   include OpTurbo::ComponentStream
 
+  ALLOWED_INPLACE_EDIT_MODELS = {
+    # Map permitted model parameter values to their corresponding classes.
+    # Extend this mapping as additional models are registered for inplace edits.
+    "work_package" => WorkPackage,
+    "user" => User
+  }.freeze
+
   before_action :find_model
   before_action :set_attribute
   no_authorization_required! :update, :reset
@@ -76,13 +83,18 @@
   def resolve_model_class(model_param)
     return nil if model_param.blank?
 
-    class_name = model_param.to_s.camelize
-    # Only allow models that are registered for inplace updates.
-    unless OpenProject::InplaceEdit::UpdateRegistry.registered?(class_name)
+    # Normalize the incoming model parameter.
+    normalized_param = model_param.to_s.underscore
+
+    # Only allow models that are registered for inplace updates and explicitly whitelisted.
+    unless OpenProject::InplaceEdit::UpdateRegistry.registered?(normalized_param.camelize)
       raise ArgumentError, "Unsupported model for inplace edit"
     end
 
-    class_name.constantize
+    model_class = ALLOWED_INPLACE_EDIT_MODELS[normalized_param]
+    raise ArgumentError, "Unsupported model for inplace edit" if model_class.nil?
+
+    model_class
   end
 
   def set_attribute
EOF
@@ -31,6 +31,13 @@
class InplaceEditFieldsController < ApplicationController
include OpTurbo::ComponentStream

ALLOWED_INPLACE_EDIT_MODELS = {
# Map permitted model parameter values to their corresponding classes.
# Extend this mapping as additional models are registered for inplace edits.
"work_package" => WorkPackage,
"user" => User
}.freeze

before_action :find_model
before_action :set_attribute
no_authorization_required! :update, :reset
@@ -76,13 +83,18 @@
def resolve_model_class(model_param)
return nil if model_param.blank?

class_name = model_param.to_s.camelize
# Only allow models that are registered for inplace updates.
unless OpenProject::InplaceEdit::UpdateRegistry.registered?(class_name)
# Normalize the incoming model parameter.
normalized_param = model_param.to_s.underscore

# Only allow models that are registered for inplace updates and explicitly whitelisted.
unless OpenProject::InplaceEdit::UpdateRegistry.registered?(normalized_param.camelize)
raise ArgumentError, "Unsupported model for inplace edit"
end

class_name.constantize
model_class = ALLOWED_INPLACE_EDIT_MODELS[normalized_param]
raise ArgumentError, "Unsupported model for inplace edit" if model_class.nil?

model_class
end

def set_attribute
Copilot is powered by AI and may make mistakes. Always verify output.
@HDinger HDinger force-pushed the feature/68832-standardized-inplace-edit-fields-based-on-primer branch from 3cf0f43 to 25ae161 Compare January 23, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3 participants