Merge remote-tracking branch 'origin/release/12.1' into dev

pull/10734/head
Oliver Günther 3 years ago
commit ebcf660043
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 2
      .github/workflows/pullpreview.yml
  2. 6
      app/helpers/application_helper.rb
  3. 18
      app/helpers/search_helper.rb
  4. 25
      app/helpers/text_formatting_helper.rb
  5. 2
      app/views/activities/index.html.erb
  6. 2
      app/views/users/show.html.erb
  7. 52
      docs/system-admin-guide/authentication/saml/README.md
  8. 24
      frontend/src/app/features/calendar/te-calendar/te-calendar.component.sass
  9. 10
      lib/open_project/text_formatting/truncation.rb
  10. 2
      modules/documents/app/views/documents/_document.html.erb
  11. 30
      modules/documents/spec/controllers/documents_controller_spec.rb
  12. 19
      spec/helpers/application_helper_spec.rb
  13. 16
      spec/helpers/search_helper_spec.rb
  14. 29
      spec/helpers/text_formatting_helper_spec.rb

@ -32,7 +32,7 @@ jobs:
admins: crohr,HDinger,machisuji,oliverguenther,ulferts,wielinde,b12f,cbliard
always_on: dev
compose_files: docker-compose.pullpreview.yml
instance_type: medium_2_0
instance_type: large_2_0
ports: 80,443
default_port: 443
env:

