Merge branch 'dev' into release/9.0

pull/7437/head
Oliver Günther 6 years ago
commit 27fa97aaea
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 2
      CONTRIBUTING.md
  2. 10
      Dockerfile
  3. 6
      app/controllers/application_controller.rb
  4. 1
      app/helpers/application_helper.rb
  5. 31
      app/helpers/colors_helper.rb
  6. 7
      app/models/color.rb
  7. 4
      app/models/journal/aggregated_journal.rb
  8. 7
      app/models/permitted_params.rb
  9. 2
      app/models/queries/projects/orders/latest_activity_at_order.rb
  10. 2
      app/models/status.rb
  11. 14
      app/models/user.rb
  12. 6
      app/models/version.rb
  13. 23
      app/views/colors/_color_autocomplete_field.html.erb
  14. 2
      app/views/enumerations/_form.html.erb
  15. 1
      app/views/highlighting/styles.css.erb
  16. 2
      app/views/statuses/_form.html.erb
  17. 3
      app/views/types/form/_settings.html.erb
  18. 4
      app/views/work_packages/bulk/edit.html.erb
  19. 4
      app/views/work_packages/moves/new.html.erb
  20. 824
      config/brakeman.ignore
  21. 2
      config/locales/en.yml
  22. 3
      config/locales/js-en.yml
  23. 22
      db/migrate/20180105130053_rebuild_dag.rb
  24. 5
      db/migrate/20180510184732_rename_planning_elemnt_type_colors_to_colors.rb
  25. 4
      docker/entrypoint.sh
  26. 22
      docker/mysql-to-postgres/Dockerfile
  27. 21
      docker/mysql-to-postgres/bin/build
  28. 180
      docker/mysql-to-postgres/bin/migrate-mysql-to-postgres
  29. 23
      frontend/src/app/components/filters/query-filters/query-filters.component.html
  30. 16
      frontend/src/app/components/filters/query-filters/query-filters.component.ts
  31. 2
      frontend/src/app/components/op-context-menu/op-context-menu.service.ts
  32. 8
      frontend/src/app/components/wp-fast-table/builders/highlighting/highlighting.functions.ts
  33. 111
      frontend/src/app/modules/common/colors/colors-autocompleter.component.ts
  34. 4
      lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/versions.rb
  35. 4
      lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
  36. 2
      modules/boards/spec/features/support/onboarding_steps.rb
  37. 5
      spec/features/work_packages/table/queries/filter_spec.rb
  38. 2
      spec/support/components/ng_select_autocomplete_helpers.rb
  39. 9
      spec/support/components/work_packages/filters.rb

