{"record":{"id":"39a4716dd056a2a1","repo":"instructure/canvas-lms","slug":"content-item-is-invalid","errorCode":null,"errorMessage":"#{content_item} is invalid","messagePattern":"#(.+?) is invalid","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/models/content_participation.rb","lineNumber":147,"sourceCode":"  end\n  private_class_method :build_item\n\n  def self.same_workflow_state?(participant, workflow_state)\n    participant.present? && participant.workflow_state == workflow_state\n  end\n  private_class_method :same_workflow_state?\n\n  def self.submission_read?(content:, user:)\n    submission_read_state(content, user) != \"unread\"\n  end\n\n  def self.submission_item_read?(content:, user:, content_item:)\n    submission_read_state(content, user, content_item) != \"unread\"\n  end\n\n  def self.submission_read_state(content, user, content_item = nil)\n    raise \"content is not a Submission\" unless content.is_a?(Submission)\n    raise \"#{content_item} is invalid\" if content_item.present? && !CONTENT_ITEMS.include?(content_item)\n\n    states = if content_item.present?\n               ContentParticipation.where(content:, user:, content_item:).pluck(:workflow_state)\n             else\n               ContentParticipation.where(content:, user:).pluck(:workflow_state)\n             end\n\n    return nil if states.empty?\n    return \"unread\" if states.any?(\"unread\")\n\n    \"read\"\n  end\n\n  def self.items_by_submission(participations, workflow_state)\n    unread_items = {}\n\n    participations.each do |cp|\n      unread_items[cp.content_id] ||= []","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/content_participation.rb#L129-L165","documentation":"submission_read_state validates the optional content_item against the CONTENT_ITEMS allowlist and raises \"#{content_item} is invalid\" for any value not in the list, since it is used directly as a query column.","triggerScenarios":"Calling submission_read_state/submission_item_read? with a content_item symbol/string not in ContentParticipation::CONTENT_ITEMS (typo, wrong enum value, atom key from an API payload).","commonSituations":"Clients pass arbitrary content_item values from request params; enum renamed in a refactor so old callers pass obsolete keys; symbol vs string mismatch not normalized before the allowlist check.","solutions":["Pass one of the exact values in ContentParticipation::CONTENT_ITEMS","Normalize/whitelist user-supplied content_item params before the call","Check CONTENT_ITEMS in this file to confirm the current allowed keys"],"exampleFix":"# before\nContentParticipation.submission_item_read?(content:, user:, content_item: params[:item].to_sym)\n# after\nitem = params[:item].to_sym\nContentParticipation.submission_item_read?(content:, user:, content_item: item) if ContentParticipation::CONTENT_ITEMS.include?(item)","handlingStrategy":"validation","validationCode":"item = params[:content_item]&.to_sym\nraise 'invalid content_item' unless item.nil? || ContentParticipation::CONTENT_ITEMS.include?(item)","typeGuard":"valid_item = ->(v) { v.nil? || ContentParticipation::CONTENT_ITEMS.include?(v) }","tryCatchPattern":"begin\n  ContentParticipation.submission_item_read?(content:, user:, content_item: item)\nrescue RuntimeError => e\n  raise e unless e.message.end_with?('is invalid')\n  # fall back to whole-content read state without content_item\nend","preventionTips":["Whitelist content_item from user input against CONTENT_ITEMS","Grep call sites whenever CONTENT_ITEMS enum changes","Prefer the enum constant over hand-typed symbols"],"tags":["ruby","content-participation","enum"],"backgroundTag":"invalid-enum-value","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}