@ -109,12 +109,6 @@ module ApplicationHelper
date == User.current.today ? I18n.t(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
html_escape_once(truncate(text.to_s, length: 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...'))
.gsub(/[\r\n]+/, '<br />')
.html_safe
end
def due_date_distance_in_words(date)
if date
label = date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in

@ -146,6 +146,22 @@ module SearchHelper
end
def abbreviated_text(words)
h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
formatted_words = truncate_formatted_text(words, length: nil)
abbreviated_words = if formatted_words.length > 100
"#{formatted_words.slice(0..44)} ... #{formatted_words.slice(-44..-1)}"
else
formatted_words
end
if words[0] == ' '
abbreviated_words = " #{abbreviated_words}"
end
if words[-1] == ' ' && words.length > 1
abbreviated_words = "#{abbreviated_words} "
end
abbreviated_words
end
end

@ -27,7 +27,9 @@
#++
module TextFormattingHelper
include OpenProject::TextFormatting
extend Forwardable
def_delegators :current_formatting_helper,
:wikitoolbar_for
@ -74,4 +76,27 @@ module TextFormattingHelper
project_preview_context(object, project)
end
end
def truncate_formatted_text(text, length: 120)
# rubocop:disable Rails/OutputSafety
stripped_text = strip_tags(format_text(text.to_s)).html_safe
if length
truncate_multiline(stripped_text)
else
stripped_text
end
.strip
.gsub(/[\r\n]+/, '<br />')
.html_safe
# rubocop:enable Rails/OutputSafety
end
def truncate_multiline(string)
if string.to_s =~ /\A(.{120}).*?$/m
"#{$1}..."
else
string
end
end
end

@ -50,7 +50,7 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<%= link_to format_activity_title(e.event_title), e.event_path%>
</div>
<div class="description"><%= format_activity_description(e.event_description) %></div>
<div class="description"><%= truncate_formatted_text(e.event_description) %></div>
<div class="author">
<%= avatar(e.event_author, {class: 'avatar-mini'}) if e.respond_to?(:event_author) %>
<%= link_to_user(e.event_author) if e.respond_to?(:event_author) %>

@ -95,7 +95,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= link_to format_activity_title(e.event_title), e.event_url %>
</div>
<div class="description">
<%= format_activity_description(e.event_description) %>
<%= truncate_formatted_text(e.event_description) %>
</div>
</li>
<% end -%>

@ -172,11 +172,35 @@ Setting.plugin_openproject_auth_saml = Hash(Setting.plugin_openproject_auth_saml
"saml" => {
"name" => "saml",
"display_name" => "My SSO",
"assertion_consumer_service_url" => "https://<YOUR OPENPROJECT HOSTNAME>/auth/saml/callback"
"assertion_consumer_service_url" => "https://<YOUR OPENPROJECT HOSTNAME>/auth/saml/callback",
# The SAML issuer string that OpenProject will call your idP with
"issuer" => "https://<YOUR OPENPROJECT HOSTNAME>",
### one liner to generate certificate in ONE line
### awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' <yourcert.pem>
"idp_cert" => "-----BEGIN CERTIFICATE-----\nMI................IEr\n-----END CERTIFICATE-----\n",
# etc.
# Otherwise, the certificate fingerprint must be added
# Either `idp_cert` or `idp_cert_fingerprint` must be present!
"idp_cert_fingerprint" => "E7:91:B2:E1:...",
# Replace with your SAML 2.0 redirect flow single sign on URL
# For example: "https://sso.example.com/saml/singleSignOn"
"idp_sso_target_url" => "<YOUR SSO URL>",
# Replace with your redirect flow single sign out URL
# or comment out
# For example: "https://sso.example.com/saml/proxySingleLogout"
"idp_slo_target_url" => "<YOUR SSO logout URL>",
# Attribute map in SAML
"attribute_statements" => {
# What attribute in SAML maps to email (default: mail)
"email" => ['mail'],
# What attribute in SAML maps to the user login (default: uid)
"login" => ['uid'],
# What attribute in SAML maps to the first name (default: givenName)
"first_name" => ['givenName'],
# What attribute in SAML maps to the last name (default: sn)
"last_name" => ['sn']
}
}
}
})
@ -223,13 +247,18 @@ The OpenProject username is taken by default from the `email` attribute if no ex
Setting.plugin_openproject_auth_saml = Hash(Setting.plugin_openproject_auth_saml).deep_merge({
"providers" => {
"saml" => {
"email" => "email",
"login" => "username",
"first_name" => "firstname",
"last_name" => "lastname"
# another example for combined attributes in an array:
"login" => ['username', 'samAccountName', 'uid'],
# etc.
# ... other attributes, see above.
# Attribute map in SAML
"attribute_statements" => {
# What attribute in SAML maps to email (default: mail)
"email" => ['mail'],
# another example for combined attributes in an array:
"login" => ['username', 'samAccountName', 'uid'],
# What attribute in SAML maps to the first name (default: givenName)
"first_name" => ['givenName'],
# What attribute in SAML maps to the last name (default: sn)
"last_name" => ['sn']
}
}
}
})
@ -287,8 +316,11 @@ To enable request signing, enable the following flag:
certificate: "-----BEGIN CERTIFICATE-----\n .... certificate contents ....\n-----END CERTIFICATE-----",
private_key: "-----BEGIN PRIVATE KEY-----\n .... private key contents ....\n-----END PRIVATE KEY-----",
security: {
# Whether SP and idP should sign requests and assertions
authn_requests_signed: true,
want_assertions_signed: true,
# Whether the idP should encrypt assertions
want_assertions_signed: false,
embed_sign: true,
signature_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
digest_method: 'http://www.w3.org/2001/04/xmlenc#sha256',
@ -301,7 +333,7 @@ With request signing enabled, the certificate will be added to the identity prov
### 3: Restarting the server
Once the configuration is completed, restart your OpenProject server with `service openproject restart`.
Once the configuration is completed, restart your OpenProject server with `service openproject restart`. If you configured SAML through settings, this step can be ignored.
#### XML Metadata exchange

@ -1,4 +1,22 @@
te-calendar
// The two media queries below make sure the "Log time" button does not overlap
// with the dates on mobile. Instead, it will be shown above the full calendar toolbar.
// The media queries are the normal mobile/tablet breakpoints.
@media screen and (max-width: 679px)
.fc-toolbar-chunk:empty
display: none
@media screen and (min-width: 680px)
.te-calendar--create-button
position: absolute
right: 0
top: 0
margin-right: 0px
.fc-toolbar-chunk:last-child
flex-basis: 165px
flex-shrink: 0
full-calendar
overflow-x: auto
@ -119,9 +137,3 @@ te-calendar
margin-right: 5px
padding-right: 5px
font-weight: bold
.te-calendar--create-button
position: absolute
right: 0
top: 6px
margin-right: 0px

@ -36,16 +36,6 @@ module OpenProject
def truncate_single_line(string, *args)
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ').html_safe
end
# Truncates at line break after 250 characters or options[:length]
def truncate_lines(string, options = {})
length = options[:length] || 250
if string.to_s =~ /\A(.{#{length}}.*?)$/m
"#{$1}..."
else
string
end
end
end
end
end

@ -31,5 +31,5 @@ See COPYRIGHT and LICENSE files for more details.
<p class="document-category-elements--date"><em><%= format_time(document.updated_at) %></em></p>
<div class="wiki op-uc-container">
<%= format_text(truncate_lines(document.description)) %>
<%= format_text(document.description) %>
</div>

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.dirname(__FILE__) + '/../spec_helper'
require "#{File.dirname(__FILE__)}/../spec_helper"
describe DocumentsController do
render_views
@ -40,32 +40,14 @@ describe DocumentsController do
create(:document_category, project: project, name: "Default Category")
end
let(:document) do
let!(:document) do
create(:document, title: "Sample Document", project: project, category: default_category)
end
current_user { admin }
describe "index" do
let(:long_description) do
<<-LOREM.strip_heredoc
Lorem ipsum dolor sit amet, consectetur adipiscing elit.\
Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci,\
eget tristique ante sem vel mi. Nulla facilisi.\
Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna.\
Duis ultrices molestie ante, eget scelerisque sem iaculis vitae.\
Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium.\
Proin sollicitudin elementum quam quis pharetra.\
Aenean facilisis nunc quis elit volutpat mollis.\
Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio.\
Integer et lectus neque, nec scelerisque nisi. EndOfLineHere
Praesent a nunc lorem, ac porttitor eros.
LOREM
end
before do
document.update(description: long_description)
get :index, params: { project_id: project.identifier }
end
@ -78,12 +60,6 @@ describe DocumentsController do
expect(assigns(:grouped)).not_to be_nil
expect(assigns(:grouped).keys.map(&:name)).to eql [default_category.name]
end
it "renders documents with long descriptions properly" do
expect(response.body).to have_selector('.wiki p', visible: :all)
expect(response.body).to have_selector('.wiki p', visible: :all, text: (document.description.split("\n").first + '...'))
expect(response.body).to have_selector('.wiki p', visible: :all, text: /EndOfLineHere.../)
end
end
describe 'new' do
@ -150,7 +126,7 @@ describe DocumentsController do
it "adds an attachment" do
document = Document.last
expect(document.attachments.count).to eql 1
expect(document.attachments.count).to be 1
attachment = document.attachments.first
expect(uncontainered.reload).to eql attachment
end

@ -32,25 +32,6 @@ describe ApplicationHelper, type: :helper do
include ApplicationHelper
include WorkPackagesHelper
describe 'format_activity_description' do
it 'truncates given text' do
text = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore'
expect(format_activity_description(text).size).to eq(120)
end
it 'replaces escaped line breaks with html line breaks and should be html_safe' do
text = "Lorem ipsum dolor sit \namet, consetetur sadipscing elitr, sed diam nonumy eirmod\r tempor invidunt"
text_html = 'Lorem ipsum dolor sit <br />amet, consetetur sadipscing elitr, sed diam nonumy eirmod<br /> tempor invidunt'
expect(format_activity_description(text)).to be_html_eql(text_html)
expect(format_activity_description(text).html_safe?).to be_truthy
end
it 'escapes potentially harmful code' do
text = "Lorem ipsum dolor <script>alert('pwnd');</script> tempor invidunt"
expect(format_activity_description(text).include?('&lt;script&gt;alert(&#39;pwnd&#39;);&lt;/script&gt;')).to be_truthy
end
end
describe 'footer_content' do
context 'no additional footer content' do
before do

@ -44,7 +44,6 @@ describe 'search/index', type: :helper do
describe '#highlight_tokens' do
let(:maximum_length) { 1300 }
subject { helper.highlight_tokens title, tokens }
subject(:highlighted_title) { helper.highlight_tokens title, tokens }
context 'with single token' do
@ -59,7 +58,11 @@ describe 'search/index', type: :helper do
let(:tokens) { %w(token another) }
let(:title) { 'This is a token and another token.' }
let(:expected_title) do
'This is a <span class="search-highlight token-0">token</span> and <span class="search-highlight token-1">another</span> <span class="search-highlight token-0">token</span>.'
<<~TITLE.squish
This is a <span class="search-highlight token-0">token</span>
and <span class="search-highlight token-1">another</span>
<span class="search-highlight token-0">token</span>.
TITLE
end
it { is_expected.to eq expected_title }
@ -67,7 +70,7 @@ describe 'search/index', type: :helper do
context 'with huge content' do
let(:tokens) { %w(token) }
let(:title) { (('1234567890' * 100) + ' token ') * 100 }
let(:title) { "#{'1234567890' * 100} token " * 100 }
let(:highlighted_token) { '<span class="search-highlight token-0">token</span>' }
it { expect(highlighted_title).to include highlighted_token }
@ -79,9 +82,9 @@ describe 'search/index', type: :helper do
context 'with multibyte title' do
let(:tokens) { %w(token) }
let(:title) { ('й' * 200) + ' token ' + ('й' * 200) }
let(:title) { "#{'й' * 200} token #{'й' * 200}" }
let(:expected_title) do
('й' * 45) + ' ... ' + ('й' * 44) + ' <span class="search-highlight token-0">token</span> ' + ('й' * 44) + ' ... ' + ('й' * 45)
"#{'й' * 45} ... #{'й' * 44} <span class=\"search-highlight token-0\">token</span> #{'й' * 45} ... #{'й' * 44}"
end
it { is_expected.to eq expected_title }
@ -95,7 +98,7 @@ describe 'search/index', type: :helper do
let(:attachment_filename) { "attachment_filename.txt" }
let(:journal) { build_stubbed(:work_package_journal, notes: journal_notes) }
let(:event) do
instance_double('WorkPackage',
instance_double(WorkPackage,
last_journal: journal,
last_loaded_journal: journal,
event_description: event_description,
@ -125,7 +128,6 @@ describe 'search/index', type: :helper do
it 'shows the text in the notes' do
expect(helper.highlight_tokens_in_event(event, tokens))
.to eql '<span class="search-highlight token-0">Journals</span> notes'
end
end

@ -57,4 +57,33 @@ describe TextFormattingHelper, type: :helper do
end
end
end
describe 'truncate_formatted_text' do
it 'truncates given text' do
text = <<~TEXT.squish
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua. At vero eos et accusam et justo duo dolores
et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
ipsum dolor sit amet. Lore
TEXT
expect(truncate_formatted_text(text).size).to eq(123)
end
it 'replaces escaped line breaks with html line breaks and should be html_safe' do
text = "Lorem ipsum dolor sit \namet, consetetur sadipscing elitr, sed diam nonumy eirmod\n tempor invidunt"
text_html = 'Lorem ipsum dolor sit <br /> amet, consetetur sadipscing elitr, sed diam nonumy eirmod <br /> tempor invidunt'
expect(truncate_formatted_text(text))
.to be_html_eql(text_html)
expect(truncate_formatted_text(text))
.to be_html_safe
end
it 'escapes potentially harmful code' do
text = "Lorem ipsum dolor <script>alert('pwnd');</script> tempor invidunt"
expect(truncate_formatted_text(text))
.to include('&lt;script&gt;alert(\'pwnd\');&lt;/script&gt;')
end
end
end

Loading…
Cancel
Save