@ -111,7 +111,7 @@ which are not labelled as `work in progress` by us.
## Security
If you notice a security issue in OpenProject, please send us a gpg encrypted email to security@openproject.org and describe the issue you found. Download our public gpg key [here](https://pgp.mit.edu/pks/lookup?op=get&search=0x7D669C6D47533958).
If you notice a security issue in OpenProject, please send us a gpg encrypted email to security@openproject.com and describe the issue you found. Download our public gpg key [here](https://pgp.mit.edu/pks/lookup?op=get&search=0x7D669C6D47533958).
Please include a description on how to reproduce the issue if possible. Our security team will get your email and will attempt to reproduce and fix the issue as soon as possible.

@ -18,6 +18,8 @@ ENV OPENPROJECT_INSTALLATION__TYPE docker
ENV NEW_RELIC_AGENT_ENABLED false
ENV ATTACHMENTS_STORAGE_PATH $APP_DATA_PATH/files
ENV PGLOADER_DEPENDENCIES "libsqlite3-dev make curl gawk freetds-dev libzip-dev"
# Set a default key base, ensure to provide a secure value in production environments!
ENV SECRET_KEY_BASE OVERWRITE_ME
@ -35,10 +37,18 @@ RUN apt-get update -qq && \
memcached \
postfix \
postgresql \
$PGLOADER_DEPENDENCIES \
apache2 \
supervisor && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# pgloader
ENV CCL_DEFAULT_DIRECTORY /opt/ccl
COPY docker/mysql-to-postgres/bin/build /tmp/build-pgloader
RUN /tmp/build-pgloader && rm /tmp/build-pgloader
# Add MySQL-to-Postgres migration script to path (used in entrypoint.sh)
COPY docker/mysql-to-postgres/bin/migrate-mysql-to-postgres /usr/local/bin/
# Set up pg defaults
RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.6/main/pg_hba.conf
RUN echo "listen_addresses='*'" >> /etc/postgresql/9.6/main/postgresql.conf

@ -114,8 +114,10 @@ class ApplicationController < ActionController::Base
status: :bad_request
end
rescue_from StandardError do |exception|
render_500 exception: exception
unless Rails.application.config.consider_all_requests_local
rescue_from StandardError do |exception|
render_500 exception: exception
end
end
before_action :user_setup,

@ -367,6 +367,7 @@ module ApplicationHelper
params.permit!
back_url = url_for(params)
end
hidden_field_tag('back_url', back_url) unless back_url.blank?
end

@ -29,10 +29,13 @@
#++
module ColorsHelper
def options_for_colors(colored_thing, default_label: I18n.t('colors.label_no_color'), default_color: '')
s = content_tag(:option, default_label, value: default_color)
def options_for_colors(colored_thing, allow_bright_colors)
colors = []
Color.find_each do |c|
next if !allow_bright_colors && c.super_bright?
options = {}
options[:name] = c.name
options[:value] = c.id
options[:data] = {
color: c.hexcode,
@ -41,9 +44,13 @@ module ColorsHelper
}
options[:selected] = true if c.id == colored_thing.color_id
s << content_tag(:option, c.name, options)
colors.push(options)
end
s
colors.to_json
end
def selected_color(colored_thing)
colored_thing.color_id
end
def darken_color(hex_color, amount = 0.4)
@ -61,6 +68,21 @@ module ColorsHelper
content_tag(:span, color.hexcode, class: 'color--text-preview', style: style)
end
#
# Styles to display colors itself (e.g. for the colors autocompleter)
##
def color_css
Color.find_each do |color|
concat ".__hl_inline_color_#{color.id}_dot::before { background-color: #{color.hexcode} !important;}"
concat ".__hl_inline_color_#{color.id}_dot::before { border: 1px solid #555555 !important;}" if color.bright?
concat ".__hl_inline_color_#{color.id}_text { color: #{color.hexcode} !important;}"
concat ".__hl_inline_color_#{color.id}_text { -webkit-text-stroke: 0.5px grey; text-stroke: 0.5px grey;}" if color.super_bright?
end
end
#
# Styles to display the color of attributes (type, status etc.) for example in the WP view
##
def resource_color_css(name, scope)
scope.includes(:color).find_each do |entry|
color = entry.color
@ -77,6 +99,7 @@ module ColorsHelper
if name === 'type'
concat ".__hl_inline_#{name}_#{entry.id} { color: #{color.hexcode} !important;}"
concat ".__hl_inline_#{name}_#{entry.id} { -webkit-text-stroke: 0.5px grey;}" if color.super_bright?
else
concat ".__hl_inline_#{name}_#{entry.id}::before { #{background_style}; border-color: #{border_color}; }\n"
end

@ -71,6 +71,13 @@ class Color < ActiveRecord::Base
brightness_yiq >= 128
end
##
# Returns whether the color is very bright according to
# YIQ lightness.
def super_bright?
brightness_yiq >= 200
end
##
# Sum the color values of each channel
# Same as in frontend color-contrast.functions.ts

@ -56,7 +56,7 @@ class Journal::AggregatedJournal
# Therefore we have to provide the notes_id to the aggregation on top of it being used
# in the where clause to pick the desired AggregatedJournal.
raw_journal = query_aggregated_journals(journal_id: notes_id)
.where("#{table_name}.id = #{notes_id}")
.where("#{table_name}.id = ?", notes_id)
.first
raw_journal ? Journal::AggregatedJournal.new(raw_journal) : nil
@ -143,7 +143,7 @@ class Journal::AggregatedJournal
.query_aggregated_journals(
journable: successor.journable,
until_version: successor.version - 1)
.where("#{version_projection} = #{predecessor.version}")
.where("#{version_projection} = ?", predecessor.version)
.exists?
end

@ -305,12 +305,7 @@ class PermittedParams
type_ids: [],
enabled_module_names: [])
unless params[:project][:custom_field_values].nil?
# Permit the sub-hash for custom_field_values
whitelist[:custom_field_values] = params[:project][:custom_field_values].permit!
end
whitelist
whitelist.merge(custom_field_values(:project))
end
def time_entry

@ -38,6 +38,6 @@ class Queries::Projects::Orders::LatestActivityAtOrder < Queries::BaseOrder
private
def order
model.order("activity.#{attribute} #{direction}")
model.order("activity.latest_activity_at #{direction}")
end
end

@ -48,7 +48,7 @@ class Status < ActiveRecord::Base
after_save :unmark_old_default_value, if: :is_default?
def unmark_old_default_value
Status.where(['id <> ?', id]).update_all("is_default=#{self.class.connection.quoted_false}")
Status.where.not(id: id).update_all(is_default: false)
end
# Returns the default status for new issues

@ -444,10 +444,16 @@ class User < Principal
end
def notified_project_ids=(ids)
Member.where(['user_id = ?', id])
.update_all("mail_notification = #{self.class.connection.quoted_false}")
Member.where(['user_id = ? AND project_id IN (?)', id, ids])
.update_all("mail_notification = #{self.class.connection.quoted_true}") if ids && !ids.empty?
Member
.where(user_id: id)
.update_all(mail_notification: false)
if ids && !ids.empty?
Member
.where(user_id: id, project_id: ids)
.update_all(mail_notification: true)
end
@notified_projects_ids = nil
notified_projects_ids
end

@ -259,13 +259,17 @@ class Version < ActiveRecord::Base
@issues_progress ||= {}
@issues_progress[open] ||= begin
progress = 0
if issues_count > 0
ratio = open ? 'done_ratio' : 100
sum_sql = self.class.sanitize_sql_array(
["COALESCE(#{WorkPackage.table_name}.estimated_hours, ?) * #{ratio}", estimated_average]
)
done = fixed_issues
.where(statuses: { is_closed: !open })
.includes(:status)
.sum("COALESCE(#{WorkPackage.table_name}.estimated_hours, #{estimated_average}) * #{ratio}")
.sum(sum_sql)
progress = done.to_f / (estimated_average * issues_count)
end
progress

@ -1,12 +1,19 @@
<% form_field_class = form_field_class || '' %>
<% container_class = container_class || '' %>
<div class="form--field <%= form_field_class %>">
<%= form.select :color_id,
options_for_colors(object),
{},
container_class: container_class.present? ? container_class : '-middle',
class: 'colors-autocomplete' %>
<% highlight_text_inline = highlight_text_inline || false %>
<div class="form--field <%= form_field_class %>"<%= %>>
<label class="form--label">
<%= t('activerecord.attributes.color') %>
</label>
<div class="form--field-container">
<%= form.hidden_field :color_id %>
<%= content_tag('colors-autocompleter',
'',
class: "colors-autocomplete form--select-container " + "#{container_class.present? ? container_class : '-middle'}",
data: { colors: options_for_colors(object, allow_bright_colors),
selected_color: selected_color(object),
highlight_text_inline: highlight_text_inline,
update_input: type + '[color_id]'}) %>
</div>
<div class="form--field-instructions"><%= label %></div>
<colors-autocompleter></colors-autocompleter>
</div>

@ -44,6 +44,8 @@ See docs/COPYRIGHT.rdoc for more details.
locals: {
form: f,
object: @enumeration,
type: 'enumeration',
allow_bright_colors: true,
label: @enumeration.color_label
} %>
<% end %>

@ -2,6 +2,7 @@
<%= resource_color_css('status', ::Status) %>
<%= resource_color_css('priority', ::IssuePriority) %>
<%= resource_color_css('type', ::Type) %>
<%= color_css() %>
<%# Overdue tasks %>
.__hl_date_due_today { color: #F76707 !important; }

@ -57,6 +57,8 @@ See docs/COPYRIGHT.rdoc for more details.
locals: {
form: f,
object: @status,
type: 'status',
allow_bright_colors: true,
label: t('statuses.edit.status_color_text')
} %>

@ -36,6 +36,9 @@ See docs/COPYRIGHT.rdoc for more details.
locals: {
form: f,
object: @type,
type: 'type',
allow_bright_colors: false,
highlight_text_inline: true,
label: t('types.edit.type_color_text'),
form_field_class: '-wide-label',
container_class: '-slim'

@ -59,7 +59,9 @@ See docs/COPYRIGHT.rdoc for more details.
<%= styled_form_tag(url_for(controller: '/work_packages/bulk', action: :update),
method: :put, class: '-wide-labels') do %>
<%= @work_packages.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
<% @work_packages.each do |wp| %>
<%= hidden_field_tag 'ids[]', wp.id %>
<% end %>
<%= back_url_hidden_field_tag %>
<section class="form--section">
<fieldset class="form--fieldset">

@ -46,7 +46,9 @@ See docs/COPYRIGHT.rdoc for more details.
<%= styled_form_tag({action: 'create'},
id: 'move_form',
class: '-wide-labels') do %>
<%= @work_packages.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
<% @work_packages.each do |wp| %>
<%= hidden_field_tag 'ids[]', wp.id %>
<% end %>
<%= back_url_hidden_field_tag %>
<refresh-on-form-changes
url="<%= url_for(action: 'new') %>"

@ -1,25 +1,5 @@
{
"ignored_warnings": [
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "04438fab5130bf26f2f68cc99a87a3bd97f4da2caf256929686c140e2d04d9a0",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/journal/aggregated_journal.rb",
"line": 59,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "query_aggregated_journals(:journal_id => notes_id).where(\"#{table_name}.id = #{notes_id}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Journal::AggregatedJournal",
"method": "with_notes_id"
},
"user_input": "notes_id",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
@ -38,47 +18,47 @@
},
"user_input": "wiki_storage_sql",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "static SQL string"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "116016c47a97c5855853cea277e1c96d374ffabcde66c904acc9265d7ea3d2a7",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/versions.rb",
"line": 90,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "where(\"#{Journal.table_name}.version < #{journal_at(value)}\")",
"warning_type": "Denial of Service",
"warning_code": 76,
"fingerprint": "062a691c8a6ad25d8015bebfcc329af2e3132ed88a646c5cc8ff797312de84a9",
"check_name": "RegexDoS",
"message": "Model attribute used in regular expression",
"file": "app/models/mail_handler.rb",
"line": 309,
"link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/",
"code": "/^(#{\"#{attr.to_s.humanize}|#{all_attribute_translations(user.language)[attr]}|#{all_attribute_translations(Setting.default_language)[attr]}\"})[ \\t]*:[ \\t]*(#{\".+\"})\\s*$/i",
"render_path": null,
"location": {
"type": "method",
"class": "Redmine::Acts::Journalized::Versions",
"method": "before"
"class": "MailHandler",
"method": "extract_keyword!"
},
"user_input": "journal_at(value)",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"user_input": "Setting.default_language",
"confidence": "Weak",
"note": "Settings provided user-input, ignoring DOS aspect"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "12f8086fd28bc6f9c0582b2810ea6b74131dc56273d2c00536de3d4a99463bca",
"fingerprint": "0a7846a219566627938cc3c69924f53dbbcc6973e21081aef5572ffbcedd77d3",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/api/v2/reportings_controller.rb",
"line": 127,
"file": "app/models/project/activity.rb",
"line": 57,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "@project.reportings_via_target.includes(:project).where([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)])",
"code": "Project.select(\"projects.*\").select(\"activity.latest_activity_at\").joins(\"LEFT JOIN (#{latest_activity_sql}) activity ON projects.id = activity.project_id\")",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V2::ReportingsController",
"method": "index"
"class": "Project::Activity::Scopes",
"method": "with_latest_activity"
},
"user_input": "Project.quoted_table_name",
"user_input": "latest_activity_sql",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "static SQL string"
},
{
"warning_type": "File Access",
@ -118,92 +98,14 @@
},
"user_input": "quoted_parent_column_name",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "1dd381d4d189b7875ba40e80be2ccfea8a1aebccb8f0bbc589c07ce90050bce2",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/reportings_controller.rb",
"line": 154,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Project.find(params[:project_id]).reportings_via_target.includes(:project).where(([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)] or [(\" OR \" + \"#{Project.quoted_table_name}.lft < ? AND #{Project.quoted_table_name}.rgt > ?\"), set[0], set[1]]))",
"render_path": null,
"location": {
"type": "method",
"class": "ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
"fingerprint": "2634fe41842902ed42b413062e594e4a8431547a0144d471d963da1187a388bb",
"check_name": "Render",
"message": "Render path contains parameter value",
"file": "app/views/repositories/settings/_vendor_attribute_groups.html.erb",
"line": 28,
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(partial => \"/repositories/settings/#{Scm::RepositoryFactoryService.new(Project.find(params[:project_id]), params).repository.vendor}/#{type}\", { :locals => ({ :form => f, :repository => Scm::RepositoryFactoryService.new(Project.find(params[:project_id]), params).repository }) })",
"render_path": [{"type":"controller","class":"RepositoriesController","method":"edit","line":65,"file":"app/controllers/repositories_controller.rb"},{"type":"template","name":"repositories/settings/repository_form","line":3,"file":"app/views/repositories/settings/repository_form.js.erb"},{"type":"template","name":"repositories/_settings","line":57,"file":"app/views/repositories/_settings.html.erb"},{"type":"template","name":"repositories/settings/_vendor_form","line":43,"file":"app/views/repositories/settings/_vendor_form.html.erb"}],
"location": {
"type": "template",
"template": "repositories/settings/_vendor_attribute_groups"
},
"user_input": "params",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): Vendor and type is statically decided"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "266e6c2ce8a175d146d7b58e0546686fc18ea90e6dfdb90785ad36d11bb17f2e",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/layouts/user_mailer.html.erb",
"line": 71,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.localized_emails_footer)",
"render_path": null,
"location": {
"type": "template",
"template": "layouts/user_mailer"
},
"user_input": "Setting.text_formatting",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): Admin-only formatted text"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "2d90bf580babd84fcda4455089d72832b56407579504bac27345bb028b62b50d",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/api/v2/reportings_controller.rb",
"line": 154,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "@project.reportings_via_target.includes(:project).where(([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)] or [(\" OR \" + \"#{Project.quoted_table_name}.lft < ? AND #{Project.quoted_table_name}.rgt > ?\"), set[0], set[1]]))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V2::ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "Never called with user input"
},
{
"warning_type": "Remote Code Execution",
"warning_code": 24,
"fingerprint": "3d0ae98ed047bde3475cd8a4afa84dbc2de8845bef18ca9abf5e25c8673057a9",
"check_name": "UnsafeReflection",
"message": "Unsafe reflection method const_get called with model attribute",
"message": "Unsafe reflection method `const_get` called with model attribute",
"file": "app/controllers/attribute_help_texts_controller.rb",
"line": 112,
"link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/",
@ -216,204 +118,7 @@
},
"user_input": "AttributeHelpText.available_types.find",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input to const_get"
},
{
"warning_type": "Mass Assignment",
"warning_code": 70,
"fingerprint": "3f8c4150cbec05c711a2f5affb016b4e7bd729d97c7c49608f702ab12382ef93",
"check_name": "MassAssignment",
"message": "Parameters should be whitelisted for mass assignment",
"file": "app/helpers/application_helper.rb",
"line": 497,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit!",
"render_path": null,
"location": {
"type": "method",
"class": "ApplicationHelper",
"method": "back_url_to_current_page_hidden_field_tag"
},
"user_input": null,
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): Only used for url_for which re-uses data from routes to generate valid params"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "41123de9d9e921bd7b8f064fe00383dc103fe5f4f52653d9560e76da590b8e36",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/work_packages/bulk/edit.html.erb",
"line": 35,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "WorkPackage.includes(:project).where(:id => ((params[:work_package_id] or params[:ids]))).order(\"id ASC\").collect do\n hidden_field_tag(\"ids[]\", i.id)\n end.join",
"render_path": [{"type":"controller","class":"WorkPackages::BulkController","method":"edit","line":46,"file":"app/controllers/work_packages/bulk_controller.rb"}],
"location": {
"type": "template",
"template": "work_packages/bulk/edit"
},
"user_input": "WorkPackage.includes(:project).where(:id => ((params[:work_package_id] or params[:ids])))",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): Only internal ids used"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "4813324589832e2cf3abc1eba58012465cd08e3890cfac42f3423871a2273aed",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/work_packages/bulk/edit.html.erb",
"line": 32,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "WorkPackage.includes(:project).where(:id => ((params[:work_package_id] or params[:ids]))).order(\"id ASC\").collect do\n content_tag(\"li\", (link_to(h(\"#{i.type} ##{i.id}\"), work_package_path(i)) + h(\": #{i.subject}\")))\n end.join(\"\\n\")",
"render_path": [{"type":"controller","class":"WorkPackages::BulkController","method":"edit","line":46,"file":"app/controllers/work_packages/bulk_controller.rb"}],
"location": {
"type": "template",
"template": "work_packages/bulk/edit"
},
"user_input": "WorkPackage.includes(:project).where(:id => ((params[:work_package_id] or params[:ids])))",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): User data is escaped with h()"
},
{
"warning_type": "Remote Code Execution",
"warning_code": 24,
"fingerprint": "4bf7d21114e2bb347609451957ac3e722cfabc12c58733aca56c1b5068e1eada",
"check_name": "UnsafeReflection",
"message": "Unsafe reflection method constantize called with parameter value",
"file": "app/controllers/watchers_controller.rb",
"line": 50,
"link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/",
"code": "params[:object_type].singularize.camelcase.constantize",
"render_path": null,
"location": {
"type": "method",
"class": "WatchersController",
"method": "find_watched_by_object"
},
"user_input": "params[:object_type].singularize.camelcase",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): Safe reflection due to strict checks to allowed instances"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "51c873d0c99ac23be184826ad73e405838c095d633a8ed123e1f99ccabb96485",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/queries/projects/orders/required_disk_space_order.rb",
"line": 43,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "model.order(\"#{Project.required_disk_space_sum} #{direction}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Queries::Projects::Orders::RequiredDiskSpaceOrder",
"method": "order"
},
"user_input": "Project.required_disk_space_sum",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Cross-Site Request Forgery",
"warning_code": 7,
"fingerprint": "5e65c348a8bd7b3086babd3cecce252782c80a0f6298dcef685a8d0e31f175e5",
"check_name": "ForgerySetting",
"message": "'protect_from_forgery' should be called in SysController",
"file": "app/controllers/sys_controller.rb",
"line": 32,
"link": "https://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/",
"code": null,
"render_path": null,
"location": {
"type": "controller",
"controller": "SysController"
},
"user_input": null,
"confidence": "High",
"note": "Invalid (confirmed by oliverguenther): Internal API authentication controller only"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "5f02fcb201690516b2f45f2f67ef000e1947e9f00415e2bfe147341f31d280bb",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/work_package.rb",
"line": 533,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "joins(\"LEFT OUTER JOIN (#{Relation.hierarchy.group(:to_id).select(:to_id, \"MAX(hierarchy) AS depth\").to_sql}) AS max_depth ON max_depth.to_id = work_packages.id\").reorder(\"COALESCE(max_depth.depth, 0) #{direction}\")",
"render_path": null,
"location": {
"type": "method",
"class": "WorkPackage",
"method": "WorkPackage.order_by_ancestors"
},
"user_input": "direction",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Denial of Service",
"warning_code": 76,
"fingerprint": "6b5137a422554a5461478ec648dce2195ad50ddfac673a0b6c5da654da7b1eb1",
"check_name": "RegexDoS",
"message": "Model attribute used in regex",
"file": "app/models/mail_handler.rb",
"line": 288,
"link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/",
"code": "/^(#{[attr.to_s.humanize, all_attribute_translations(user.language)[attr], all_attribute_translations(Setting.default_language)[attr]].join(\"|\")})[ \\t]*:[ \\t]*(#{\".+\"})\\s*$/i",
"render_path": null,
"location": {
"type": "method",
"class": "MailHandler",
"method": "extract_keyword!"
},
"user_input": "Setting.default_language",
"confidence": "Weak",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "76f52669a570406621f7ecfde04bbe98032eb724800b58ea0ba21b270de39ce3",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/status.rb",
"line": 49,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Status.where([\"id <> ?\", id]).update_all(\"is_default=#{self.class.connection.quoted_false}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Status",
"method": "unmark_old_default_value"
},
"user_input": "self.class.connection.quoted_false",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Mass Assignment",
"warning_code": 70,
"fingerprint": "7d12897ac6a83af64ed48129cd00675bdf68d0ab08a9fe1a20cd5633790d9182",
"check_name": "MassAssignment",
"message": "Parameters should be whitelisted for mass assignment",
"file": "app/models/permitted_params.rb",
"line": 303,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.require(:timeline).permit(:name, :options => ({})).permit!",
"render_path": null,
"location": {
"type": "method",
"class": "PermittedParams",
"method": "timeline"
},
"user_input": null,
"confidence": "Medium",
"note": "Removed in 8.0"
"note": "const_get is only used from whitelisted set of types"
},
{
"warning_type": "Dynamic Render Path",
@ -425,132 +130,14 @@
"line": 32,
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(partial => Redmine::Plugin.find(params[:id]).settings[:partial], { :locals => ({ :settings => Setting[\"plugin_#{Redmine::Plugin.find(params[:id]).id}\"] }) })",
"render_path": [{"type":"controller","class":"SettingsController","method":"plugin","line":70,"file":"app/controllers/settings_controller.rb"}],
"render_path": [{"type":"controller","class":"SettingsController","method":"plugin","line":71,"file":"app/controllers/settings_controller.rb","rendered":{"name":"settings/plugin","file":"/home/oliver/openproject/dev/app/views/settings/plugin.html.erb"}}],
"location": {
"type": "template",
"template": "settings/plugin"
},
"user_input": "params[:id]",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "857f76189f1ebd5c145cd5c35e5fae051d59f54f2fee0231609a3ec8e1cd7072",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/project/activity.rb",
"line": 56,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Project.select(\"projects.*, activity.latest_activity_at\").joins(\"LEFT JOIN (#{latest_activity_sql}) activity ON projects.id = activity.project_id\")",
"render_path": null,
"location": {
"type": "method",
"class": "Project::Activity::Scopes",
"method": "with_latest_activity"
},
"user_input": "latest_activity_sql",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "85a463dc3822e216dd57d138b2c78fa4bb66ec2bce2a509ec41d4a5d59de65a6",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/queries/projects/orders/latest_activity_at_order.rb",
"line": 41,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "model.order(\"activity.#{attribute} #{direction}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Queries::Projects::Orders::LatestActivityAtOrder",
"method": "order"
},
"user_input": "attribute",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Cross-Site Request Forgery",
"warning_code": 7,
"fingerprint": "884f6802782762b7f271d663df669bd906bdcb5ae6c3b2b0f69de432d2910448",
"check_name": "ForgerySetting",
"message": "'protect_from_forgery' should be called in MailHandlerController",
"file": "app/controllers/mail_handler_controller.rb",
"line": 31,
"link": "https://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/",
"code": null,
"render_path": null,
"location": {
"type": "controller",
"controller": "MailHandlerController"
},
"user_input": null,
"confidence": "High",
"note": "s"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "8e8c36e50e4cd07cc9cd08f8114c99db3f3d44d53f1107f775bff90001fc365f",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/work_packages/moves/new.html.erb",
"line": 38,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "WorkPackage.includes(:project).where(:id => ((params[:work_package_id] or params[:ids]))).order(\"id ASC\").collect do\n hidden_field_tag(\"ids[]\", i.id)\n end.join",
"render_path": [{"type":"controller","class":"WorkPackages::MovesController","method":"new","line":37,"file":"app/controllers/work_packages/moves_controller.rb"}],
"location": {
"type": "template",
"template": "work_packages/moves/new"
},
"user_input": "WorkPackage.includes(:project).where(:id => ((params[:work_package_id] or params[:ids])))",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): Only internal ids used"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "90f66bf21d85808b17f6a4807262d548cfd9421941d1ca7bed05c2790cb814de",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/api/v2/reportings_controller.rb",
"line": 124,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "@project.reportings_via_source.includes(:project).where([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)])",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V2::ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "91bf807443aff72717534de4fcdbca42e9053fb4dfcedb485070663561a85693",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/api/v2/reportings_controller.rb",
"line": 151,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "@project.reportings_via_source.includes(:project).where(([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)] or [(\" OR \" + \"#{Project.quoted_table_name}.lft < ? AND #{Project.quoted_table_name}.rgt > ?\"), set[0], set[1]]))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V2::ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "partial variable is from static plugin definition"
},
{
"warning_type": "SQL Injection",
@ -570,66 +157,27 @@
},
"user_input": "version_projection",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "9578c686e00182c19d984528388c0b091d9aa401f28bae63bfb01b0159b6660c",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/reportings_controller.rb",
"line": 152,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Project.find(params[:project_id]).reportings_via_source.includes(:project).where(([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)] or [(\" OR \" + \"#{Project.quoted_table_name}.lft < ? AND #{Project.quoted_table_name}.rgt > ?\"), set[0], set[1]]))",
"render_path": null,
"location": {
"type": "method",
"class": "ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "version_projection is static"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "96fa66c4cda85c48c18805a94480529ab016eb33e6c7b038964d36b1e0d6c029",
"fingerprint": "9a92548d9b0e0531f76138ad8db70e6d9c4375f7dcd3d7173cc3cd37cb50911d",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/journal/aggregated_journal.rb",
"line": 103,
"line": 411,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Journal.from(\"(#{sql_rough_group(1, journable, until_version, journal_id)}) #{table_name}\")",
"code": "self.class.query_aggregated_journals(:journable => journable).where(\"#{self.class.version_projection} > ?\", version)",
"render_path": null,
"location": {
"type": "method",
"class": "Journal::AggregatedJournal",
"method": "query_aggregated_journals"
},
"user_input": "sql_rough_group(1, journable, until_version, journal_id)",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "9954f90e0ebcea7ced93dcb81589324b59ed305b59fad2f645da9dd5171cc686",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/groups/change_memberships.js.erb",
"line": 39,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "escape_javascript(l(:notice_failed_to_save_members, :errors => (Member.find(permitted_params.group_membership[:membership_id]) or Member.new(:principal => Group.find(params[:id]))).errors.full_messages.join(\", \")))",
"render_path": [{"type":"controller","class":"GroupsController","method":"create_memberships","line":154,"file":"app/controllers/groups_controller.rb"}],
"location": {
"type": "template",
"template": "groups/change_memberships"
"method": "successor"
},
"user_input": "Member.find(permitted_params.group_membership[:membership_id])",
"user_input": "self.class.version_projection",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): Attribute to message is escaped"
"note": "version_projection is static"
},
{
"warning_type": "Command Injection",
@ -638,7 +186,7 @@
"check_name": "Execute",
"message": "Possible command injection",
"file": "lib/open_project/scm/adapters/subversion.rb",
"line": 209,
"line": 228,
"link": "https://brakemanscanner.org/docs/warning_types/command_injection/",
"code": "popen3([\"blame\", \"#{target(path)}@#{(identifier.to_i or \"HEAD\")}\"])",
"render_path": null,
@ -649,27 +197,7 @@
},
"user_input": "target(path)",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): Single argument to non-tty open3"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "b439669154330196a18f18b87af0496c8d141a30b472b813539bc614a23cb5c8",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/journal/aggregated_journal.rb",
"line": 408,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "self.class.query_aggregated_journals(:journable => journable).where(\"#{self.class.version_projection} > ?\", version).except(:order).order(\"#{self.class.version_projection} ASC\")",
"render_path": null,
"location": {
"type": "method",
"class": "Journal::AggregatedJournal",
"method": "successor"
},
"user_input": "self.class.version_projection",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "open3 does not spawn a shell with array-args, each input is escaped individually"
},
{
"warning_type": "SQL Injection",
@ -678,7 +206,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/work_package.rb",
"line": 466,
"line": 520,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "ActiveRecord::Base.connection.select_all(\"select s.id as status_id,\\n s.is_closed as closed,\\n i.project_id as project_id,\\n count(i.id) as total\\n from\\n #{WorkPackage.table_name} i, #{Status.table_name} s\\n where\\n i.status_id=s.id\\n and i.project_id IN (#{project.descendants.active.map(&:id).join(\",\")})\\n group by s.id, s.is_closed, i.project_id\")",
"render_path": null,
@ -689,14 +217,14 @@
},
"user_input": "project.descendants.active.map(&:id).join(\",\")",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "no user input"
},
{
"warning_type": "Denial of Service",
"warning_code": 76,
"fingerprint": "c1448e5550005717fd0491975352fdc389aaf9987f7cfd32cdad1460f5a6a86c",
"check_name": "RegexDoS",
"message": "Model attribute used in regex",
"message": "Model attribute used in regular expression",
"file": "app/models/changeset.rb",
"line": 138,
"link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/",
@ -709,27 +237,7 @@
},
"user_input": "Setting.commit_fix_keywords.downcase",
"confidence": "Weak",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "c1aa5b29ac6d8095270805bd64d774c7e160d85a1157736158cbca78fcff456c",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/user.rb",
"line": 451,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Member.where([\"user_id = ? AND project_id IN (?)\", id, ids]).update_all(\"mail_notification = #{self.class.connection.quoted_true}\")",
"render_path": null,
"location": {
"type": "method",
"class": "User",
"method": "notified_project_ids="
},
"user_input": "self.class.connection.quoted_true",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "Settings provided user-input, ignoring DOS aspect"
},
{
"warning_type": "SQL Injection",
@ -749,45 +257,7 @@
},
"user_input": "quoted_parent_column_name",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "c4e1c49393d6b7948533e116eb00a669d1353ecabe1b1608d9f0c5ec11540bc9",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/layouts/user_mailer.html.erb",
"line": 66,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.localized_emails_header)",
"render_path": null,
"location": {
"type": "template",
"template": "layouts/user_mailer"
},
"user_input": "Setting.text_formatting",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): Admin-only formatted text"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "c8b31c0e32ca511fe63d45a43ab1a48c4b7d189de3e51c983731a9b6849fd4ab",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/views/my/blocks/_news.html.erb",
"line": 31,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "News.limit(10).order(\"#{News.table_name}.created_on DESC\").where(\"#{News.table_name}.project_id in (#{@user.projects.collect do\n m.id\n end.join(\",\")})\")",
"render_path": null,
"location": {
"type": "template",
"template": "my/blocks/_news"
},
"user_input": "@user.projects.collect do\n m.id\n end.join(\",\")",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): Only internal project ids used"
"note": "Never called with user input"
},
{
"warning_type": "Redirect",
@ -807,7 +277,7 @@
},
"user_input": "Attachment.find(params[:id]).external_url.to_s",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): URL is not determined from user input"
"note": "external_url does not take in user input. Only user-provided ID is used to find attachment"
},
{
"warning_type": "SQL Injection",
@ -816,7 +286,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/work_package.rb",
"line": 562,
"line": 616,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "where(\"id IN (SELECT common_id FROM (#{[Relation.hierarchy.where(:from_id => Relation.where(:to => work_packages).hierarchy_or_follows.select(:from_id)).select(\"to_id common_id\"), Relation.where(:to => work_packages).hierarchy_or_follows.select(\"from_id common_id\")].map(&:to_sql).join(\" UNION \")}) following_relations)\")",
"render_path": null,
@ -827,7 +297,7 @@
},
"user_input": "Relation.where(:to => work_packages).hierarchy_or_follows",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "static SQL"
},
{
"warning_type": "SQL Injection",
@ -847,107 +317,7 @@
},
"user_input": "quoted_right_column_name",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "d4edabc9b617b04b17aea1c7d68f6713a408e70d5378f0ca1a61cf704abcd0dc",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/reportings_controller.rb",
"line": 127,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Project.find(params[:project_id]).reportings_via_source.includes(:project).where([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)])",
"render_path": null,
"location": {
"type": "method",
"class": "ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "d548385e2a6bd304c2700632872a0e58f17836a163aaa597e82fdfde036334a7",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/user.rb",
"line": 449,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Member.where([\"user_id = ?\", id]).update_all(\"mail_notification = #{self.class.connection.quoted_false}\")",
"render_path": null,
"location": {
"type": "method",
"class": "User",
"method": "notified_project_ids="
},
"user_input": "self.class.connection.quoted_false",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "d9d70b2895a30cfaabf6feb4bba0a09a306775f1c59abe6e74f639c7244bb488",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/version.rb",
"line": 270,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "fixed_issues.where(:statuses => ({ :is_closed => (not open) })).includes(:status).sum(\"COALESCE(#{WorkPackage.table_name}.estimated_hours, #{estimated_average}) * #{(\"done_ratio\" or 100)}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Version",
"method": "issues_progress"
},
"user_input": "estimated_average",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "df1cfa95719b9279d1f148d9abf9842e9f5b5aa9704b23856846473665f7a906",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/journal/aggregated_journal.rb",
"line": 146,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Journal::AggregatedJournal.query_aggregated_journals(:journable => successor.journable, :until_version => (successor.version - 1)).where(\"#{version_projection} = #{predecessor.version}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Journal::AggregatedJournal",
"method": "hides_notifications?"
},
"user_input": "version_projection",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "df843c9405c03846e42d1e4d8bd5f9fb784fed7532f5350dc481306cf220d5d9",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/principal.rb",
"line": 83,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "where(\"#{Principal.table_name}.status <> #{{ :builtin => 0, :active => 1, :registered => 2, :locked => 3, :invited => 4 }.freeze[:builtin]}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Principal",
"method": "not_builtin"
},
"user_input": "{ :builtin => 0, :active => 1, :registered => 2, :locked => 3, :invited => 4 }.freeze[:builtin]",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
"note": "Never called with user input"
},
{
"warning_type": "SQL Injection",
@ -967,105 +337,9 @@
},
"user_input": "Project.find(params[:project_id]).project_condition(Setting.display_subprojects_work_packages?)",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "e4980aba10de99b642dcc49c9bc0af7ad9b3b1060c4d3081ec5d364a42c96af8",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/controllers/reportings_controller.rb",
"line": 129,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Project.find(params[:project_id]).reportings_via_target.includes(:project).where([(((((((((\"\" + (((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.project_type_id IN (?)\") + \" OR #{Project.quoted_table_name}.project_type_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\") or \"(#{((\"\" + \"#{Reporting.quoted_table_name}.reported_project_status_id IN (?)\") + \" OR #{Reporting.quoted_table_name}.reported_project_status_id IS NULL\")})\")) + \" AND \") + (((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\") or \"(#{((\"\" + \"#{Project.quoted_table_name}.responsible_id IN (?)\") + \" OR #{Project.quoted_table_name}.responsible_id IS NULL\")})\")) + \" AND \") + (\"\" + \"#{Project.quoted_table_name}.lft IN (?)\")) + \" OR \") + (\"\" + \"#{Project.quoted_table_name}.id IN (?)\")), params[:project_types].split(/,/).map(&:to_i), params[:project_statuses].split(/,/).map(&:to_i), params[:project_responsibles].split(/,/).map(&:to_i), Project.find(params[:project_parents].split(/,/).map(&:to_i)).map do\n (p.lft..p.rgt)\n end.inject([]) do\n e.each do\n (r << i)\n end\n(r << i)\n end, params[:grouping_one].split(/,/).map(&:to_i)])",
"render_path": null,
"location": {
"type": "method",
"class": "ReportingsController",
"method": "index"
},
"user_input": "Project.quoted_table_name",
"confidence": "High",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "f4cd13d77b22c79c03e5da9baa4a9764eaccb6b28c0a1b2bac63dedb821369a5",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/journal/aggregated_journal.rb",
"line": 394,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "self.class.query_aggregated_journals(:journable => journable).where(\"#{self.class.version_projection} < ?\", version).except(:order).order(\"#{self.class.version_projection} DESC\")",
"render_path": null,
"location": {
"type": "method",
"class": "Journal::AggregatedJournal",
"method": "predecessor"
},
"user_input": "self.class.version_projection",
"confidence": "Weak",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "f74999cd49a6b90002e8056d484199cbc48b6e81bad050ce19286faf9badad06",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/work_package.rb",
"line": 686,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "ActiveRecord::Base.connection.select_all(\"select s.id as status_id,\\n s.is_closed as closed,\\n j.id as #{options.delete(:field)},\\n count(i.id) as total\\n from\\n #{WorkPackage.table_name} i, #{Status.table_name} s, #{options.delete(:joins)} j\\n where\\n i.status_id=s.id\\n and #{\"i.#{options.delete(:field)}=j.id\"}\\n and i.project_id=#{options.delete(:project).id}\\n group by s.id, s.is_closed, j.id\")",
"render_path": null,
"location": {
"type": "method",
"class": "WorkPackage",
"method": "WorkPackage.count_and_group_by"
},
"user_input": "options.delete(:field)",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input in select_field"
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "fe67e8e02ca6c47d0f5e84212bcd583d68c831feb19f3dbeb5393cbae7354d35",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/versions.rb",
"line": 98,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "where(\"#{Journal.table_name}.version > #{journal_at(value)}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Redmine::Acts::Journalized::Versions",
"method": "after"
},
"user_input": "journal_at(value)",
"confidence": "Medium",
"note": "False positive (confirmed by oliverguenther): No user input"
},
{
"warning_type": "Default Routes",
"warning_code": 11,
"fingerprint": "ff2b76e22c9fd2bc3930f9a935124b9ed9f6ea710bbb5bc7c51505d70ca0f2d5",
"check_name": "DefaultRoutes",
"message": "All public methods in controllers are available as actions in routes.rb",
"file": "config/routes.rb",
"line": 596,
"link": "https://brakemanscanner.org/docs/warning_types/default_routes/",
"code": null,
"render_path": null,
"location": null,
"user_input": null,
"confidence": "High",
"note": "s"
"note": "Static SQL built from `project_condition`"
}
],
"updated": "2018-01-15 10:40:12 +0100",
"brakeman_version": "4.1.1"
"updated": "2019-04-30 09:36:52 +0200",
"brakeman_version": "4.5.0"
}

@ -252,7 +252,7 @@ en:
reset: "Reset to defaults"
type_color_text: |
Click to assign or change the color of this type. The selected color distinguishes work packages
in Gantt charts.
in Gantt charts. It is therefore recommended to use a strong color.
versions:
overview:

@ -285,6 +285,7 @@ en:
label_more_than_ago: "more than days ago"
label_my_page: "My page"
label_next: "Next"
label_no_color: "No color"
label_no_data: "No data to display"
label_no_due_date: "no end date"
label_no_start_date: "no start date"
@ -388,7 +389,7 @@ en:
overview: 'Manage your work within an intuitive <b>Boards</b> view.'
lists: 'You can create multiple lists (columns) within one Board view, e.g. to create a KANBAN board.'
add: 'Click the + will <b>add a new card</b> to the list within a Board.'
drag: 'Drag & Drop your cards within a list to re-order, or the another list. A double click will open the details view.'
drag: 'Drag & Drop your cards within a list to re-order, or to drag to another list. A double click will open the details view.'
wp:
toggler: "Now let's have a look at the <b>Work package</b> section, which gives you a more detailed view of your work."
list: 'This is the <b>Work package</b> list with the important work within your project, such as tasks, features, milestones, bugs, and more. <br> You can create or edit a work package directly within this list. To see its details you can double click on a row.'

@ -58,16 +58,18 @@ class RebuildDag < ActiveRecord::Migration[5.0]
def down
remove_column :relations, :count
remove_index :relations,
name: 'index_relations_hierarchy_follows_scheduling'
remove_index :relations,
name: 'index_relations_only_hierarchy'
remove_index :relations,
name: 'index_relations_to_from_only_follows'
remove_index :relations,
name: 'index_relations_direct_non_hierarchy'
remove_index :relations,
name: 'index_relations_on_type_columns'
if index_exists? :relations, 'index_relations_hierarchy_follows_scheduling'
remove_index :relations,
name: 'index_relations_hierarchy_follows_scheduling'
remove_index :relations,
name: 'index_relations_only_hierarchy'
remove_index :relations,
name: 'index_relations_to_from_only_follows'
remove_index :relations,
name: 'index_relations_direct_non_hierarchy'
remove_index :relations,
name: 'index_relations_on_type_columns'
end
truncate_closure_entries
end

@ -6,6 +6,11 @@ class RenamePlanningElemntTypeColorsToColors < ActiveRecord::Migration[5.1]
rename_index :planning_element_type_colors, :timelines_colors_pkey, :planning_element_type_colors_pkey
end
if ActiveRecord::Base.connection.execute("SELECT 1 as value FROM pg_class c WHERE c.relkind = 'S' and c.relname = 'planning_element_type_colors_id_seq'").to_a.present?
puts "Renaming id_seq to pkey which seems to be required by rename_table"
rename_index :planning_element_type_colors, :planning_element_type_colors_id_seq, :planning_element_type_colors_pkey
end
rename_table :planning_element_type_colors, :colors
remove_column :colors, :position
end

@ -5,6 +5,10 @@ set -o pipefail
APACHE_PIDFILE=/run/apache2/apache2.pid
if [ -n "$DATABASE_URL" ]; then
/usr/local/bin/migrate-mysql-to-postgres || exit 1
fi
# handle legacy configs
if [ -d "$PGDATA_LEGACY" ]; then
echo "WARN: You are using a legacy volume path for your postgres data. You should mount your postgres volumes at $PGDATA instead of $PGDATA_LEGACY."

@ -0,0 +1,22 @@
FROM ruby:2.6-stretch
MAINTAINER operations@openproject.com
ENV PGLOADER_DEPENDENCIES "libsqlite3-dev make curl gawk freetds-dev libzip-dev"
# Install
#
# 1) mysql and postgres clients
# 2) pgloader dependencies minus SBCL since we use CCL
RUN apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
mysql-client postgresql-client \
$PGLOADER_DEPENDENCIES && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# pgloader
ENV CCL_DEFAULT_DIRECTORY /opt/ccl
COPY docker/mysql-to-postgres/bin/build /tmp/build-pgloader
RUN /tmp/build-pgloader && rm /tmp/build-pgloader
COPY docker/mysql-to-postgres/bin/migrate-mysql-to-postgres /usr/local/bin/
CMD ["migrate-mysql-to-postgres"]

@ -0,0 +1,21 @@
#!/bin/bash
set -e
cd /opt
# install Clozure CL to avoid memory issues with the standard SBCL
wget -q https://github.com/Clozure/ccl/releases/download/v1.11.5/ccl-1.11.5-linuxx86.tar.gz
tar -xzf ccl-1.11.5-linuxx86.tar.gz
rm ccl-1.11.5-linuxx86.tar.gz
ln -s /opt/ccl/scripts/ccl64 /usr/local/bin/ccl
export CCL_DEFAULT_DIRECTORY=/opt/ccl
# build pgloader from source using CCL as the lisp runtime
git clone https://github.com/dimitri/pgloader.git
cd pgloader
git checkout v3.6.1
make CL=ccl pgloader >& /tmp/pgloader-compile.log || (cat /tmp/pgloader-compile.log && exit 1)
mv /opt/pgloader/build/bin/pgloader /usr/local/bin/pgloader-ccl
rm -rf /opt/pgloader

@ -0,0 +1,180 @@
#!/usr/bin/env ruby
require 'uri'
db_url = URI(ENV.fetch("DATABASE_URL"))
mysql_url = URI(ENV.fetch("MYSQL_DATABASE_URL")) if ENV.has_key?("MYSQL_DATABASE_URL")
if db_url.scheme.start_with?("mysql")
puts "WARNING: You are running MySQL. OpenProject will drop support for MySQL in 10.0."
puts " We can convert it to Postgres for you. Please setup postgres and "
puts " rerun this with MYSQL_DATABASE_URL pointing to your original database "
puts " and DATABASE_URL pointing to a new Postgres database."
puts ""
puts "EXAMPLE: docker run \\"
puts " -e MYSQL_DATABASE_URL=mysql://openproject:<PASSWORD>@localhost:3306/openproject \\"
puts " -e DATABASE_URL=postgresql://openproject:<PASSWORD>@localhost:5432/openproject \\"
puts " -it openproject/community:latest"
exit 0 # MySQL still supported until 9.x - with 10.0 we must make this an error (exit 1)
end
if db_url.scheme.start_with?("postgresql") && mysql_url.nil?
# nothing to do
exit 0
end
filtered_mysql_url = mysql_url.dup.tap{|url| url.password = "REDACTED"}
filtered_db_url = db_url.dup.tap{|url| url.password = "REDACTED"}
puts "Migrating database from MySQL to PostgreSQL."
puts
puts "Import"
puts " MySQL database"
puts " #{filtered_mysql_url.to_s}"
puts " into Postgres database "
puts " #{filtered_db_url.to_s}"
puts " ?"
puts "WARNING: This resets the given Postgres database."
puts
print "Y/n "
answer = ENV.fetch("FORCE_YES") { gets.chomp }
if answer.downcase == "n"
exit 0
end
db_user, db_password, db_host, db_port, db_name = db_url.user, db_url.password, db_url.host, db_url.port, db_url.path.sub("/", "")
db_port ||= 5432
if [db_host, db_name, db_port].any?{|value| [nil, ""].include?(value)}
puts "ERROR: Could not parse database URL (#{filtered_db_url})"
exit 1
end
require 'pty'
# Memcached is not yet running at this point so we use the file cache store.
# This will and is supposed to affect the spawned ruby processes.
ENV["RAILS_CACHE_STORE"] = "file_store"
def run(cmd, silent: false, record_output: true)
output = []
PTY.spawn(cmd) do |stdout, stdin, pid|
begin
stdout.each do |line|
output << line if record_output
puts line unless silent
end
rescue Errno::EIO
# raised when stdout is closed after process finished
ensure
stdout.close
stdin.close
end
Process.wait pid
end
[output.join("\n").strip, $?.to_i]
end
puts "Resetting target database..."
drop_cmd = "PGPASSWORD=#{db_password} psql -U #{db_user} -h #{db_host} -p #{db_port} -d postgres -c 'DROP DATABASE #{db_name}'"
drop_output, _ = run drop_cmd, silent: true
if drop_output.include?("database \"#{db_name}\" does not exist") || drop_output == "DROP DATABASE"
# that's ok then we don't have to drop it or dropped database successfully
puts "Database dropped"
else
puts drop_output
exit 1 # something went wrong
end
puts "Creating database..."
create_cmd = "PGPASSWORD=#{db_password} psql -U #{db_user} -h #{db_host} -p #{db_port} -d postgres -c 'CREATE DATABASE #{db_name}'"
create_output, _ = run create_cmd, silent: true
if create_output == "CREATE DATABASE"
# created database successfully
else
puts create_output
exit 1 # something went wrong
end
puts "Importing database ..."
mysql_url.query = nil
mysql_url.scheme = "mysql"
_, pgloader_status = run "pgloader-ccl --verbose #{mysql_url} #{db_url}", record_output: false
if pgloader_status != 0
puts "\nFailed to import MySQL database into Postgres. See above."
exit 1
end
check_fulltext_cmd = "PGPASSWORD=#{db_password} psql -o /dev/stdout -U #{db_user} -h #{db_host} -p #{db_port} -d #{db_name} -c \"select 'true' from schema_migrations where version = '20180122135443'\""
check_output, check_status = run check_fulltext_cmd, silent: true
if check_status != 0
puts check_output
puts "Failed to check full-text status of database. See above."
exit 1
end
# if the version was present on MySQL already we need to redo it to
# add the postgres specific columns.
needs_fulltext_migration = check_output.include?("true")
puts "Migrating database ..."
_, migrate_status = run "bundle exec rake db:migrate", record_output: false
if migrate_status != 0
puts "\nMigration failed. See above."
end
if needs_fulltext_migration
drop_version_cmd = "PGPASSWORD=#{db_password} psql -U #{db_user} -h #{db_host} -p #{db_port} -d #{db_name} -c \"delete from schema_migrations where version = '20180122135443'\""
drop_version_output, drop_version_status = run drop_version_cmd, silent: true
if drop_version_status != 0
puts drop_version_output
puts "Failed to drop schema_migrations entry. See above."
exit 1
end
puts "Running full-text search migration"
_, redo_status = run "bundle exec rake db:migrate:up VERSION=20180122135443", record_output: false
if redo_status != 0
puts "\nFull-text search migration failed. See above."
exit 1
end
_, extract_status = run "bundle exec rake attachments:extract_fulltext_where_missing", record_output: false
if extract_status != 0
puts "\nFull-text extraction failed. See above."
exit 1
end
end
puts "\nRe-building DAG to create Postgres-specific indices"
_, dag_status = run "bundle exec rake db:migrate:redo VERSION=20180105130053", record_output: false
if dag_status != 0
puts "\nRebuild DAG migration failed. See above."
exit 1
end
puts "Migration from MySQL to Postgres completed successfully!"

@ -51,16 +51,19 @@
</label>
<div class="advanced-filters--add-filter-value">
<select class="advanced-filters--select"
id="add_filter_select"
[ngModel]="filterToBeAdded"
(ngModelChange)="onFilterAdded($event)">
<option [textContent]="text.please_select" value="" disabled></option>
<option *ngFor="let filter of remainingFilters"
[textContent]="filter.name"
[ngValue]="filter">
</option>
</select>
<ng-select [items]="remainingFilters"
[clearSearchOnAdd]="true"
[multiple]="false"
[clearable]="true"
[clearOnBackspace]="true"
[closeOnSelect]="true"
[virtualScroll]="true"
[placeholder]="text.please_select"
(change)="onFilterAdded($event)"
id="add_filter_select"
bindLabel="name"
appendTo="body">
</ng-select>
</div>
<enterprise-banner class="advanced-filters--add-filter-info"

@ -28,7 +28,16 @@
import {WorkPackageTableFiltersService} from '../../wp-fast-table/state/wp-table-filters.service';
import {WorkPackageFiltersService} from "../../filters/wp-filters/wp-filters.service";
import {ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit, Output} from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource';
import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@ -36,6 +45,7 @@ import {QueryFilterResource} from 'core-app/modules/hal/resources/query-filter-r
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
import {BannersService} from "core-app/modules/common/enterprise/banners.service";
import {NgSelectComponent} from "@ng-select/ng-select";
const ADD_FILTER_SELECT_INDEX = -1;
@ -46,12 +56,12 @@ const ADD_FILTER_SELECT_INDEX = -1;
})
export class QueryFiltersComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild(NgSelectComponent) public ngSelectComponent:NgSelectComponent;
@Input() public filters:QueryFilterInstanceResource[];
@Input() public showCloseFilter:boolean = false;
@Output() public filtersChanged = new DebouncedEventEmitter<QueryFilterInstanceResource[]>(componentDestroyed(this));
public filterToBeAdded:QueryFilterResource|undefined;
public remainingFilters:any[] = [];
public eeShowBanners:boolean = false;
public focusElementIndex:number = 0;
@ -92,13 +102,13 @@ export class QueryFiltersComponent implements OnInit, OnChanges, OnDestroy {
if (filterToBeAdded) {
let newFilter = this.wpTableFilters.instantiate(filterToBeAdded);
this.filters.push(newFilter);
this.filterToBeAdded = undefined;
const index = this.currentFilterLength();
this.updateFilterFocus(index);
this.updateRemainingFilters();
this.filtersChanged.emit(this.filters);
this.ngSelectComponent.clearItem(filterToBeAdded);
}
}

@ -50,7 +50,7 @@ export class OPContextMenuService {
// Listen to any click and close the active context menu
jQuery(window).on('click', (evt:JQueryEventObject) => {
if (this.active && !this.portalHostElement.contains(evt.target as Element)) {
if (this.active && evt.button !== 2 && !this.portalHostElement.contains(evt.target as Element)) {
this.close();
}
});

@ -7,6 +7,14 @@ export namespace Highlighting {
return `__hl_inline_${property}_${id}`;
}
export function colorClass(highlightColorTextInline:boolean, id:string|number) {
if (highlightColorTextInline) {
return `__hl_inline_color_${id}_text`;
} else {
return `__hl_inline_color_${id}_dot`;
}
}
/**
* Given the difference from today (negative = n days in the past),
* output the fixed overdue classes

@ -28,73 +28,84 @@
import {Component, ElementRef, OnInit} from '@angular/core';
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
export const selector = 'colors-autocompleter';
import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions";
import {DynamicCssService} from "core-app/modules/common/dynamic-css/dynamic-css.service";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
@Component({
selector: selector,
template: '',
template: `
<ng-select [items]="options"
[virtualScroll]="true"
bindLabel="name"
bindValue="value"
[(ngModel)]="selectedOption"
(change)="onModelChange($event)"
[clearable]="false"
appendTo="body">
<ng-template ng-label-tmp let-item="item">
<span [ngClass]="highlightColor(item)">{{item.name}}</span>
</ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index">
<span [ngClass]="highlightColor(item)">{{item.name}}</span>
</ng-template>
</ng-select>
`,
selector: 'colors-autocompleter'
})
export class ColorsAutocompleter implements OnInit {
private $element:JQuery;
private $select:JQuery;
constructor(private readonly elementRef:ElementRef) {
public options:any[];
public selectedOption:any;
private highlightTextInline:boolean = false;
private updateInputField:HTMLInputElement|undefined;
private selectedColorId:string;
constructor(protected elementRef:ElementRef,
protected readonly I18n:I18nService,
protected readonly dynamicCssService:DynamicCssService) {
}
ngOnInit() {
this.$element = jQuery(this.elementRef.nativeElement);
this.$select = jQuery(this.$element.parent().find('select.colors-autocomplete'));
this.dynamicCssService.requireHighlighting();
this.setColorOptions();
this.$select.removeClass('form--select');
this.setupSelect2();
this.updateInputField = document.getElementsByName(this.elementRef.nativeElement.dataset.updateInput)[0] as HTMLInputElement|undefined;
this.highlightTextInline = JSON.parse(this.elementRef.nativeElement.dataset.highlightTextInline);
}
protected formatter(state:any) {
const item:JQuery = jQuery(state.element);
const color = item.data('color');
const contrastingColor = item.data('background');
const bright = item.data('bright');
// Special case, no color
if (!color) {
const div = jQuery('<div>')
.append(item.text())
.addClass('ui-menu-item-wrapper');
return div;
public onModelChange(color:any) {
if (color && this.updateInputField) {
this.updateInputField.value = color.value;
}
}
const colorSquare = jQuery('<span>')
.addClass('color--preview')
.css('background-color', color);
const colorText = jQuery('<span>')
.addClass('color--text-preview')
.css('color', bright ? '#333333' : '#FFFFFF')
.css('background-color', color)
.text(item.text());
private setColorOptions() {
this.options = JSON.parse(this.elementRef.nativeElement.dataset.colors);
this.options.unshift({name: this.I18n.t('js.label_no_color'), value: ''});
const div = jQuery('<div>')
.append(colorSquare)
.append(colorText)
.addClass('ui-menu-item-wrapper');
this.selectedOption = this.options.find((item) => item.selected === true);
return div;
if (this.selectedOption) {
this.selectedOption = this.selectedOption.value;
} else {
// Differentiate between "No color" and a color that is now not selectable any more
this.selectedColorId = this.elementRef.nativeElement.dataset.selectedColor;
this.selectedOption = this.selectedColorId ? this.selectedColorId : '';
}
}
protected setupSelect2() {
this.$select.select2({
formatResult: this.formatter,
formatSelection: this.formatter,
escapeMarkup: (m:any) => m
});
private highlightColor(item:any) {
if (item.value === '') { return; }
let highlightingClass;
if (this.highlightTextInline) {
highlightingClass = '__hl_inline_type_ ';
} else {
highlightingClass = '__hl_inline_ ';
}
return highlightingClass + Highlighting.colorClass(this.highlightTextInline, item.value);
}
}
DynamicBootstrapper.register({
selector: selector,
cls: ColorsAutocompleter
});
DynamicBootstrapper.register({ selector: 'colors-autocompleter', cls: ColorsAutocompleter });

@ -87,7 +87,7 @@ module Redmine::Acts::Journalized
# Returns all journal records created before the journal associated with the given value.
def before(value)
return [] if (version = journal_at(value)).nil?
where("#{Journal.table_name}.version < #{version}")
where("#{Journal.table_name}.version < ?", version)
end
# Returns all journal records created after the journal associated with the given value.
@ -95,7 +95,7 @@ module Redmine::Acts::Journalized
# This is useful for dissociating records during use of the +reset_to!+ method.
def after(value)
return [] if (version = journal_at(value)).nil?
where("#{Journal.table_name}.version > #{version}")
where("#{Journal.table_name}.version > ?", version)
end
# Returns a single journal associated with the given value. The following formats are valid:

@ -118,7 +118,7 @@ module Redmine
sql << ' OR ' + tsv_clauses.join(' OR ')
end
find_conditions = [sql, * (tokens.map { |w| "%#{w.downcase}%" } * token_clauses.size).sort]
find_conditions = [sql, *(tokens.map { |w| "%#{w.downcase}%" } * token_clauses.size).sort]
project_conditions = [searchable_projects_condition]
@ -152,7 +152,7 @@ module Redmine
Project.allowed_to(User.current, searchable_options[:permission])
end
"#{searchable_options[:project_key]} IN (#{projects .select(:id).to_sql})"
"#{searchable_options[:project_key]} IN (#{projects.select(:id).to_sql})"
end
end
end

@ -40,7 +40,7 @@ module OnboardingSteps
next_button.click
expect(page)
.to have_text 'Drag & Drop your cards within a list to re-order, or the another list. A double click will open the details view.'
.to have_text 'Drag & Drop your cards within a list to re-order, or to drag to another list. A double click will open the details view.'
end
end

@ -214,10 +214,11 @@ describe 'filter work packages', js: true do
end
it 'allows filtering, saving and retrieving the saved filter' do
filters.open
expect(page).to have_selector('#add_filter_select option', text: list_cf.name, wait: 10)
# Wait for form to load
filters.expect_loaded
filters.open
filters.add_filter_by(list_cf.name,
'is not',
list_cf.custom_options.last.value,

@ -33,7 +33,7 @@ module Components
text = select_text.presence || query
# click the element to select it
target_dropdown.find('.ng-option', text: text).click
target_dropdown.find('.ng-option', text: text, match: :first).click
end
end
end

@ -74,8 +74,15 @@ module Components
expect(page).to have_conditional_selector(present, '.advanced-filters--add-filter-value option', text: name)
end
def expect_loaded
expect(filter_button).to have_selector('.badge', wait: 20)
end
def add_filter_by(name, operator, value, selector = nil)
select name, from: "add_filter_select"
select_autocomplete page.find('.advanced-filters--add-filter-value'),
query: name,
results_selector: '.ng-dropdown-panel-items'
set_filter(name, operator, value, selector)
end

Loading…
Cancel
Save