Redmine 4.1.1

This commit is contained in:
Manuel Cillero 2020-11-22 21:20:06 +01:00
parent 33e7b881a5
commit 3d976f1b3b
1593 changed files with 36180 additions and 19489 deletions

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -54,7 +56,9 @@ module Redmine
end
def position_scope_was
build_position_scope {|c| send("#{c}_was")}
# this can be called in after_update or after_destroy callbacks
# with different methods in Rails 5 for retrieving the previous value
build_position_scope {|c| send(destroyed? ? "#{c}_was" : "#{c}_before_last_save")}
end
def build_position_scope
@ -75,8 +79,8 @@ module Redmine
if !new_record? && position_scope_changed?
remove_position
insert_position
elsif position_changed?
if position_was.nil?
elsif saved_change_to_position?
if position_before_last_save.nil?
insert_position
else
shift_positions
@ -89,16 +93,19 @@ module Redmine
end
def remove_position
position_scope_was.where("position >= ? AND id <> ?", position_was, id).update_all("position = position - 1")
# this can be called in after_update or after_destroy callbacks
# with different methods in Rails 5 for retrieving the previous value
previous = destroyed? ? position_was : position_before_last_save
position_scope_was.where("position >= ? AND id <> ?", previous, id).update_all("position = position - 1")
end
def position_scope_changed?
(changed & self.class.positioned_options[:scope].map(&:to_s)).any?
(saved_changes.keys & self.class.positioned_options[:scope].map(&:to_s)).any?
end
def shift_positions
offset = position_was <=> position
min, max = [position, position_was].sort
offset = position_before_last_save <=> position
min, max = [position, position_before_last_save].sort
r = position_scope.where("id <> ? AND position BETWEEN ? AND ?", id, min, max).update_all("position = position + #{offset}")
if r != max - min
reset_positions_in_list

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,9 +1,11 @@
# frozen_string_literal: true
module Redmine
module CodesetUtil
def self.replace_invalid_utf8(str)
return str if str.nil?
return nil if str.nil?
str = str.dup
str.force_encoding('UTF-8')
if ! str.valid_encoding?
str = str.encode("UTF-16LE", :invalid => :replace,
@ -13,14 +15,14 @@ module Redmine
end
def self.to_utf8(str, encoding)
return str if str.nil?
str.force_encoding("ASCII-8BIT")
return if str.nil?
str = str.b
if str.empty?
str.force_encoding("UTF-8")
return str
end
enc = encoding.blank? ? "UTF-8" : encoding
if enc.upcase != "UTF-8"
if enc.casecmp("UTF-8") != 0
str.force_encoding(enc)
str = str.encode("UTF-8", :invalid => :replace,
:undef => :replace, :replace => '?')
@ -31,15 +33,16 @@ module Redmine
end
def self.to_utf8_by_setting(str)
return str if str.nil?
return if str.nil?
str = str.dup
self.to_utf8_by_setting_internal(str).force_encoding('UTF-8')
end
def self.to_utf8_by_setting_internal(str)
return str if str.nil?
str.force_encoding('ASCII-8BIT')
return if str.nil?
str = str.b
return str if str.empty?
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match?(str) # for us-ascii
str.force_encoding('UTF-8')
encodings = Setting.repositories_encodings.split(',').collect(&:strip)
encodings.each do |encoding|
@ -55,9 +58,11 @@ module Redmine
end
def self.from_utf8(str, encoding)
return if str.nil?
str = str.dup
str ||= ''
str.force_encoding('UTF-8')
if encoding.upcase != 'UTF-8'
if encoding.casecmp('UTF-8') != 0
str = str.encode(encoding, :invalid => :replace,
:undef => :replace, :replace => '?')
else

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -20,6 +22,7 @@ module Redmine
# Configuration default values
@defaults = {
'avatar_server_url' => 'https://www.gravatar.com',
'email_delivery' => nil,
'max_concurrent_ajax_uploads' => 2
}
@ -53,6 +56,12 @@ module Redmine
if @config['email_delivery']
ActionMailer::Base.perform_deliveries = true
@config['email_delivery'].each do |k, v|
# Comprehensive error message for those who used async_smtp and async_sendmail
# delivery methods that are removed in Redmine 4.0.
if k == 'delivery_method' && v.to_s =~ /\Aasync_(.+)/
abort "Redmine now uses ActiveJob to send emails asynchronously and the :#{v} delivery method is no longer available.\n" +
"Please update your config/configuration.yml to use :#$1 delivery method instead."
end
v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
ActionMailer::Base.send("#{k}=", v)
end
@ -114,7 +123,7 @@ module Redmine
# Checks the validness of regular expressions set for repository paths at startup
def check_regular_expressions
@config.each do |name, value|
if value.present? && name =~ /^scm_.+_path_regexp$/
if value.present? && /^scm_.+_path_regexp$/.match?(name)
begin
Regexp.new value.to_s.strip
rescue => e

View file

@ -1 +1,3 @@
# frozen_string_literal: true
Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,7 +21,7 @@ class DateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
before_type_cast = record.attributes_before_type_cast[attribute.to_s]
if before_type_cast.is_a?(String) && before_type_cast.present?
unless before_type_cast =~ /\A\d{4}-\d{2}-\d{2}( 00:00:00)?\z/ && value
unless /\A\d{4}-\d{2}-\d{2}( 00:00:00)?\z/.match?(before_type_cast) && value
record.errors.add attribute, :not_a_date
end
end

View file

@ -1,5 +0,0 @@
require File.dirname(__FILE__) + '/date/calculations'
class Date #:nodoc:
include Redmine::CoreExtensions::Date::Calculations
end

View file

@ -1,35 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine #:nodoc:
module CoreExtensions #:nodoc:
module Date #:nodoc:
# Custom date calculations
module Calculations
# Returns difference with specified date in months
def months_ago(date = self.class.today)
(date.year - self.year)*12 + (date.month - self.month)
end
# Returns difference with specified date in weeks
def weeks_ago(date = self.class.today)
(date.year - self.year)*52 + (date.cweek - self.cweek)
end
end
end
end
end

View file

@ -1,7 +1,10 @@
# frozen_string_literal: true
require File.dirname(__FILE__) + '/string/conversions'
require File.dirname(__FILE__) + '/string/inflections'
class String #:nodoc:
# @private
class String
include Redmine::CoreExtensions::String::Conversions
include Redmine::CoreExtensions::String::Inflections
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -15,10 +17,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
module Redmine
# @private
module CoreExtensions
# @private
module String
# Custom string conversions
# @private
module Conversions
# Parses hours format and returns a float
def to_hours

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -15,10 +17,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
module Redmine
# @private
module CoreExtensions
# @private
module String
# Custom string inflections
# @private
module Inflections
def with_leading_slash
starts_with?('/') ? self : "/#{ self }"

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,7 +23,7 @@ module Redmine
class << self
# Returns true if the database is PostgreSQL
def postgresql?
(ActiveRecord::Base.connection.adapter_name =~ /postgresql/i).present?
/postgresql/i.match?(ActiveRecord::Base.connection.adapter_name)
end
# Returns the PostgreSQL version or nil if another DBMS is used
@ -46,7 +48,7 @@ module Redmine
# Returns true if the database is MySQL
def mysql?
(ActiveRecord::Base.connection.adapter_name =~ /mysql/i).present?
/mysql/i.match?(ActiveRecord::Base.connection.adapter_name)
end
# Returns a SQL statement for case/accent (if possible) insensitive match
@ -64,6 +66,27 @@ module Redmine
end
end
# Returns a SQL statement to cast a timestamp column to a date given a time zone
# Returns nil if not implemented for the current database
def timestamp_to_date(column, time_zone)
if postgresql?
if time_zone
identifier = ActiveSupport::TimeZone.find_tzinfo(time_zone.name).identifier
"(#{column}::timestamptz AT TIME ZONE '#{identifier}')::date"
else
"#{column}::date"
end
elsif mysql?
if time_zone
user_identifier = ActiveSupport::TimeZone.find_tzinfo(time_zone.name).identifier
local_identifier = ActiveSupport::TimeZone.find_tzinfo(Time.zone.name).identifier
"DATE(CONVERT_TZ(#{column},'#{local_identifier}', '#{user_identifier}'))"
else
"DATE(#{column})"
end
end
end
# Resets database information
def reset
@postgresql_unaccent = nil

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -17,7 +19,7 @@
module Redmine
module DefaultData
class DataAlreadyLoaded < Exception; end
class DataAlreadyLoaded < StandardError; end
module Loader
include Redmine::I18n
@ -48,9 +50,10 @@ module Redmine
manager.permissions = manager.setable_permissions.collect {|p| p.name}
manager.save!
developer = Role.create! :name => l(:default_role_developer),
:position => 2,
:permissions => [:manage_versions,
developer = Role.create!(
:name => l(:default_role_developer),
:position => 2,
:permissions => [:manage_versions,
:manage_categories,
:view_issues,
:add_issues,
@ -80,11 +83,11 @@ module Redmine
:browse_repository,
:view_changesets,
:commit_access,
:manage_related_issues]
reporter = Role.create! :name => l(:default_role_reporter),
:position => 3,
:permissions => [:view_issues,
:manage_related_issues])
reporter = Role.create!(
:name => l(:default_role_reporter),
:position => 3,
:permissions => [:view_issues,
:add_issues,
:add_issue_notes,
:save_queries,
@ -102,7 +105,7 @@ module Redmine
:edit_own_messages,
:view_files,
:browse_repository,
:view_changesets]
:view_changesets])
Role.non_member.update_attribute :permissions, [:view_issues,
:add_issues,

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -31,15 +31,16 @@ module Redmine
class << self
def generate(&block)
def generate(options = {}, &block)
col_sep = l(:general_csv_separator)
encoding = l(:general_csv_encoding)
encoding = Encoding.find(options[:encoding]) rescue Encoding.find(l(:general_csv_encoding))
str = ''.force_encoding(encoding)
if encoding == 'UTF-8'
# BOM
str = "\xEF\xBB\xBF".force_encoding(encoding)
end
str =
if encoding == Encoding::UTF_8
+"\xEF\xBB\xBF" # BOM
else
(+'').force_encoding(encoding)
end
super(str, :col_sep => col_sep, :encoding => encoding, &block)
end

View file

@ -1,7 +1,8 @@
# encoding: utf-8
# frozen_string_literal: true
#
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -51,6 +52,7 @@ module Redmine
end
def SetFont(family, style='', size=0, fontfile='')
style = +style
# FreeSerif Bold Thai font has problem.
style.delete!('B') if family.to_s.casecmp('freeserif') == 0
# DejaVuSans Italic Arabic and Persian font has problem.
@ -69,7 +71,7 @@ module Redmine
Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
end
def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
def RDMCell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link='')
cell(w, h, txt, border, ln, align, fill, link)
end
@ -91,7 +93,7 @@ module Redmine
</style>'
# Strip {{toc}} tags
txt.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
txt = txt.gsub(/<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i, '')
writeHTMLCell(w, h, x, y, css_tag + txt, border, ln, fill)
end
@ -133,15 +135,13 @@ module Redmine
class RDMPdfEncoding
def self.rdm_from_utf8(txt, encoding)
txt ||= ''
txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
txt.force_encoding('ASCII-8BIT')
txt
Redmine::CodesetUtil.from_utf8(txt, encoding).b
end
def self.attach(attachments, filename, encoding)
filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
atta = nil
if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
if /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i.match?(filename_utf8)
atta = Attachment.latest_attach(attachments, filename_utf8)
end
if atta && atta.readable? && atta.visible?

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -45,35 +45,31 @@ module Redmine
pdf.SetFontStyle('',8)
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
pdf.ln
left = []
left << [l(:field_status), issue.status]
left << [l(:field_priority), issue.priority]
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
right = []
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
rows = left.size > right.size ? left.size : right.size
while left.size < rows
left << nil
end
while right.size < rows
right << nil
end
left << nil while left.size < rows
right << nil while right.size < rows
custom_field_values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
half = (custom_field_values.size / 2.0).ceil
custom_field_values.each_with_index do |custom_value, i|
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
end
if pdf.get_rtl
border_first_top = 'RT'
border_last_top = 'LT'
@ -85,7 +81,7 @@ module Redmine
border_first = 'L'
border_last = 'R'
end
rows = left.size > right.size ? left.size : right.size
rows.times do |i|
heights = []
@ -100,34 +96,36 @@ module Redmine
item = right[i]
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
height = heights.max
item = left[i]
pdf.SetFontStyle('B',9)
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
pdf.SetFontStyle('',9)
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
item = right[i]
pdf.SetFontStyle('B',9)
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
pdf.SetFontStyle('',9)
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
pdf.set_x(base_x)
end
pdf.SetFontStyle('B',9)
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
pdf.SetFontStyle('',9)
# Set resize image scale
pdf.set_image_scale(1.6)
text = textilizable(issue, :description,
:only_path => false,
:edit_section_links => false,
:headings => false,
:inline_attachments => false
)
text =
textilizable(
issue, :description,
:only_path => false,
:edit_section_links => false,
:headings => false,
:inline_attachments => false
)
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
custom_field_values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
@ -157,7 +155,7 @@ module Redmine
pdf.ln
end
end
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
unless relations.empty?
truncate_length = (!is_cjk? ? 80 : 60)
@ -185,7 +183,7 @@ module Redmine
end
pdf.RDMCell(190,5, "", "T")
pdf.ln
if issue.changesets.any? &&
User.current.allowed_to?(:view_changesets, issue.project)
pdf.SetFontStyle('B',9)
@ -199,13 +197,14 @@ module Redmine
pdf.ln
unless changeset.comments.blank?
pdf.SetFontStyle('',8)
pdf.RDMwriteHTMLCell(190,5,'','',
pdf.RDMwriteHTMLCell(
190,5,'','',
changeset.comments.to_s, issue.attachments, "")
end
pdf.ln
end
end
if assoc[:journals].present?
pdf.SetFontStyle('B',9)
pdf.RDMCell(190,5, l(:label_history), "B")
@ -213,7 +212,7 @@ module Redmine
assoc[:journals].each do |journal|
pdf.SetFontStyle('B',8)
title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
title << " (#{l(:field_private_notes)})" if journal.private_notes?
title += " (#{l(:field_private_notes)})" if journal.private_notes?
pdf.RDMCell(190,5, title)
pdf.ln
pdf.SetFontStyle('I',8)
@ -223,18 +222,20 @@ module Redmine
if journal.notes?
pdf.ln unless journal.details.empty?
pdf.SetFontStyle('',8)
text = textilizable(journal, :notes,
:only_path => false,
:edit_section_links => false,
:headings => false,
:inline_attachments => false
)
text =
textilizable(
journal, :notes,
:only_path => false,
:edit_section_links => false,
:headings => false,
:inline_attachments => false
)
pdf.RDMwriteFormattedCell(190,5,'','', text, issue.attachments, "")
end
pdf.ln
end
end
if issue.attachments.any?
pdf.SetFontStyle('B',9)
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
@ -261,7 +262,7 @@ module Redmine
pdf.footer_date = format_date(User.current.today)
pdf.set_auto_page_break(false)
pdf.add_page("L")
# Landscape A4 = 210 x 297 mm
page_height = pdf.get_page_height # 210
page_width = pdf.get_page_width # 297
@ -269,7 +270,7 @@ module Redmine
right_margin = pdf.get_original_margins['right'] # 10
bottom_margin = pdf.get_footer_margin
row_height = 4
# column widths
table_width = page_width - right_margin - left_margin
col_width = []
@ -277,13 +278,13 @@ module Redmine
col_width = calc_col_width(issues, query, table_width, pdf)
table_width = col_width.inject(0, :+)
end
# use full width if the description or last_notes are displayed
if table_width > 0 && (query.has_column?(:description) || query.has_column?(:last_notes))
# use full width if the query has block columns (description, last_notes or full width custom fieds)
if table_width > 0 && query.block_columns.any?
col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
table_width = col_width.inject(0, :+)
end
# title
pdf.SetFontStyle('B',11)
pdf.RDMCell(190, 8, title)
@ -303,9 +304,9 @@ module Redmine
issue_list(issues) do |issue, level|
if query.grouped? &&
(group = query.group_by_column.value(issue)) != previous_group
(group = query.group_by_column.group_value(issue)) != previous_group
pdf.SetFontStyle('B',10)
group_label = group.blank? ? 'None' : group.to_s.dup
group_label = group.blank? ? +'None' : group.to_s.dup
group_label << " (#{result_count_by_group[group]})"
pdf.bookmark group_label, 0, -1
pdf.RDMCell(table_width, row_height * 2, group_label, 'LR', 1, 'L')
@ -317,10 +318,10 @@ module Redmine
end
previous_group = group
end
# fetch row values
col_values = fetch_row_values(issue, query, level)
# make new page if it doesn't fit on the current one
base_y = pdf.get_y
max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
@ -330,26 +331,30 @@ module Redmine
render_table_header(pdf, query, col_width, row_height, table_width)
base_y = pdf.get_y
end
# write the cells on page
issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
pdf.set_y(base_y + max_height)
if query.has_column?(:description) && issue.description?
pdf.set_x(10)
pdf.set_auto_page_break(true, bottom_margin)
pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.description.to_s, issue.attachments, "LRBT")
pdf.set_auto_page_break(false)
end
if query.has_column?(:last_notes) && issue.last_notes.present?
pdf.set_x(10)
pdf.set_auto_page_break(true, bottom_margin)
pdf.RDMwriteHTMLCell(0, 5, 10, '', issue.last_notes.to_s, [], "LRBT")
pdf.set_auto_page_break(false)
query.block_columns.each do |column|
if column.is_a?(QueryCustomFieldColumn)
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
text = show_value(cv, false)
else
text = issue.send(column.name)
end
next if text.blank?
pdf.set_x(10)
pdf.set_auto_page_break(true, bottom_margin)
pdf.SetFontStyle('B',9)
pdf.RDMCell(0, 5, column.caption, "LRT", 1)
pdf.SetFontStyle('',9)
pdf.RDMwriteHTMLCell(0, 5, '', '', text, [], "LRB")
pdf.set_auto_page_break(false)
end
end
end
if issues.size == Setting.issues_export_limit.to_i
pdf.SetFontStyle('B',10)
pdf.RDMCell(0, row_height, '...')
@ -369,25 +374,28 @@ module Redmine
# fetch row values
def fetch_row_values(issue, query, level)
query.inline_columns.collect do |column|
s = if column.is_a?(QueryCustomFieldColumn)
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv, false)
else
value = issue.send(column.name)
case column.name
when :subject
value = " " * level + value
when :attachments
value = value.to_a.map {|a| a.filename}.join("\n")
end
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
s =
if column.is_a?(QueryCustomFieldColumn)
cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv, false)
else
value
value = column.value_object(issue)
case column.name
when :subject
value = " " * level + value
when :attachments
value = value.to_a.map {|a| a.filename}.join("\n")
end
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
elsif value.is_a?(Float)
sprintf "%.2f", value
else
value
end
end
end
s.to_s
end
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -46,8 +46,9 @@ module Redmine
pdf.footer_date = format_date(User.current.today)
pdf.add_page
pdf.SetFontStyle('B',11)
pdf.RDMMultiCell(190,5,
"#{project} - #{page.title} - # #{page.content.version}")
pdf.RDMMultiCell(
190,5,
"#{project} - #{page.title} - # #{page.content.version}")
pdf.ln
# Set resize image scale
pdf.set_image_scale(1.6)
@ -70,12 +71,14 @@ module Redmine
end
def write_wiki_page(pdf, page)
text = textilizable(page.content, :text,
:only_path => false,
:edit_section_links => false,
:headings => false,
:inline_attachments => false
)
text =
textilizable(
page.content, :text,
:only_path => false,
:edit_section_links => false,
:headings => false,
:inline_attachments => false
)
pdf.RDMwriteFormattedCell(190,5,'','', text, page.attachments, 0)
if page.attachments.any?
pdf.ln(5)

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -155,7 +157,7 @@ module Redmine
def target_class
nil
end
def possible_custom_value_options(custom_value)
possible_values_options(custom_value.custom_field, custom_value.customized)
end
@ -183,7 +185,7 @@ module Redmine
def parse_keyword(custom_field, keyword, &block)
separator = Regexp.escape ","
keyword = keyword.to_s
keyword = keyword.dup.to_s
if custom_field.multiple?
values = []
@ -248,7 +250,10 @@ module Redmine
url = url_from_pattern(custom_field, single_value, customized)
[text, url]
end
links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
links = texts_and_urls.sort_by(&:first).map do |text, url|
css_class = (/^https?:\/\//.match?(url)) ? 'external' : nil
view.link_to_if uri_with_safe_scheme?(url), text, url, :class => css_class
end
links.join(', ').html_safe
else
casted
@ -300,8 +305,12 @@ module Redmine
if custom_field.is_required?
''.html_safe
else
view.content_tag('label',
view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
view.content_tag(
'label',
view.check_box_tag(
tag_name,
'__none__', (value == '__none__'), :id => nil,
:data => {:disables => "##{tag_id}"}) + l(:button_clear),
:class => 'inline'
)
end
@ -320,7 +329,7 @@ module Redmine
# Returns nil if the custom field can not be used for sorting.
def order_statement(custom_field)
# COALESCE is here to make sure that blank and NULL values are sorted equally
"COALESCE(#{join_alias custom_field}.value, '')"
Arel.sql "COALESCE(#{join_alias custom_field}.value, '')"
end
# Returns a GROUP BY clause that can used to group by custom value
@ -355,7 +364,7 @@ module Redmine
def validate_single_value(custom_field, value, customized=nil)
errs = super
value = value.to_s
unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
unless custom_field.regexp.blank? or Regexp.new(custom_field.regexp).match?(value)
errs << ::I18n.t('activerecord.errors.messages.invalid')
end
if custom_field.min_length && value.length < custom_field.min_length
@ -437,12 +446,13 @@ module Redmine
url = url_from_pattern(custom_field, value, customized)
else
url = value.to_s
unless url =~ %r{\A[a-z]+://}i
unless %r{\A[a-z]+://}i.match?(url)
# no protocol found, use http by default
url = "http://" + url
end
end
view.link_to value.to_s.truncate(40), url
css_class = (/^https?:\/\//.match?(url)) ? 'external' : nil
view.link_to value.to_s.truncate(40), url, :class => css_class
else
value.to_s
end
@ -457,7 +467,7 @@ module Redmine
# Make the database cast values into numeric
# Postgresql will raise an error if a value can not be casted!
# CustomValue validations should ensure that it doesn't occur
"CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
Arel.sql "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
end
# Returns totals for the given scope
@ -486,7 +496,7 @@ module Redmine
def validate_single_value(custom_field, value, customized=nil)
errs = super
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s.strip =~ /^[+-]?\d+$/
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless /^[+-]?\d+$/.match?(value.to_s.strip)
errs
end
@ -530,7 +540,7 @@ module Redmine
end
def validate_single_value(custom_field, value, customized=nil)
if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
if /^\d{4}-\d{2}-\d{2}$/.match?(value) && (value.to_date rescue false)
[]
else
[::I18n.t('activerecord.errors.messages.not_a_date')]
@ -621,7 +631,7 @@ module Redmine
value ||= label
checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
tag = view.send(tag_method, tag_name, value, checked, :id => nil)
s << view.content_tag('label', tag + ' ' + label)
s << view.content_tag('label', tag + ' ' + label)
end
if custom_value.custom_field.multiple?
s << view.hidden_field_tag(tag_name, '', :id => nil)
@ -726,7 +736,7 @@ module Redmine
def reset_target_class
@target_class = nil
end
def possible_custom_value_options(custom_value)
options = possible_values_options(custom_value.custom_field, custom_value.customized)
missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
@ -753,7 +763,7 @@ module Redmine
end
def group_statement(custom_field)
"COALESCE(#{join_alias custom_field}.value, '')"
Arel.sql "COALESCE(#{join_alias custom_field}.value, '')"
end
def join_for_order_statement(custom_field)
@ -782,7 +792,7 @@ module Redmine
class EnumerationFormat < RecordList
add 'enumeration'
self.form_partial = 'custom_fields/formats/enumeration'
def label
"label_field_format_enumeration"
end
@ -812,7 +822,10 @@ module Redmine
field_attributes :user_role
def possible_values_options(custom_field, object=nil)
possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
users = possible_values_records(custom_field, object)
options = users.map {|u| [u.name, u.id.to_s]}
options = [["<< #{l(:label_me)} >>", User.current.id]] + options if users.include?(User.current)
options
end
def possible_values_records(custom_field, object=nil)
@ -970,7 +983,7 @@ module Redmine
end
else
if custom_value.value.present?
attachment = Attachment.where(:id => custom_value.value.to_s).first
attachment = Attachment.find_by(:id => custom_value.value.to_s)
extensions = custom_value.custom_field.extensions_allowed
if attachment && extensions.present? && !attachment.extension_in?(extensions)
errors << "#{::I18n.t('activerecord.errors.messages.invalid')} (#{l(:setting_attachment_extensions_allowed)}: #{extensions})"
@ -982,16 +995,16 @@ module Redmine
end
def after_save_custom_value(custom_field, custom_value)
if custom_value.value_changed?
if custom_value.saved_change_to_value?
if custom_value.value.present?
attachment = Attachment.where(:id => custom_value.value.to_s).first
attachment = Attachment.find_by(:id => custom_value.value.to_s)
if attachment
attachment.container = custom_value
attachment.save!
end
end
if custom_value.value_was.present?
attachment = Attachment.where(:id => custom_value.value_was.to_s).first
if custom_value.value_before_last_save.present?
attachment = Attachment.find_by(:id => custom_value.value_before_last_save.to_s)
if attachment
attachment.destroy
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -44,7 +46,7 @@ module Redmine
add_at = nil
add_to = nil
del_at = nil
deleted = ""
deleted = +""
diff.each do |change|
pos = change[1]
if change[0] == "+"

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,7 +21,7 @@ module Redmine
module Helpers
# Simple class to handle gantt chart data
class Gantt
class MaxLinesLimitReached < Exception
class MaxLinesLimitReached < StandardError
end
include ERB::Util
@ -32,8 +34,10 @@ module Redmine
IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
}.freeze
# :nodoc:
UNAVAILABLE_COLUMNS = [:tracker, :id, :subject]
# Some utility methods for the PDF export
# @private
class PDF
MaxCharactorsForSubject = 45
TotalWidth = 280
@ -65,17 +69,19 @@ module Redmine
zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2
months = (options[:months] || User.current.pref[:gantt_months]).to_i
@months = (months > 0 && months < 25) ? months : 6
@months = (months > 0 && months < Setting.gantt_months_limit.to_i + 1) ? months : 6
# Save gantt parameters as user preference (zoom and months count)
if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
@months != User.current.pref[:gantt_months]))
if User.current.logged? &&
(@zoom != User.current.pref[:gantt_zoom] ||
@months != User.current.pref[:gantt_months])
User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
User.current.preference.save
end
@date_from = Date.civil(@year_from, @month_from, 1)
@date_to = (@date_from >> @months) - 1
@subjects = ''
@lines = ''
@subjects = +''
@lines = +''
@columns ||= {}
@number_of_rows = nil
@truncated = false
if options.has_key?(:max_rows)
@ -135,11 +141,17 @@ module Redmine
@lines
end
# Renders the selected column of the Gantt chart, the right side of subjects.
def selected_column_content(options={})
render(options.merge(:only => :selected_columns)) unless @columns.has_key?(options[:column].name)
@columns[options[:column].name]
end
# Returns issues that will be rendered
def issues
@issues ||= @query.issues(
:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
:order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
:order => ["#{Project.table_name}.lft ASC", "#{Issue.table_name}.id ASC"],
:limit => @max_rows
)
end
@ -196,8 +208,9 @@ module Redmine
:indent_increment => 20, :render => :subject,
:format => :html}.merge(options)
indent = options[:indent] || 4
@subjects = '' unless options[:only] == :lines
@lines = '' unless options[:only] == :subjects
@subjects = +'' unless options[:only] == :lines || options[:only] == :selected_columns
@lines = +'' unless options[:only] == :subjects || options[:only] == :selected_columns
@columns[options[:column].name] = +'' if options[:only] == :selected_columns && @columns.has_key?(options[:column]) == false
@number_of_rows = 0
begin
Project.project_tree(projects) do |project, level|
@ -207,8 +220,8 @@ module Redmine
rescue MaxLinesLimitReached
@truncated = true
end
@subjects_rendered = true unless options[:only] == :lines
@lines_rendered = true unless options[:only] == :subjects
@subjects_rendered = true unless options[:only] == :lines || options[:only] == :selected_columns
@lines_rendered = true unless options[:only] == :subjects || options[:only] == :selected_columns
render_end(options)
end
@ -254,8 +267,9 @@ module Redmine
def render_object_row(object, options)
class_name = object.class.name.downcase
send("subject_for_#{class_name}", object, options) unless options[:only] == :lines
send("line_for_#{class_name}", object, options) unless options[:only] == :subjects
send("subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :selected_columns
send("line_for_#{class_name}", object, options) unless options[:only] == :subjects || options[:only] == :selected_columns
column_content_for_issue(object, options) if options[:only] == :selected_columns && options[:column].present? && object.is_a?(Issue)
options[:top] += options[:top_increment]
@number_of_rows += 1
if @max_rows && @number_of_rows >= @max_rows
@ -301,9 +315,9 @@ module Redmine
def line_for_version(version, options)
# Skip versions that don't have a start_date
if version.is_a?(Version) && version.due_date && version.start_date
label = "#{h(version)} #{h(version.completed_percent.to_f.round)}%"
label = "#{h(version)} #{h(version.visible_fixed_issues.completed_percent.to_f.round)}%"
label = h("#{version.project} -") + label unless @project && @project == version.project
line(version.start_date, version.due_date, version.completed_percent, true, label, options, version)
line(version.start_date, version.due_date, version.visible_fixed_issues.completed_percent, true, label, options, version)
end
end
@ -323,6 +337,17 @@ module Redmine
end
end
def column_content_for_issue(issue, options)
if options[:format] == :html
data_options = {}
data_options[:collapse_expand] = "issue-#{issue.id}"
style = "position: absolute;top: #{options[:top]}px; font-size: 0.8em;"
content = view.content_tag(:div, view.column_content(options[:column], issue), :style => style, :class => "issue_#{options[:column].name}", :id => "#{options[:column].name}_issue_#{issue.id}", :data => data_options)
@columns[options[:column].name] << content if @columns.has_key?(options[:column].name)
content
end
end
def subject(label, options, object=nil)
send "#{options[:format]}_subject", options, label, object
end
@ -335,7 +360,7 @@ module Redmine
end
# Generates a gantt image
# Only defined if RMagick is avalaible
# Only defined if MiniMagick is avalaible
def to_image(format='PNG')
date_to = (@date_from >> @months) - 1
show_weeks = @zoom > 1
@ -348,98 +373,123 @@ module Redmine
g_height = 20 * number_of_rows + 30
headers_height = (show_weeks ? 2 * header_height : header_height)
height = g_height + headers_height
imgl = Magick::ImageList.new
imgl.new_image(subject_width + g_width + 1, height)
gc = Magick::Draw.new
gc.font = Redmine::Configuration['rmagick_font_path'] || ""
# Subjects
gc.stroke('transparent')
subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
# Months headers
month_f = @date_from
left = subject_width
@months.times do
width = ((month_f >> 1) - month_f) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, 0, left + width, height)
gc.fill('black')
# TODO: Remove rmagick_font_path in a later version
Rails.logger.warn('rmagick_font_path option is deprecated. Use minimagick_font_path instead.') \
unless Redmine::Configuration['rmagick_font_path'].nil?
font_path = Redmine::Configuration['minimagick_font_path'].presence || Redmine::Configuration['rmagick_font_path'].presence
img = MiniMagick::Image.create(".#{format}", false)
MiniMagick::Tool::Convert.new do |gc|
gc.size('%dx%d' % [subject_width + g_width + 1, height])
gc.xc('white')
gc.font(font_path) if font_path.present?
# Subjects
gc.stroke('transparent')
gc.stroke_width(1)
gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
left = left + width
month_f = month_f >> 1
end
# Weeks headers
if show_weeks
subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
# Months headers
month_f = @date_from
left = subject_width
height = header_height
if @date_from.cwday == 1
# date_from is monday
week_f = date_from
else
# find next monday after date_from
week_f = @date_from + (7 - @date_from.cwday + 1)
width = (7 - @date_from.cwday + 1) * zoom
@months.times do
width = ((month_f >> 1) - month_f) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1)
left = left + width
end
while week_f <= date_to
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1)
gc.strokewidth(1)
gc.draw('rectangle %d,%d %d,%d' % [
left, 0, left + width, height
])
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
gc.strokewidth(1)
gc.draw('text %d,%d %s' % [
left.round + 8, 14, Redmine::Utils::Shell.shell_quote("#{month_f.year}-#{month_f.month}")
])
left = left + width
week_f = week_f + 7
month_f = month_f >> 1
end
end
# Days details (week-end in grey)
if show_days
left = subject_width
height = g_height + header_height - 1
wday = @date_from.cwday
(date_to - @date_from + 1).to_i.times do
width = zoom
gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
gc.stroke('#ddd')
gc.stroke_width(1)
gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1)
left = left + width
wday = wday + 1
wday = 1 if wday > 7
# Weeks headers
if show_weeks
left = subject_width
height = header_height
if @date_from.cwday == 1
# date_from is monday
week_f = date_from
else
# find next monday after date_from
week_f = @date_from + (7 - @date_from.cwday + 1)
width = (7 - @date_from.cwday + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.strokewidth(1)
gc.draw('rectangle %d,%d %d,%d' % [
left, header_height, left + width, 2 * header_height + g_height - 1
])
left = left + width
end
while week_f <= date_to
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.strokewidth(1)
gc.draw('rectangle %d,%d %d,%d' % [
left.round, header_height, left.round + width, 2 * header_height + g_height - 1
])
gc.fill('black')
gc.stroke('transparent')
gc.strokewidth(1)
gc.draw('text %d,%d %s' % [
left.round + 2, header_height + 14, Redmine::Utils::Shell.shell_quote(week_f.cweek.to_s)
])
left = left + width
week_f = week_f + 7
end
end
# Days details (week-end in grey)
if show_days
left = subject_width
height = g_height + header_height - 1
wday = @date_from.cwday
(date_to - @date_from + 1).to_i.times do
width = zoom
gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
gc.stroke('#ddd')
gc.strokewidth(1)
gc.draw('rectangle %d,%d %d,%d' % [
left, 2 * header_height, left + width, 2 * header_height + g_height - 1
])
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
# border
gc.fill('transparent')
gc.stroke('grey')
gc.strokewidth(1)
gc.draw('rectangle %d,%d %d,%d' % [
0, 0, subject_width + g_width, headers_height
])
gc.stroke('black')
gc.draw('rectangle %d,%d %d,%d' % [
0, 0, subject_width + g_width, g_height + headers_height - 1
])
# content
top = headers_height + 20
gc.stroke('transparent')
lines(:image => gc, :top => top, :zoom => zoom,
:subject_width => subject_width, :format => :image)
# today red line
if User.current.today >= @date_from and User.current.today <= date_to
gc.stroke('red')
x = (User.current.today - @date_from + 1) * zoom + subject_width
gc.draw('line %g,%g %g,%g' % [
x, headers_height, x, headers_height + g_height - 1
])
end
gc << img.path
end
# border
gc.fill('transparent')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(0, 0, subject_width + g_width, headers_height)
gc.stroke('black')
gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1)
# content
top = headers_height + 20
gc.stroke('transparent')
lines(:image => gc, :top => top, :zoom => zoom,
:subject_width => subject_width, :format => :image)
# today red line
if User.current.today >= @date_from and User.current.today <= date_to
gc.stroke('red')
x = (User.current.today - @date_from + 1) * zoom + subject_width
gc.line(x, headers_height, x, headers_height + g_height - 1)
end
gc.draw(imgl)
imgl.format = format
imgl.to_blob
end if Object.const_defined?(:Magick)
img.to_blob
ensure
img.destroy! if img
end if Object.const_defined?(:MiniMagick)
def to_pdf
pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
@ -581,7 +631,7 @@ module Redmine
coords[:bar_start] = 0
end
if end_date < self.date_to
coords[:end] = end_date - self.date_from
coords[:end] = end_date - self.date_from + 1
coords[:bar_end] = end_date - self.date_from + 1
else
coords[:bar_end] = self.date_to - self.date_from + 1
@ -595,11 +645,11 @@ module Redmine
coords[:bar_progress_end] = self.date_to - self.date_from + 1
end
end
if progress_date < User.current.today
late_date = [User.current.today, end_date].min
if progress_date <= User.current.today
late_date = [User.current.today, end_date].min + 1
if late_date > self.date_from && late_date > start_date
if late_date < self.date_to
coords[:bar_late_end] = late_date - self.date_from + 1
coords[:bar_late_end] = late_date - self.date_from
else
coords[:bar_late_end] = self.date_to - self.date_from + 1
end
@ -608,7 +658,7 @@ module Redmine
end
end
# Transforms dates into pixels witdh
coords.keys.each do |key|
coords.each_key do |key|
coords[key] = (coords[key] * zoom).floor
end
coords
@ -618,23 +668,26 @@ module Redmine
start_date + (end_date - start_date + 1) * (progress / 100.0)
end
def self.sort_issues!(issues)
issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
end
# Singleton class method is public
class << self
def sort_issues!(issues)
issues.sort_by! {|issue| sort_issue_logic(issue)}
end
def self.sort_issue_logic(issue)
julian_date = Date.new()
ancesters_start_date = []
current_issue = issue
begin
ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
current_issue = current_issue.parent
end while (current_issue)
ancesters_start_date
end
def sort_issue_logic(issue)
julian_date = Date.new()
ancesters_start_date = []
current_issue = issue
begin
ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
current_issue = current_issue.parent
end while (current_issue)
ancesters_start_date
end
def self.sort_versions!(versions)
versions.sort!
def sort_versions!(versions)
versions.sort!
end
end
def pdf_new_page?(options)
@ -650,7 +703,7 @@ module Redmine
case object
when Issue
issue = object
css_classes = ''
css_classes = +''
css_classes << ' issue-overdue' if issue.overdue?
css_classes << ' issue-behind-schedule' if issue.behind_schedule?
css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
@ -661,26 +714,21 @@ module Redmine
css_classes << ' behind-start-date' if progress_date < self.date_from
css_classes << ' over-end-date' if progress_date > self.date_to
end
s = "".html_safe
if issue.assigned_to.present?
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
s << view.avatar(issue.assigned_to,
:class => 'gravatar icon-gravatar',
:size => 13,
:title => assigned_string).to_s.html_safe
end
s = (+"").html_safe
s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar')
s << view.link_to_issue(issue).html_safe
s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => issue.id, :style => 'display:none;', :class => 'toggle-selection')
view.content_tag(:span, s, :class => css_classes).html_safe
when Version
version = object
html_class = ""
html_class = +""
html_class << 'icon icon-package '
html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
html_class << (version.overdue? ? 'version-overdue' : '')
html_class << ' version-closed' unless version.open?
if version.start_date && version.due_date && version.completed_percent
if version.start_date && version.due_date && version.visible_fixed_issues.completed_percent
progress_date = calc_progress_date(version.start_date,
version.due_date, version.completed_percent)
version.due_date, version.visible_fixed_issues.completed_percent)
html_class << ' behind-start-date' if progress_date < self.date_from
html_class << ' over-end-date' if progress_date > self.date_to
end
@ -688,7 +736,7 @@ module Redmine
view.content_tag(:span, s, :class => html_class).html_safe
when Project
project = object
html_class = ""
html_class = +""
html_class << 'icon icon-projects '
html_class << (project.overdue? ? 'project-overdue' : '')
s = view.link_to_project(project).html_safe
@ -697,21 +745,43 @@ module Redmine
end
def html_subject(params, subject, object)
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
content = html_subject_content(object) || subject
tag_options = {:style => style}
tag_options = {}
case object
when Issue
tag_options[:id] = "issue-#{object.id}"
tag_options[:class] = "issue-subject"
tag_options[:class] = "issue-subject hascontextmenu"
tag_options[:title] = object.subject
children = object.children & project_issues(object.project)
has_children = children.present? && (children.collect(&:fixed_version).uniq & [object.fixed_version]).present?
when Version
tag_options[:id] = "version-#{object.id}"
tag_options[:class] = "version-name"
has_children = object.fixed_issues.exists?
when Project
tag_options[:class] = "project-name"
has_children = object.issues.exists? || object.versions.exists?
end
if object
tag_options[:data] = {
:collapse_expand => {
:top_increment => params[:top_increment],
:obj_id => "#{object.class}-#{object.id}".downcase,
},
}
end
if has_children
content = view.content_tag(:span, nil, :class => 'icon icon-expended expander') + content
tag_options[:class] += ' open'
else
if params[:indent]
params = params.dup
params[:indent] += 12
end
end
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
style += "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
tag_options[:style] = style
output = view.content_tag(:div, content, tag_options)
@subjects << output
output
@ -725,7 +795,7 @@ module Redmine
params[:pdf].RDMCell(params[:subject_width] - 15, 5,
(" " * params[:indent]) +
subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
"LR")
"LR")
params[:pdf].SetY(params[:top])
params[:pdf].SetX(params[:subject_width])
params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
@ -734,8 +804,10 @@ module Redmine
def image_subject(params, subject, options={})
params[:image].fill('black')
params[:image].stroke('transparent')
params[:image].stroke_width(1)
params[:image].text(params[:indent], params[:top] + 2, subject)
params[:image].strokewidth(1)
params[:image].draw('text %d,%d %s' % [
params[:indent], params[:top] + 2, Redmine::Utils::Shell.shell_quote(subject)
])
end
def issue_relations(issue)
@ -749,9 +821,11 @@ module Redmine
end
def html_task(params, coords, markers, label, object)
output = ''
css = "task " + case object
output = +''
data_options = {}
data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase if object
css = "task " +
case object
when Project
"project"
when Version
@ -761,11 +835,10 @@ module Redmine
else
""
end
# Renders the task bar, with progress and late
if coords[:bar_start] && coords[:bar_end]
width = coords[:bar_end] - coords[:bar_start] - 2
style = ""
style = +""
style << "top:#{params[:top]}px;"
style << "left:#{coords[:bar_start]}px;"
style << "width:#{width}px;"
@ -773,27 +846,30 @@ module Redmine
html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
content_opt = {:style => style,
:class => "#{css} task_todo",
:id => html_id}
:id => html_id,
:data => {}}
if object.is_a?(Issue)
rels = issue_relations(object)
if rels.present?
content_opt[:data] = {"rels" => rels.to_json}
end
end
content_opt[:data].merge!(data_options)
output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
if coords[:bar_late_end]
width = coords[:bar_late_end] - coords[:bar_start] - 2
style = ""
style = +""
style << "top:#{params[:top]}px;"
style << "left:#{coords[:bar_start]}px;"
style << "width:#{width}px;"
output << view.content_tag(:div, '&nbsp;'.html_safe,
:style => style,
:class => "#{css} task_late")
:class => "#{css} task_late",
:data => data_options)
end
if coords[:bar_progress_end]
width = coords[:bar_progress_end] - coords[:bar_start] - 2
style = ""
style = +""
style << "top:#{params[:top]}px;"
style << "left:#{coords[:bar_start]}px;"
style << "width:#{width}px;"
@ -802,46 +878,51 @@ module Redmine
output << view.content_tag(:div, '&nbsp;'.html_safe,
:style => style,
:class => "#{css} task_done",
:id => html_id)
:id => html_id,
:data => data_options)
end
end
# Renders the markers
if markers
if coords[:start]
style = ""
style = +""
style << "top:#{params[:top]}px;"
style << "left:#{coords[:start]}px;"
style << "width:15px;"
output << view.content_tag(:div, '&nbsp;'.html_safe,
:style => style,
:class => "#{css} marker starting")
:class => "#{css} marker starting",
:data => data_options)
end
if coords[:end]
style = ""
style = +""
style << "top:#{params[:top]}px;"
style << "left:#{coords[:end] + params[:zoom]}px;"
style << "left:#{coords[:end]}px;"
style << "width:15px;"
output << view.content_tag(:div, '&nbsp;'.html_safe,
:style => style,
:class => "#{css} marker ending")
:class => "#{css} marker ending",
:data => data_options)
end
end
# Renders the label on the right
if label
style = ""
style = +""
style << "top:#{params[:top]}px;"
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
style << "width:15px;"
output << view.content_tag(:div, label,
:style => style,
:class => "#{css} label")
:class => "#{css} label",
:data => data_options)
end
# Renders the tooltip
if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
s = view.content_tag(:span,
view.render_issue_tooltip(object).html_safe,
:class => "tip")
style = ""
s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => object.id, :style => 'display:none;', :class => 'toggle-selection')
style = +""
style << "position: absolute;"
style << "top:#{params[:top]}px;"
style << "left:#{coords[:bar_start]}px;"
@ -849,7 +930,8 @@ module Redmine
style << "height:12px;"
output << view.content_tag(:div, s.html_safe,
:style => style,
:class => "tooltip")
:class => "tooltip hascontextmenu",
:data => data_options)
end
@lines << output
output
@ -863,21 +945,24 @@ module Redmine
height /= 2 if markers
# Renders the task bar, with progress and late
if coords[:bar_start] && coords[:bar_end]
width = [1, coords[:bar_end] - coords[:bar_start]].max
params[:pdf].SetY(params[:top] + 1.5)
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
params[:pdf].SetFillColor(200, 200, 200)
params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
params[:pdf].RDMCell(width, height, "", 0, 0, "", 1)
if coords[:bar_late_end]
width = [1, coords[:bar_late_end] - coords[:bar_start]].max
params[:pdf].SetY(params[:top] + 1.5)
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
params[:pdf].SetFillColor(255, 100, 100)
params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
params[:pdf].RDMCell(width, height, "", 0, 0, "", 1)
end
if coords[:bar_progress_end]
width = [1, coords[:bar_progress_end] - coords[:bar_start]].max
params[:pdf].SetY(params[:top] + 1.5)
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
params[:pdf].SetFillColor(90, 200, 90)
params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
params[:pdf].RDMCell(width, height, "", 0, 0, "", 1)
end
end
# Renders the markers
@ -910,23 +995,29 @@ module Redmine
# Renders the task bar, with progress and late
if coords[:bar_start] && coords[:bar_end]
params[:image].fill('#aaa')
params[:image].rectangle(params[:subject_width] + coords[:bar_start],
params[:top],
params[:subject_width] + coords[:bar_end],
params[:top] - height)
params[:image].draw('rectangle %d,%d %d,%d' % [
params[:subject_width] + coords[:bar_start],
params[:top],
params[:subject_width] + coords[:bar_end],
params[:top] - height
])
if coords[:bar_late_end]
params[:image].fill('#f66')
params[:image].rectangle(params[:subject_width] + coords[:bar_start],
params[:top],
params[:subject_width] + coords[:bar_late_end],
params[:top] - height)
params[:image].draw('rectangle %d,%d %d,%d' % [
params[:subject_width] + coords[:bar_start],
params[:top],
params[:subject_width] + coords[:bar_late_end],
params[:top] - height
])
end
if coords[:bar_progress_end]
params[:image].fill('#00c600')
params[:image].rectangle(params[:subject_width] + coords[:bar_start],
params[:top],
params[:subject_width] + coords[:bar_progress_end],
params[:top] - height)
params[:image].draw('rectangle %d,%d %d,%d' % [
params[:subject_width] + coords[:bar_start],
params[:top],
params[:subject_width] + coords[:bar_progress_end],
params[:top] - height
])
end
end
# Renders the markers
@ -935,21 +1026,31 @@ module Redmine
x = params[:subject_width] + coords[:start]
y = params[:top] - height / 2
params[:image].fill('blue')
params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [
x - 4, y,
x, y - 4,
x + 4, y,
x, y + 4
])
end
if coords[:end]
x = params[:subject_width] + coords[:end] + params[:zoom]
x = params[:subject_width] + coords[:end]
y = params[:top] - height / 2
params[:image].fill('blue')
params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
params[:image].draw('polygon %d,%d %d,%d %d,%d %d,%d' % [
x - 4, y,
x, y - 4,
x + 4, y,
x, y + 4
])
end
end
# Renders the label on the right
if label
params[:image].fill('black')
params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,
params[:top] + 1,
label)
params[:image].draw('text %d,%d %s' % [
params[:subject_width] + (coords[:bar_end] || 0) + 5, params[:top] + 1, Redmine::Utils::Shell.shell_quote(label)
])
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -56,7 +58,7 @@ module Redmine
end
@hours << h
end
@hours.each do |row|
case @columns
when 'year'
@ -69,13 +71,13 @@ module Redmine
row['day'] = "#{row['spent_on']}"
end
end
min = @hours.collect {|row| row['spent_on']}.min
@from = min ? min.to_date : User.current.today
max = @hours.collect {|row| row['spent_on']}.max
@to = max ? max.to_date : User.current.today
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
@ -129,13 +131,13 @@ module Redmine
}
# Add time entry custom fields
custom_fields = TimeEntryCustomField.all
custom_fields = TimeEntryCustomField.visible
# Add project custom fields
custom_fields += ProjectCustomField.all
custom_fields += ProjectCustomField.visible
# Add issue custom fields
custom_fields += (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields += @project.nil? ? IssueCustomField.visible.for_all : @project.all_issue_custom_fields.visible
# Add time entry activity custom fields
custom_fields += TimeEntryActivityCustomField.all
custom_fields += TimeEntryActivityCustomField.visible
# Add list and boolean custom fields as available criteria
custom_fields.select {|cf| %w(list bool).include?(cf.field_format) && !cf.multiple?}.each do |cf|

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -24,10 +26,10 @@ module Redmine
# URLs relative to the current document or document root (without a protocol
# separator, should be harmless
return true unless uri.to_s.include? ":"
# Other URLs need to be parsed
schemes.include? URI.parse(uri).scheme
rescue URI::InvalidURIError
rescue URI::Error
false
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -29,4 +31,4 @@ module Redmine
end
end
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -65,11 +67,11 @@ module Redmine
end
end
end
def controller
nil
end
def config
ActionController::Base.config
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -61,7 +63,7 @@ module Redmine
# Localizes the given args with user's language
def lu(user, *args)
lang = user.try(:language).presence || Setting.default_language
ll(lang, *args)
ll(lang, *args)
end
def format_date(date)
@ -77,8 +79,7 @@ module Redmine
options = {}
options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format)
time = time.to_time if time.is_a?(String)
zone = user.time_zone
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
local = user.convert_time_to_user_timezone(time)
(include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options)
end
@ -116,16 +117,18 @@ module Redmine
# The result is cached to prevent from loading all translations files
# unless :cache => false option is given
def languages_options(options={})
options = if options[:cache] == false
valid_languages.
select {|locale| ::I18n.exists?(:general_lang_name, locale)}.
map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.
sort {|x,y| x.first <=> y.first }
else
ActionController::Base.cache_store.fetch "i18n/languages_options/#{Redmine::VERSION}" do
languages_options :cache => false
options =
if options[:cache] == false
available_locales = ::I18n.backend.available_locales
valid_languages.
select {|locale| available_locales.include?(locale)}.
map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.
sort_by(&:first)
else
ActionController::Base.cache_store.fetch "i18n/languages_options/#{Redmine::VERSION}" do
languages_options :cache => false
end
end
end
options.map {|name, lang| [name.force_encoding("UTF-8"), lang.force_encoding("UTF-8")]}
end
@ -145,71 +148,17 @@ module Redmine
end
# Custom backend based on I18n::Backend::Simple with the following changes:
# * lazy loading of translation files
# * available_locales are determined by looking at translation file names
class Backend
(class << self; self; end).class_eval { public :include }
class Backend < ::I18n::Backend::Simple
module Implementation
include ::I18n::Backend::Base
include ::I18n::Backend::Pluralization
# Stores translations for the given locale in memory.
# This uses a deep merge for the translations hash, so existing
# translations will be overwritten by new ones only at the deepest
# level of the hash.
def store_translations(locale, data, options = {})
locale = locale.to_sym
translations[locale] ||= {}
data = data.deep_symbolize_keys
translations[locale].deep_merge!(data)
end
# Get available locales from the translations filenames
def available_locales
@available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym)
end
# Clean up translations
def reload!
@translations = nil
@available_locales = nil
super
end
protected
def init_translations(locale)
locale = locale.to_s
paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
load_translations(paths)
translations[locale] ||= {}
end
def translations
@translations ||= {}
end
# Looks up a translation from the translations hash. Returns nil if
# eiher key is nil, or locale, scope or key do not exist as a key in the
# nested translations hash. Splits keys or scopes containing dots
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
# <tt>%w(currency format)</tt>.
def lookup(locale, key, scope = [], options = {})
init_translations(locale) unless translations.key?(locale)
keys = ::I18n.normalize_keys(locale, key, scope, options[:separator])
keys.inject(translations) do |result, _key|
_key = _key.to_sym
return nil unless result.is_a?(Hash) && result.has_key?(_key)
result = result[_key]
result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
result
end
end
end
include Implementation
# Adds fallback to default locale for untranslated strings
include ::I18n::Backend::Fallbacks
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: false
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Redmine
module Info
class << self
@ -7,13 +9,15 @@ module Redmine
def versioned_name; "#{app_name} #{Redmine::VERSION}" end
def environment
s = "Environment:\n"
s = +"Environment:\n"
s << [
["Redmine version", Redmine::VERSION],
["Ruby version", "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"],
["Rails version", Rails::VERSION::STRING],
["Environment", Rails.env],
["Database adapter", ActiveRecord::Base.connection.adapter_name]
["Database adapter", ActiveRecord::Base.connection.adapter_name],
["Mailer queue", ActionMailer::DeliveryJob.queue_adapter.class.name],
["Mailer delivery", ActionMailer::Base.delivery_method]
].map {|info| " %-30s %s" % info}.join("\n") + "\n"
s << "SCM:\n"

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -17,7 +19,8 @@
module Redmine
module MenuManager
class MenuError < StandardError #:nodoc:
# @private
class MenuError < StandardError
end
module MenuController
@ -121,7 +124,7 @@ module Redmine
else
caption, url, selected = extract_node_details(node, project)
return content_tag('li',
render_single_menu_node(node, caption, url, selected))
render_single_menu_node(node, caption, url, selected))
end
end
@ -204,18 +207,19 @@ module Redmine
def extract_node_details(node, project=nil)
item = node
url = case item.url
when Hash
project.nil? ? item.url : {item.param => project}.merge(item.url)
when Symbol
if project
send(item.url, project)
url =
case item.url
when Hash
project.nil? ? item.url : {item.param => project}.merge(item.url)
when Symbol
if project
send(item.url, project)
else
send(item.url)
end
else
send(item.url)
item.url
end
else
item.url
end
caption = item.caption(project)
return [caption, url, (current_menu_item == item.name)]
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -15,8 +17,6 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'mime/types'
module Redmine
module MimeType
@ -35,6 +35,7 @@ module Redmine
'text/x-ruby' => 'rb,rbw,ruby,rake,erb',
'text/x-csh' => 'csh',
'text/x-sh' => 'sh',
'text/x-textile' => 'textile',
'text/xml' => 'xml,xsd,mxml',
'text/yaml' => 'yml,yaml',
'text/csv' => 'csv',
@ -46,6 +47,8 @@ module Redmine
'image/x-ms-bmp' => 'bmp',
'application/javascript' => 'js',
'application/pdf' => 'pdf',
'video/mp4' => 'mp4',
'video/webm' => 'webm',
}.freeze
EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
@ -60,28 +63,23 @@ module Redmine
# returns mime type for name or nil if unknown
def self.of(name)
return nil unless name.present?
if m = name.to_s.match(/(^|\.)([^\.]+)$/)
extension = m[2].downcase
@known_types ||= Hash.new do |h, ext|
type = EXTENSIONS[ext]
type ||= MIME::Types.type_for(ext).first.to_s.presence
h[ext] = type
end
@known_types[extension]
ext = File.extname(name.to_s)[1..-1]
if ext
ext.downcase!
EXTENSIONS[ext] || MiniMime.lookup_by_extension(ext)&.content_type
end
end
# Returns the css class associated to
# the mime type of name
def self.css_class_of(name)
mime = of(name)
mime && mime.gsub('/', '-')
mimetype = of(name)
mimetype&.tr('/', '-')
end
def self.main_mimetype_of(name)
mimetype = of(name)
mimetype.split('/').first if mimetype
mimetype&.split('/')&.first
end
# return true if mime-type for name is type/*

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -24,12 +26,14 @@ module Redmine
CORE_BLOCKS = {
'issuesassignedtome' => {:label => :label_assigned_to_me_issues},
'issuesreportedbyme' => {:label => :label_reported_issues},
'issuesupdatedbyme' => {:label => :label_updated_issues},
'issueswatched' => {:label => :label_watched_issues},
'issuequery' => {:label => :label_issue_plural, :max_occurs => 3},
'news' => {:label => :label_news_latest},
'calendar' => {:label => :label_calendar},
'documents' => {:label => :label_document_plural},
'timelog' => {:label => :label_spent_time}
'timelog' => {:label => :label_spent_time},
'activity' => {:label => :label_activity}
}
def self.groups

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -84,19 +86,19 @@ module Redmine
if parent
previous_root_id = root_id
self.root_id = parent.root_id
lft_after_move = target_lft
self.class.where(:root_id => parent.root_id).update_all([
"lft = CASE WHEN lft >= :lft THEN lft + :shift ELSE lft END, " +
"rgt = CASE WHEN rgt >= :lft THEN rgt + :shift ELSE rgt END",
{:lft => lft_after_move, :shift => (rgt - lft + 1)}
])
self.class.where(:root_id => previous_root_id).update_all([
"root_id = :root_id, lft = lft + :shift, rgt = rgt + :shift",
{:root_id => parent.root_id, :shift => lft_after_move - lft}
])
self.lft, self.rgt = lft_after_move, (rgt - lft + lft_after_move)
parent.send :reload_nested_set_values
end
@ -105,7 +107,7 @@ module Redmine
def remove_from_nested_set
self.class.where(:root_id => root_id).where("lft >= ? AND rgt <= ?", lft, rgt).
update_all(["root_id = :id, lft = lft - :shift, rgt = rgt - :shift", {:id => id, :shift => lft - 1}])
self.class.where(:root_id => root_id).update_all([
"lft = CASE WHEN lft >= :lft THEN lft - :shift ELSE lft END, " +
"rgt = CASE WHEN rgt >= :lft THEN rgt - :shift ELSE rgt END",
@ -149,7 +151,7 @@ module Redmine
end
def lock_nested_set
if self.class.connection.adapter_name =~ /sqlserver/i
if /sqlserver/i.match?(self.class.connection.adapter_name)
lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)"
# Custom lock for SQLServer
# This can be problematic if root_id or parent root_id changes

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -119,7 +121,7 @@ module Redmine
def lock_nested_set
lock = true
if self.class.connection.adapter_name =~ /sqlserver/i
if /sqlserver/i.match?(self.class.connection.adapter_name)
lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)"
end
self.class.order(:id).lock(lock).ids

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Redmine
class Notifiable < Struct.new(:name, :parent)
@ -14,6 +16,7 @@ module Redmine
notifications << Notifiable.new('issue_status_updated', 'issue_updated')
notifications << Notifiable.new('issue_assigned_to_updated', 'issue_updated')
notifications << Notifiable.new('issue_priority_updated', 'issue_updated')
notifications << Notifiable.new('issue_fixed_version_updated', 'issue_updated')
notifications << Notifiable.new('news_added')
notifications << Notifiable.new('news_comment_added')
notifications << Notifiable.new('document_added')

View file

@ -1,7 +1,7 @@
# encoding: utf-8
#
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -159,7 +159,7 @@ module Redmine
per_page_links = false if count.nil?
page_param = paginator.page_param
html = '<ul class="pages">'
html = +'<ul class="pages">'
if paginator.multiple_pages?
# \xc2\xab(utf-8) = &#171;

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,8 +21,8 @@ module Redmine
module Platform
class << self
def mswin?
(RUBY_PLATFORM =~ /(:?mswin|mingw)/) ||
(RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
(/(:?mswin|mingw)/.match?(RUBY_PLATFORM)) ||
(RUBY_PLATFORM == 'java' && /windows/i.match?(ENV['OS'] || ENV['os']))
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -15,9 +17,12 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine #:nodoc:
module Redmine
# Exception raised when a plugin cannot be found given its id.
class PluginNotFound < StandardError; end
# Exception raised when a plugin requirement is not met.
class PluginRequirementError < StandardError; end
# Base class for Redmine plugins.
@ -42,10 +47,14 @@ module Redmine #:nodoc:
# In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
#
# When rendered, the plugin settings value is available as the local variable +settings+
#
# See: http://www.redmine.org/projects/redmine/wiki/Plugin_Tutorial
class Plugin
# Absolute path to the directory where plugins are located
cattr_accessor :directory
self.directory = File.join(Rails.root, 'plugins')
# Absolute path to the plublic directory where plugins assets are copied
cattr_accessor :public_directory
self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
@ -69,7 +78,17 @@ module Redmine #:nodoc:
def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
attr_reader :id
# Plugin constructor
# Plugin constructor: instanciates a new Redmine::Plugin with given +id+
# and make it evaluate the given +block+
#
# Example
# Redmine::Plugin.register :example do
# name 'Example plugin'
# author 'John Smith'
# description 'This is an example plugin for Redmine'
# version '0.0.1'
# requires_redmine version_or_higher: '3.0.0'
# end
def self.register(id, &block)
p = new(id)
p.instance_eval(&block)
@ -79,6 +98,10 @@ module Redmine #:nodoc:
# Set a default directory if it was not provided during registration
p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
unless File.directory?(p.directory)
raise PluginNotFound, "Plugin not found. The directory for plugin #{p.id} should be #{p.directory}."
end
# Adds plugin locales if any
# YAML translation files should be found under <plugin>/config/locales/
Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
@ -90,10 +113,13 @@ module Redmine #:nodoc:
ActionMailer::Base.prepend_view_path(view_path)
end
# Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
ActiveSupport::Dependencies.autoload_paths += [dir]
end
# Add the plugin directories to rails autoload paths
engine_cfg = Rails::Engine::Configuration.new(p.directory)
engine_cfg.paths.add 'lib', eager_load: true
Rails.application.config.eager_load_paths += engine_cfg.eager_load_paths
Rails.application.config.autoload_once_paths += engine_cfg.autoload_once_paths
Rails.application.config.autoload_paths += engine_cfg.autoload_paths
ActiveSupport::Dependencies.autoload_paths += engine_cfg.eager_load_paths + engine_cfg.autoload_once_paths + engine_cfg.autoload_paths
# Defines plugin setting if present
if p.settings
@ -170,6 +196,7 @@ module Redmine #:nodoc:
id
end
# Returns the absolute path to the plugin assets directory
def assets_directory
File.join(directory, 'assets')
end
@ -248,7 +275,11 @@ module Redmine #:nodoc:
arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
arg.assert_valid_keys(:version, :version_or_higher)
plugin = Plugin.find(plugin_name)
begin
plugin = Plugin.find(plugin_name)
rescue PluginNotFound
raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin")
end
current = plugin.version.split('.').collect(&:to_i)
arg.each do |k, v|
@ -311,7 +342,7 @@ module Redmine #:nodoc:
# permission :say_hello, { :example => :say_hello }, :require => :member
def permission(name, actions, options = {})
if @project_module
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map| map.permission(name, actions, options)}}
else
Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
end
@ -367,7 +398,7 @@ module Redmine #:nodoc:
# * :label - label for the formatter displayed in application settings
#
# Examples:
# wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")
# wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")
#
def wiki_format_provider(name, *args)
Redmine::WikiFormatting.register(name, *args)
@ -391,7 +422,7 @@ module Redmine #:nodoc:
base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
begin
FileUtils.mkdir_p(base_target_dir)
rescue Exception => e
rescue => e
raise "Could not create directory #{base_target_dir}: " + e.message
end
end
@ -402,7 +433,7 @@ module Redmine #:nodoc:
target_dir = File.join(destination, dir.gsub(source, ''))
begin
FileUtils.mkdir_p(target_dir)
rescue Exception => e
rescue => e
raise "Could not create directory #{target_dir}: " + e.message
end
end
@ -413,7 +444,7 @@ module Redmine #:nodoc:
unless File.exist?(target) && FileUtils.identical?(file, target)
FileUtils.cp(file, target)
end
rescue Exception => e
rescue => e
raise "Could not copy #{file} to #{target}: " + e.message
end
end
@ -449,7 +480,6 @@ module Redmine #:nodoc:
# Migrate this plugin to the given version
def migrate(version = nil)
puts "Migrating #{id} (#{name})..."
Redmine::Plugin::Migrator.migrate_plugin(self, version)
end
@ -469,6 +499,36 @@ module Redmine #:nodoc:
end
end
class MigrationContext < ActiveRecord::MigrationContext
def up(target_version = nil)
selected_migrations =
if block_given?
migrations.select { |m| yield m }
else
migrations
end
Migrator.new(:up, selected_migrations, target_version).migrate
end
def down(target_version = nil)
selected_migrations =
if block_given?
migrations.select { |m| yield m }
else
migrations
end
Migrator.new(:down, selected_migrations, target_version).migrate
end
def run(direction, target_version)
Migrator.new(direction, migrations, target_version).run
end
def open
Migrator.new(:up, migrations, nil)
end
end
class Migrator < ActiveRecord::Migrator
# We need to be able to set the 'current' plugin being migrated.
cattr_accessor :current_plugin
@ -478,22 +538,29 @@ module Redmine #:nodoc:
def migrate_plugin(plugin, version)
self.current_plugin = plugin
return if current_version(plugin) == version
migrate(plugin.migration_directory, version)
MigrationContext.new(plugin.migration_directory).migrate(version)
end
def current_version(plugin=current_plugin)
def get_all_versions(plugin = current_plugin)
# Delete migrations that don't match .. to_i will work because the number comes first
::ActiveRecord::Base.connection.select_values(
"SELECT version FROM #{schema_migrations_table_name}"
).delete_if{ |v| v.match(/-#{plugin.id}$/) == nil }.map(&:to_i).max || 0
@all_versions ||= {}
@all_versions[plugin.id.to_s] ||= begin
sm_table = ::ActiveRecord::SchemaMigration.table_name
migration_versions = ActiveRecord::Base.connection.select_values("SELECT version FROM #{sm_table}")
versions_by_plugins = migration_versions.group_by { |version| version.match(/-(.*)$/).try(:[], 1) }
@all_versions = versions_by_plugins.transform_values! {|versions| versions.map!(&:to_i).sort! }
@all_versions[plugin.id.to_s] || []
end
end
def current_version(plugin = current_plugin)
get_all_versions(plugin).last || 0
end
end
def migrated
sm_table = self.class.schema_migrations_table_name
::ActiveRecord::Base.connection.select_values(
"SELECT version FROM #{sm_table}"
).delete_if{ |v| v.match(/-#{current_plugin.id}$/) == nil }.map(&:to_i).sort
def load_migrated
@migrated_versions = Set.new(self.class.get_all_versions(current_plugin))
end
def record_version_state_after_migrating(version)

View file

@ -1,5 +1,7 @@
# frozen_string_literal: false
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -0,0 +1,113 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine
class ProjectJumpBox
def initialize(user)
@user = user
@pref_project_ids = {}
end
def recent_projects_count
@user.pref.recently_used_projects
end
def recently_used_projects(query = nil)
project_ids = recently_used_project_ids
projects = Project.where(id: project_ids)
if query
projects = projects.like(query)
end
projects.
index_by(&:id).
values_at(*project_ids). # sort according to stored order
compact
end
def bookmarked_projects(query = nil)
projects = Project.where(id: bookmarked_project_ids).visible
if query
projects = projects.like(query)
end
projects.to_a
end
def project_used(project)
return if project.blank? || project.id.blank?
id_array = recently_used_project_ids
id_array.reject!{ |i| i == project.id }
# we dont want bookmarks in the recently used list:
id_array.unshift(project.id) unless bookmark?(project)
self.recently_used_project_ids = id_array[0, recent_projects_count]
end
def bookmark_project(project)
self.recently_used_project_ids = recently_used_project_ids.reject{|id| id == project.id}
self.bookmarked_project_ids = (bookmarked_project_ids << project.id)
end
def delete_project_bookmark(project)
self.bookmarked_project_ids = bookmarked_project_ids.reject do |id|
id == project.id
end
end
def bookmark?(project)
project && project.id && bookmarked_project_ids.include?(project.id)
end
private
def bookmarked_project_ids
pref_project_ids :bookmarked_project_ids
end
def bookmarked_project_ids=(new_ids)
set_pref_project_ids :bookmarked_project_ids, new_ids
end
def recently_used_project_ids
pref_project_ids(:recently_used_project_ids)[0,recent_projects_count]
end
def recently_used_project_ids=(new_ids)
set_pref_project_ids :recently_used_project_ids, new_ids
end
def pref_project_ids(key)
return [] unless @user.logged?
@pref_project_ids[key] ||= (@user.pref[key] || '').split(',').map(&:to_i)
end
def set_pref_project_ids(key, new_values)
return nil unless @user.logged?
old_value = @user.pref[key]
new_value = new_values.uniq.join(',')
if old_value != new_value
@user.pref[key] = new_value
@user.pref.save
end
@pref_project_ids.delete key
nil
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -32,7 +34,7 @@ module Redmine
@safe_attributes ||= []
if args.empty?
if superclass.include?(Redmine::SafeAttributes)
@safe_attributes + superclass.safe_attributes
@safe_attributes + superclass.safe_attributes
else
@safe_attributes
end
@ -80,6 +82,10 @@ module Redmine
# Sets attributes from attrs that are safe
# attrs is a Hash with string keys
def safe_attributes=(attrs, user=User.current)
if attrs.respond_to?(:to_unsafe_hash)
attrs = attrs.to_unsafe_hash
end
return unless attrs.is_a?(Hash)
self.attributes = delete_unsafe_attributes(attrs, user)
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,7 +23,8 @@ require 'redmine/scm/adapters'
module Redmine
module Scm
module Adapters
class AbstractAdapter #:nodoc:
# @private
class AbstractAdapter
include Redmine::Utils::Shell
# raised if scm command exited with error, e.g. unknown revision.
@ -175,7 +178,8 @@ module Redmine
(path[-1,1] == "/") ? path[0..-2] : path
end
private
private
def retrieve_root_url
info = self.info
info ? info.root_url : nil
@ -183,7 +187,7 @@ module Redmine
def target(path, sq=true)
path ||= ''
base = path.match(/^\//) ? root_url : url
base = /^\//.match?(path) ? root_url : url
str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
if sq
str = shell_quote(str)
@ -199,10 +203,6 @@ module Redmine
self.class.shellout(cmd, options, &block)
end
def self.logger
Rails.logger
end
# Path to the file where scm stderr output is logged
# Returns nil if the log file is not writable
def self.stderr_log_file
@ -211,7 +211,7 @@ module Redmine
path = Redmine::Configuration['scm_stderr_log_file'].presence
path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
if File.exists?(path)
if File.file?(path) && File.writable?(path)
if File.file?(path) && File.writable?(path)
writable = true
else
logger.warn("SCM log file (#{path}) is not writable")
@ -228,37 +228,41 @@ module Redmine
end
@stderr_log_file || nil
end
private_class_method :stderr_log_file
def self.shellout(cmd, options = {}, &block)
if logger && logger.debug?
logger.debug "Shelling out: #{strip_credential(cmd)}"
# Capture stderr in a log file
if stderr_log_file
cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
end
# Singleton class method is public
class << self
def logger
Rails.logger
end
begin
mode = "r+"
IO.popen(cmd, mode) do |io|
io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
io.close_write unless options[:write_stdin]
block.call(io) if block_given?
def shellout(cmd, options = {}, &block)
if logger && logger.debug?
logger.debug "Shelling out: #{strip_credential(cmd)}"
# Capture stderr in a log file
if stderr_log_file
cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
end
end
begin
mode = "r+"
IO.popen(cmd, mode) do |io|
io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
io.close_write unless options[:write_stdin]
yield(io) if block_given?
end
rescue => e
msg = strip_credential(e.message)
# The command failed, log it and re-raise
logmsg = "SCM command failed, "
logmsg += "make sure that your SCM command (e.g. svn) is "
logmsg += "in PATH (#{ENV['PATH']})\n"
logmsg += "You can configure your scm commands in config/configuration.yml.\n"
logmsg += "#{strip_credential(cmd)}\n"
logmsg += "with: #{msg}"
logger.error(logmsg)
raise CommandFailed.new(msg)
end
## If scm command does not exist,
## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
## in production environment.
# rescue Errno::ENOENT => e
rescue Exception => e
msg = strip_credential(e.message)
# The command failed, log it and re-raise
logmsg = "SCM command failed, "
logmsg += "make sure that your SCM command (e.g. svn) is "
logmsg += "in PATH (#{ENV['PATH']})\n"
logmsg += "You can configure your scm commands in config/configuration.yml.\n"
logmsg += "#{strip_credential(cmd)}\n"
logmsg += "with: #{msg}"
logger.error(logmsg)
raise CommandFailed.new(msg)
end
end
@ -267,18 +271,20 @@ module Redmine
q = (Redmine::Platform.mswin? ? '"' : "'")
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
end
private_class_method :strip_credential
def strip_credential(cmd)
self.class.strip_credential(cmd)
end
def scm_iconv(to, from, str)
return nil if str.nil?
return if str.nil?
return str if to == from && str.encoding.to_s == from
str = str.dup
str.force_encoding(from)
begin
str.encode(to)
rescue Exception => err
rescue => err
logger.error("failed to convert from #{from} to #{to}. #{err}")
nil
end
@ -328,11 +334,11 @@ module Redmine
end
def is_file?
'file' == self.kind
self.kind == 'file'
end
def is_dir?
'dir' == self.kind
self.kind == 'dir'
end
def is_text?

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -43,7 +45,7 @@ module Redmine
end
def scm_command_version
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
scm_version = scm_version_from_command_line.b
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i)
end
@ -94,10 +96,9 @@ module Redmine
cmd_args << "-r#{identifier.to_i}"
cmd_args << bzr_target(path)
scm_cmd(*cmd_args) do |io|
prefix_utf8 = "#{url}/#{path}".gsub('\\', '/')
prefix_utf8 = "#{url}/#{path}".tr('\\', '/')
logger.debug "PREFIX: #{prefix_utf8}"
prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8)
prefix.force_encoding('ASCII-8BIT')
prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8).b
re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
io.each_line do |line|
next unless line =~ re
@ -152,7 +153,7 @@ module Redmine
parsing = $1
elsif line =~ /^ (.*)$/
if parsing == 'message'
revision.message << "#{$1}\n"
revision.message += "#{$1}\n"
else
if $1 =~ /^(.*)\s+(\S+)$/
path_locale = $1.strip
@ -247,36 +248,28 @@ module Redmine
end
def self.branch_conf_path(path)
bcp = nil
return if path.nil?
m = path.match(%r{^(.*[/\\])\.bzr.*$})
if m
bcp = m[1]
else
bcp = path
end
bcp.gsub!(%r{[\/\\]$}, "")
if bcp
bcp = File.join(bcp, ".bzr", "branch", "branch.conf")
end
bcp
bcp = (m ? m[1] : path).gsub(%r{[\/\\]$}, "")
File.join(bcp, ".bzr", "branch", "branch.conf")
end
def append_revisions_only
return @aro if ! @aro.nil?
return @aro unless @aro.nil?
@aro = false
bcp = self.class.branch_conf_path(url)
if bcp && File.exist?(bcp)
begin
f = File::open(bcp, "r")
f = File.open(bcp, "r")
cnt = 0
f.each_line do |line|
l = line.chomp.to_s
if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/
str_aro = $1
if str_aro.upcase == "TRUE"
if str_aro.casecmp("TRUE") == 0
@aro = true
cnt += 1
elsif str_aro.upcase == "FALSE"
elsif str_aro.casecmp("FALSE") == 0
@aro = false
cnt += 1
end
@ -301,7 +294,7 @@ module Redmine
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
end
ret = shellout(
self.class.sq_bin + ' ' +
self.class.sq_bin + ' ' +
full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
&block
)
@ -320,7 +313,7 @@ module Redmine
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
end
ret = shellout(
self.class.sq_bin + ' ' +
self.class.sq_bin + ' ' +
full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
&block
)

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,7 +20,8 @@
module Redmine
module Scm
module Adapters
class CommandFailed < StandardError #:nodoc:
# @private
class CommandFailed < StandardError
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -43,7 +45,7 @@ module Redmine
end
def scm_command_version
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
scm_version = scm_version_from_command_line.b
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
m[2].scan(%r{\d+}).collect(&:to_i)
end
@ -61,7 +63,7 @@ module Redmine
# password -> unnecessary too
def initialize(url, root_url=nil, login=nil, password=nil,
path_encoding=nil)
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
@path_encoding = path_encoding.presence || 'UTF-8'
@url = url
# TODO: better Exception here (IllegalArgumentException)
raise CommandFailed if root_url.blank?
@ -91,7 +93,7 @@ module Redmine
def entries(path=nil, identifier=nil, options={})
logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
path_locale = scm_iconv(@path_encoding, 'UTF-8', path)
path_locale.force_encoding("ASCII-8BIT")
path_locale = path_locale.b
entries = Entries.new
cmd_args = %w|-q rls -e|
cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
@ -159,7 +161,7 @@ module Redmine
cmd_args << path_with_project_utf8
scm_cmd(*cmd_args) do |io|
state = "entry_start"
commit_log = String.new
commit_log = ""
revision = nil
date = nil
author = nil
@ -168,23 +170,23 @@ module Redmine
file_state = nil
branch_map = nil
io.each_line() do |line|
if state != "revision" && /^#{ENDLOG}/ =~ line
commit_log = String.new
if state != "revision" && /^#{ENDLOG}/.match?(line)
commit_log = ""
revision = nil
state = "entry_start"
end
if state == "entry_start"
branch_map = Hash.new
branch_map = {}
if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
entry_path = normalize_cvs_path($1)
entry_name = normalize_path(File.basename($1))
logger.debug("Path #{entry_path} <=> Name #{entry_name}")
elsif /^head: (.+)$/ =~ line
entry_headRev = $1 #unless entry.nil?
elsif /^symbolic names:/ =~ line
state = "symbolic" #unless entry.nil?
elsif /^#{STARTLOG}/ =~ line
commit_log = String.new
entry_headRev = $1
elsif /^symbolic names:/.match?(line)
state = "symbolic"
elsif /^#{STARTLOG}/.match?(line)
commit_log = ""
state = "revision"
end
next
@ -228,7 +230,7 @@ module Redmine
}]
})
end
commit_log = String.new
commit_log = ""
revision = nil
if /^#{ENDLOG}/ =~ line
state = "entry_start"
@ -241,15 +243,14 @@ module Redmine
elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
revision = $1
elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
date = Time.parse($1)
date = Time.parse($1)
line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line)
author_utf8 = /author: ([^;]+)/.match(line_utf8)[1]
author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
file_state = /state: ([^;]+)/.match(line)[1]
# TODO:
# linechanges only available in CVS....
# maybe a feature our SVN implementation.
# I'm sure, they are useful for stats or something else
# TODO: linechanges only available in CVS....
# maybe a feature our SVN implementation.
# I'm sure, they are useful for stats or something else
# linechanges =/lines: \+(\d+) -(\d+)/.match(line)
# unless linechanges.nil?
# version.line_plus = linechanges[1]
@ -259,7 +260,7 @@ module Redmine
# version.line_minus = 0
# end
else
commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
commit_log += line unless line =~ /^\*\*\* empty log message \*\*\*/
end
end
end
@ -338,9 +339,8 @@ module Redmine
# convert a date/time into the CVS-format
def time_to_cvstime(time)
return nil if time.nil?
time = Time.now if (time.kind_of?(String) && time == 'HEAD')
unless time.kind_of? Time
time = Time.now if (time.is_a?(String) && time == 'HEAD')
unless time.is_a?(Time)
time = Time.parse(time)
end
return time_to_cvstime_rlog(time)
@ -399,10 +399,6 @@ module Redmine
parseRevision()
end
def branchPoint
return @base
end
def branchVersion
if isBranchRevision
return @base+"."+@branchid
@ -428,6 +424,7 @@ module Redmine
end
private
def buildRevision(rev)
if rev == 0
if @branchid.nil?
@ -443,7 +440,7 @@ module Redmine
end
# Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
def parseRevision()
def parseRevision
pieces = @complete_rev.split(".")
@revision = pieces.last.to_i
baseSize = 1

View file

@ -1,239 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'redmine/scm/adapters/abstract_adapter'
require 'rexml/document'
module Redmine
module Scm
module Adapters
class DarcsAdapter < AbstractAdapter
# Darcs executable name
DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
class << self
def client_command
@@bin ||= DARCS_BIN
end
def sq_bin
@@sq_bin ||= shell_quote_command
end
def client_version
@@client_version ||= (darcs_binary_version || [])
end
def client_available
!client_version.empty?
end
def darcs_binary_version
darcsversion = darcs_binary_version_from_command_line.dup.force_encoding('ASCII-8BIT')
if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i)
end
end
def darcs_binary_version_from_command_line
shellout("#{sq_bin} --version") { |io| io.read }.to_s
end
end
def initialize(url, root_url=nil, login=nil, password=nil,
path_encoding=nil)
@url = url
@root_url = url
end
def supports_cat?
# cat supported in darcs 2.0.0 and higher
self.class.client_version_above?([2, 0, 0])
end
# Get info about the darcs repository
def info
rev = revisions(nil,nil,nil,{:limit => 1})
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
end
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(path=nil, identifier=nil, options={})
path_prefix = (path.blank? ? '' : "#{path}/")
if path.blank?
path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
end
entries = Entries.new
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
cmd << " #{shell_quote path}"
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
if doc.root.name == 'directory'
doc.elements.each('directory/*') do |element|
next unless ['file', 'directory'].include? element.name
entries << entry_from_xml(element, path_prefix)
end
elsif doc.root.name == 'file'
entries << entry_from_xml(doc.root, path_prefix)
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
entries.compact!
entries.sort_by_name
end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path = '.' if path.blank?
revisions = Revisions.new
cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
cmd << " --last #{options[:limit].to_i}" if options[:limit]
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
doc.elements.each("changelog/patch") do |patch|
message = patch.elements['name'].text
message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
revisions << Revision.new({:identifier => nil,
:author => patch.attributes['author'],
:scmid => patch.attributes['hash'],
:time => Time.parse(patch.attributes['local_date']),
:message => message,
:paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
})
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
revisions
end
def diff(path, identifier_from, identifier_to=nil)
path = '*' if path.blank?
cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
if identifier_to.nil?
cmd << " --match #{shell_quote("hash #{identifier_from}")}"
else
cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
end
cmd << " -u #{shell_quote path}"
diff = []
shellout(cmd) do |io|
io.each_line do |line|
diff << line
end
end
return nil if $? && $?.exitstatus != 0
diff
end
def cat(path, identifier=nil)
cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
cmd << " #{shell_quote path}"
cat = nil
shellout(cmd) do |io|
io.binmode
cat = io.read
end
return nil if $? && $?.exitstatus != 0
cat
end
private
# Returns an Entry from the given XML element
# or nil if the entry was deleted
def entry_from_xml(element, path_prefix)
modified_element = element.elements['modified']
if modified_element.elements['modified_how'].text.match(/removed/)
return nil
end
Entry.new({:name => element.attributes['name'],
:path => path_prefix + element.attributes['name'],
:kind => element.name == 'file' ? 'file' : 'dir',
:size => nil,
:lastrev => Revision.new({
:identifier => nil,
:scmid => modified_element.elements['patch'].attributes['hash']
})
})
end
def get_paths_for_patch(hash)
paths = get_paths_for_patch_raw(hash)
if self.class.client_version_above?([2, 4])
orig_paths = paths
paths = []
add_paths = []
add_paths_name = []
mod_paths = []
other_paths = []
orig_paths.each do |path|
if path[:action] == 'A'
add_paths << path
add_paths_name << path[:path]
elsif path[:action] == 'M'
mod_paths << path
else
other_paths << path
end
end
add_paths_name.each do |add_path|
mod_paths.delete_if { |m| m[:path] == add_path }
end
paths.concat add_paths
paths.concat mod_paths
paths.concat other_paths
end
paths
end
# Retrieve changed paths for a single patch
def get_paths_for_patch_raw(hash)
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
cmd << " --match #{shell_quote("hash #{hash}")} "
paths = []
shellout(cmd) do |io|
begin
# Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
# A root element is added so that REXML doesn't raise an error
doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
doc.elements.each('fake_root/summary/*') do |modif|
paths << {:action => modif.name[0,1].upcase,
:path => "/" + modif.text.chomp.gsub(/^\s*/, '')
}
end
rescue
end
end
paths
rescue CommandFailed
paths
end
end
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# FileSystem adapter
# File written by Paul Rivier, at Demotera.
@ -107,7 +109,7 @@ module Redmine
# Here we do not shell-out, so we do not want quotes.
def target(path=nil)
# Prevent the use of ..
if path and !path.match(/(^|\/)\.\.(\/|$)/)
if path and !/(^|\/)\.\.(\/|$)/.match?(path)
return "#{self.url}#{without_leading_slash(path)}"
end
return self.url

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -47,7 +49,7 @@ module Redmine
end
def scm_command_version
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
scm_version = scm_version_from_command_line.b
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i)
end
@ -68,11 +70,9 @@ module Redmine
end
def info
begin
Info.new(:root_url => url, :lastrev => lastrev('',nil))
rescue
nil
end
Info.new(:root_url => url, :lastrev => lastrev('',nil))
rescue
nil
end
def branches
@ -82,8 +82,8 @@ module Redmine
git_cmd(cmd_args) do |io|
io.each_line do |line|
branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
bran = GitBranch.new(branch_rev[2])
next unless branch_rev
bran = GitBranch.new(scm_iconv('UTF-8', @path_encoding, branch_rev[2]))
bran.revision = branch_rev[3]
bran.scmid = branch_rev[3]
bran.is_default = ( branch_rev[1] == '*' )
@ -100,7 +100,7 @@ module Redmine
@tags = []
cmd_args = %w|tag|
git_cmd(cmd_args) do |io|
@tags = io.readlines.sort!.map{|t| t.strip}
@tags = io.readlines.sort!.map{|t| scm_iconv('UTF-8', @path_encoding, t.strip)}
end
@tags
rescue ScmCommandAborted
@ -109,11 +109,11 @@ module Redmine
def default_branch
bras = self.branches
return nil if bras.nil?
default_bras = bras.select{|x| x.is_default == true}
return default_bras.first.to_s if ! default_bras.empty?
master_bras = bras.select{|x| x.to_s == 'master'}
master_bras.empty? ? bras.first.to_s : 'master'
return unless bras
default_bras = bras.detect{|x| x.is_default == true}
return default_bras.to_s if default_bras
master_bras = bras.detect{|x| x.to_s == 'master'}
master_bras ? 'master' : bras.first.to_s
end
def entry(path=nil, identifier=nil)
@ -136,8 +136,9 @@ module Redmine
p = scm_iconv(@path_encoding, 'UTF-8', path)
entries = Entries.new
cmd_args = %w|ls-tree -l|
cmd_args << "HEAD:#{p}" if identifier.nil?
cmd_args << "#{identifier}:#{p}" if identifier
identifier = 'HEAD' if identifier.nil?
git_identifier = scm_iconv(@path_encoding, 'UTF-8', identifier)
cmd_args << "#{git_identifier}:#{p}"
git_cmd(cmd_args) do |io|
io.each_line do |line|
e = line.chomp.to_s
@ -198,13 +199,19 @@ module Redmine
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
cmd_args << '--no-renames' if self.class.client_version_above?([2, 9])
cmd_args << "--reverse" if options[:reverse]
cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
cmd_args << "-n" << options[:limit].to_i.to_s if options[:limit]
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
revisions = []
if identifier_from || identifier_to
revisions << ""
revisions[0] << "#{identifier_from}.." if identifier_from
revisions[0] << "#{identifier_to}" if identifier_to
revisions << +""
if identifier_from
git_identifier_from = scm_iconv(@path_encoding, 'UTF-8', identifier_from)
revisions[0] << "#{git_identifier_from}.." if identifier_from
end
if identifier_to
git_identifier_to = scm_iconv(@path_encoding, 'UTF-8', identifier_to)
revisions[0] << git_identifier_to.to_s if identifier_to
end
else
unless options[:includes].blank?
revisions += options[:includes]
@ -220,14 +227,15 @@ module Redmine
io.close_write
files=[]
changeset = {}
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
# 0: not parsing desc or files, 1: parsing desc, 2: parsing files
parsing_descr = 0
io.each_line do |line|
if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
key = "commit"
value = $1
parents_str = $2
if (parsing_descr == 1 || parsing_descr == 2)
if [1, 2].include?(parsing_descr)
parsing_descr = 0
revision = Revision.new({
:identifier => changeset[:commit],
@ -260,16 +268,16 @@ module Redmine
end
elsif (parsing_descr == 0) && line.chomp.to_s == ""
parsing_descr = 1
changeset[:description] = ""
elsif (parsing_descr == 1 || parsing_descr == 2) \
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
changeset[:description] = +""
elsif [1, 2].include?(parsing_descr) &&
line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
parsing_descr = 2
fileaction = $1
filepath = $2
p = scm_iconv('UTF-8', @path_encoding, filepath)
files << {:action => fileaction, :path => p}
elsif (parsing_descr == 1 || parsing_descr == 2) \
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
elsif [1, 2].include?(parsing_descr) &&
line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
parsing_descr = 2
fileaction = $1
filepath = $3
@ -333,8 +341,9 @@ module Redmine
def annotate(path, identifier=nil)
identifier = 'HEAD' if identifier.blank?
git_identifier = scm_iconv(@path_encoding, 'UTF-8', identifier)
cmd_args = %w|blame --encoding=UTF-8|
cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
cmd_args << "-p" << git_identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
blame = Annotate.new
content = nil
git_cmd(cmd_args) { |io| io.binmode; content = io.read }
@ -365,11 +374,10 @@ module Redmine
end
def cat(path, identifier=nil)
if identifier.nil?
identifier = 'HEAD'
end
identifier = 'HEAD' if identifier.nil?
git_identifier = scm_iconv(@path_encoding, 'UTF-8', identifier)
cmd_args = %w|show --no-color|
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
cmd_args << "#{git_identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
cat = nil
git_cmd(cmd_args) do |io|
io.binmode

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -56,7 +58,7 @@ module Redmine
# The hg version is expressed either as a
# release number (eg 0.9.5 or 1.0) or as a revision
# id composed of 12 hexa characters.
theversion = hgversion_from_command_line.dup.force_encoding('ASCII-8BIT')
theversion = hgversion_from_command_line.b
if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i)
end
@ -90,7 +92,7 @@ module Redmine
:lastrev => Revision.new(:revision => tip['revision'],
:scmid => tip['node']))
# rescue HgCommandAborted
rescue Exception => e
rescue => e
logger.error "hg: error during getting info: #{e.message}"
nil
end
@ -133,6 +135,7 @@ module Redmine
begin
@summary = parse_xml(output)['rhsummary']
rescue
# do nothing
end
end
end
@ -146,6 +149,7 @@ module Redmine
begin
parse_xml(output)['rhmanifest']['repository']['manifest']
rescue
# do nothing
end
end
path_prefix = path.blank? ? '' : with_trailling_slash(path)
@ -191,6 +195,7 @@ module Redmine
# Mercurial < 1.5 does not support footer template for '</log>'
parse_xml("#{output}</log>")['log']
rescue
# do nothing
end
end
as_ary(log['logentry']).each do |le|
@ -201,19 +206,25 @@ module Redmine
end
cpmap = Hash[*cpalist.flatten]
paths = as_ary(le['paths']['path']).map do |e|
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']))
{:action => e['action'],
:path => with_leading_slash(p),
:from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
:from_revision => (cpmap.member?(p) ? le['node'] : nil)}
end.sort { |a, b| a[:path] <=> b[:path] }
end
paths.sort_by!{|e| e[:path]}
parents_ary = []
as_ary(le['parents']['parent']).map do |par|
parents_ary << par['__content__'] if par['__content__'] != "0000000000000000000000000000000000000000"
end
yield Revision.new(:revision => le['revision'],
:scmid => le['node'],
:author => (le['author']['__content__'] rescue ''),
:author =>
(begin
le['author']['__content__']
rescue
''
end),
:time => Time.parse(le['date']['__content__']),
:message => le['msg']['__content__'],
:paths => paths,
@ -243,7 +254,7 @@ module Redmine
hg_args << '--' << CGI.escape(hgtarget(p))
end
diff = []
hg *hg_args do |io|
hg(*hg_args) do |io|
io.each_line do |line|
diff << line
end
@ -268,8 +279,7 @@ module Redmine
blame = Annotate.new
hg 'rhannotate', '-ncu', "-r#{CGI.escape(hgrev(identifier))}", '--', hgtarget(p) do |io|
io.each_line do |line|
line.force_encoding('ASCII-8BIT')
next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
next unless line.b =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
:identifier => $3)
blame.add_line($4.rstrip, r)
@ -296,10 +306,10 @@ module Redmine
# Runs 'hg' command with the given args
def hg(*args, &block)
# as of hg 4.4.1, early parsing of bool options is not terminated at '--'
if args.any? { |s| s =~ HG_EARLY_BOOL_ARG }
if args.any? { |s| HG_EARLY_BOOL_ARG.match?(s) }
raise HgCommandArgumentError, "malicious command argument detected"
end
if args.take_while { |s| s != '--' }.any? { |s| s =~ HG_EARLY_LIST_ARG }
if args.take_while { |s| s != '--' }.any? { |s| HG_EARLY_LIST_ARG.match?(s) }
raise HgCommandArgumentError, "malicious command argument detected"
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -46,7 +48,7 @@ module Redmine
end
def svn_binary_version
scm_version = scm_version_from_command_line.dup.force_encoding('ASCII-8BIT')
scm_version = scm_version_from_command_line.b
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i)
end
@ -59,7 +61,7 @@ module Redmine
# Get info about the svn repository
def info
cmd = "#{self.class.sq_bin} info --xml #{target}"
cmd = +"#{self.class.sq_bin} info --xml #{target}"
cmd << credentials_string
info = nil
shellout(cmd) do |io|
@ -89,7 +91,7 @@ module Redmine
path ||= ''
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
entries = Entries.new
cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
cmd = +"#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
cmd << credentials_string
shellout(cmd) do |io|
output = io.read.force_encoding('UTF-8')
@ -113,7 +115,7 @@ module Redmine
})
})
end
rescue Exception => e
rescue => e
logger.error("Error parsing svn output: #{e.message}")
logger.error("Output was:\n #{output}")
end
@ -128,7 +130,7 @@ module Redmine
return nil unless self.class.client_version_above?([1, 5, 0])
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
cmd = +"#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
cmd << credentials_string
properties = {}
shellout(cmd) do |io|
@ -136,7 +138,7 @@ module Redmine
begin
doc = parse_xml(output)
each_xml_element(doc['properties']['target'], 'property') do |property|
properties[ property['name'] ] = property['__content__'].to_s
properties[property['name']] = property['__content__'].to_s
end
rescue
end
@ -150,7 +152,7 @@ module Redmine
identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
revisions = Revisions.new
cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
cmd = +"#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
cmd << credentials_string
cmd << " --verbose " if options[:with_paths]
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
@ -168,7 +170,7 @@ module Redmine
:from_revision => path['copyfrom-rev']
}
end if logentry['paths'] && logentry['paths']['path']
paths.sort! { |x,y| x[:path] <=> y[:path] }
paths.sort_by! {|e| e[:path]}
revisions << Revision.new({:identifier => logentry['revision'],
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
@ -190,7 +192,7 @@ module Redmine
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
cmd = "#{self.class.sq_bin} diff -r "
cmd = +"#{self.class.sq_bin} diff -r "
cmd << "#{identifier_to}:"
cmd << "#{identifier_from}"
cmd << " #{target(path)}@#{identifier_from}"
@ -207,7 +209,7 @@ module Redmine
def cat(path, identifier=nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
cmd = +"#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
cmd << credentials_string
cat = nil
shellout(cmd) do |io|
@ -220,7 +222,7 @@ module Redmine
def annotate(path, identifier=nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
cmd = +"#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
cmd << credentials_string
blame = Annotate.new
shellout(cmd) do |io|
@ -242,7 +244,7 @@ module Redmine
private
def credentials_string
str = ''
str = +''
str << " --username #{shell_quote(@login)}" unless @login.blank?
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
str << " --no-auth-cache --non-interactive"
@ -265,7 +267,7 @@ module Redmine
end
def target(path = '')
base = path.match(/^\//) ? root_url : url
base = /^\//.match?(path) ? root_url : url
uri = "#{base}/#{path}"
uri = URI.escape(URI.escape(uri), '[]')
shell_quote(uri.gsub(/[?<>\*]/, ''))

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Redmine
module Scm
class Base

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -60,7 +62,8 @@ module Redmine
# eg. hello "bye bye" => ["hello", "bye bye"]
@tokens = @question.scan(%r{((\s|^)"[^"]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
# tokens must be at least 2 characters long
@tokens = @tokens.uniq.select {|w| w.length > 1 }
# but for Chinese characters (Chinese HANZI/Japanese KANJI), tokens can be one character
@tokens = @tokens.uniq.select {|w| w.length > 1 || w =~ /\p{Han}/ }
# no more than 5 tokens to search for
@tokens.slice! 5..-1
end
@ -82,13 +85,11 @@ module Redmine
# Returns the results for the given offset and limit
def results(offset, limit)
result_ids_to_load = result_ids[offset, limit] || []
results_by_scope = Hash.new {|h,k| h[k] = []}
result_ids_to_load.group_by(&:first).each do |scope, scope_and_ids|
klass = scope.singularize.camelcase.constantize
results_by_scope[scope] += klass.search_results_from_ids(scope_and_ids.map(&:last))
end
result_ids_to_load.map do |scope, id|
results_by_scope[scope].detect {|record| record.id == id}
end.compact
@ -110,7 +111,6 @@ module Redmine
cache_key = ActiveSupport::Cache.expand_cache_key(
[@question, @user.id, @scope.sort, @options, project_ids.sort]
)
Redmine::Search.cache_store.fetch(cache_key, :force => !@cache) do
load_result_ids
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -88,17 +90,17 @@ module Redmine
def normalize!
self.reject! {|s| s.first.blank? }
self.uniq! {|s| s.first }
self.collect! {|s| s = Array(s); [s.first, (s.last == false || s.last.to_s == 'desc') ? 'desc' : 'asc']}
self.slice!(3)
self
self.replace self.first(3)
end
# Appends ASC/DESC to the sort criterion unless it has a fixed order
def append_order(criterion, order)
if criterion =~ / (asc|desc)$/i
if / (asc|desc)$/i.match?(criterion)
criterion
else
"#{criterion} #{order.to_s.upcase}"
Arel.sql "#{criterion} #{order.to_s.upcase}"
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -17,9 +19,9 @@
module Redmine
module SubclassFactory
def self.included(base)
def self.included(base)
base.extend ClassMethods
end
end
module ClassMethods
def get_subclass(class_name)
@ -29,7 +31,7 @@ module Redmine
rescue
# invalid class name
end
unless subclasses.include? klass
unless descendants.include? klass
klass = nil
end
klass

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'active_support/core_ext/object/to_query'
require 'rack/utils'
@ -31,7 +33,7 @@ module Redmine
#
# taken from https://github.com/brianhempel/hash_to_hidden_fields
def hash_to_hidden_fields(hash)
cleaned_hash = hash.reject { |k, v| v.nil? }
cleaned_hash = hash.to_unsafe_h.reject { |k, v| v.nil? }
pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
tags = pairs.map do |pair|
key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -50,44 +52,87 @@ module Redmine
rescue
false
end
def filename_supported?(filename)
if highlighter.respond_to? :filename_supported?
highlighter.filename_supported? filename
else
false
end
end
end
module CodeRay
require 'coderay'
module Rouge
require 'rouge'
def self.retrieve_supported_languages
::CodeRay::Scanners.list +
# Add CodeRay scanner aliases
::CodeRay::Scanners.plugin_hash.keys.map(&:to_sym) -
# Remove internal CodeRay scanners
%w(debug default raydebug scanner).map(&:to_sym)
# Customized formatter based on Rouge::Formatters::HTMLLinewise
# Syntax highlighting is completed within each line.
class CustomHTMLLinewise < ::Rouge::Formatter
def initialize(formatter)
@formatter = formatter
end
def stream(tokens, &b)
token_lines(tokens) do |line|
line.each do |tok, val|
yield @formatter.span(tok, val)
end
yield "\n"
end
end
end
private_class_method :retrieve_supported_languages
SUPPORTED_LANGUAGES = retrieve_supported_languages
class << self
# Highlights +text+ as the content of +filename+
# Should not return line numbers nor outer pre tag
def highlight_by_filename(text, filename)
language = ::CodeRay::FileType[filename]
language ? ::CodeRay.scan(text, language).html(:break_lines => true) : ERB::Util.h(text)
# TODO: Delete the following workaround for #30434 and
# test_syntax_highlight_should_normalize_line_endings in
# application_helper_test.rb when Rouge is improved to
# handle CRLF properly.
# See also: https://github.com/jneen/rouge/pull/1078
text = text.gsub(/\r\n?/, "\n")
lexer =::Rouge::Lexer.guess(:source => text, :filename => filename)
formatter = ::Rouge::Formatters::HTML.new
::Rouge.highlight(text, lexer, CustomHTMLLinewise.new(formatter))
end
# Highlights +text+ using +language+ syntax
# Should not return outer pre tag
def highlight_by_language(text, language)
::CodeRay.scan(text, language).html(:wrap => :span)
lexer =
find_lexer(language.to_s.downcase) || ::Rouge::Lexers::PlainText
::Rouge.highlight(text, lexer, ::Rouge::Formatters::HTML)
end
def language_supported?(language)
SUPPORTED_LANGUAGES.include?(language.to_s.downcase.to_sym)
rescue
false
find_lexer(language.to_s.downcase) ? true : false
end
def filename_supported?(filename)
!::Rouge::Lexer.guesses(:filename => filename).empty?
end
private
# Alias names used by CodeRay and not supported by Rouge
LANG_ALIASES = {
'delphi' => 'pascal',
'cplusplus' => 'cpp',
'ecmascript' => 'javascript',
'ecma_script' => 'javascript',
'java_script' => 'javascript',
'xhtml' => 'html'
}
def find_lexer(language)
::Rouge::Lexer.find(language) ||
::Rouge::Lexer.find(LANG_ALIASES[language])
end
end
end
end
SyntaxHighlighting.highlighter = 'CodeRay'
SyntaxHighlighting.highlighter = 'Rouge'
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -121,7 +123,7 @@ module Redmine
end
@current_theme
end
# Returns the header tags for the current theme
def heads_for_theme
if current_theme && current_theme.javascripts.include?('theme')
@ -130,8 +132,6 @@ module Redmine
end
end
private
def self.scan_themes
dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f|
# A theme should at least override application.css
@ -139,5 +139,6 @@ module Redmine
end
dirs.collect {|dir| Theme.new(dir)}.sort
end
private_class_method :scan_themes
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -23,12 +25,18 @@ module Redmine
extend Redmine::Utils::Shell
CONVERT_BIN = (Redmine::Configuration['imagemagick_convert_command'] || 'convert').freeze
ALLOWED_TYPES = %w(image/bmp image/gif image/jpeg image/png)
ALLOWED_TYPES = %w(image/bmp image/gif image/jpeg image/png application/pdf)
# Generates a thumbnail for the source image to target
def self.generate(source, target, size)
def self.generate(source, target, size, is_pdf = false)
return nil unless convert_available?
return nil if is_pdf && !gs_available?
unless File.exists?(target)
mime_type = File.open(source) {|f| MimeMagic.by_magic(f).try(:type) }
return nil if mime_type.nil?
return nil if !ALLOWED_TYPES.include? mime_type
return nil if is_pdf && mime_type != "application/pdf"
# Make sure we only invoke Imagemagick if the file type is allowed
unless File.open(source) {|f| ALLOWED_TYPES.include? MimeMagic.by_magic(f).try(:type) }
return nil
@ -38,7 +46,12 @@ module Redmine
FileUtils.mkdir_p directory
end
size_option = "#{size}x#{size}>"
cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -thumbnail #{shell_quote size_option} #{shell_quote target}"
if is_pdf
cmd = "#{shell_quote CONVERT_BIN} #{shell_quote "#{source}[0]"} -thumbnail #{shell_quote size_option} #{shell_quote "png:#{target}"}"
else
cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -auto-orient -thumbnail #{shell_quote size_option} #{shell_quote target}"
end
unless system(cmd)
logger.error("Creating thumbnail failed (#{$?}):\nCommand: #{cmd}")
return nil
@ -59,6 +72,22 @@ module Redmine
@convert_available
end
def self.gs_available?
return @gs_available if defined?(@gs_available)
if Redmine::Platform.mswin?
@gs_available = false
else
begin
`gs -version`
@gs_available = $?.success?
rescue
@gs_available = false
end
end
@gs_available
end
def self.logger
Rails.logger
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -49,7 +51,7 @@ module Redmine
# Class that represents a file diff
class DiffTable < Array
attr_reader :file_name
attr_reader :file_name, :previous_file_name
# Initialize with a Diff file and the type of Diff View
# The type view must be inline or sbs (side_by_side)
@ -60,6 +62,7 @@ module Redmine
@type = type
@style = style
@file_name = nil
@previous_file_name = nil
@git_diff = false
end
@ -75,7 +78,7 @@ module Redmine
@parsing = true
end
else
if line =~ %r{^[^\+\-\s@\\]}
if %r{^[^\+\-\s@\\]}.match?(line)
@parsing = false
return false
elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
@ -111,17 +114,21 @@ module Redmine
def file_name=(arg)
both_git_diff = false
if file_name.nil?
@git_diff = true if arg =~ %r{^(a/|/dev/null)}
@git_diff = true if %r{^(a/|/dev/null)}.match?(arg)
else
both_git_diff = (@git_diff && arg =~ %r{^(b/|/dev/null)})
both_git_diff = (@git_diff && %r{^(b/|/dev/null)}.match?(arg))
end
if both_git_diff
if file_name && arg == "/dev/null"
# keep the original file name
@file_name = file_name.sub(%r{^a/}, '')
else
# remove leading a/
@previous_file_name = file_name.sub(%r{^a/}, '') unless file_name == "/dev/null"
# remove leading b/
@file_name = arg.sub(%r{^b/}, '')
@previous_file_name = nil if @previous_file_name == @file_name
end
elsif @style == "Subversion"
# removing trailing "(revision nn)"
@ -161,7 +168,7 @@ module Redmine
true
else
write_offsets
if line[0, 1] =~ /\s/
if /\s/.match?(line[0, 1])
diff = Diff.new
diff.line_right = line[1..-1]
diff.nb_line_right = @line_num_r
@ -196,11 +203,13 @@ module Redmine
if line_left.present? && line_right.present? && line_left != line_right
max = [line_left.size, line_right.size].min
starting = 0
while starting < max && line_left[starting] == line_right[starting]
while starting < max &&
line_left[starting] == line_right[starting]
starting += 1
end
ending = -1
while ending >= -(max - starting) && (line_left[ending] == line_right[ending])
while ending >= -(max - starting) &&
(line_left[ending] == line_right[ending])
ending -= 1
end
unless starting == 0 && ending == -1
@ -220,7 +229,7 @@ module Redmine
attr_accessor :type_diff_left
attr_accessor :offsets
def initialize()
def initialize
self.nb_line_left = ''
self.nb_line_right = ''
self.line_left = ''
@ -267,7 +276,7 @@ module Redmine
def line_to_html_raw(line, offsets)
if offsets
s = ''
s = +''
unless offsets.first == 0
s << CGI.escapeHTML(line[0..offsets.first-1])
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -22,9 +24,11 @@ module Redmine
class << self
# Returns the relative root url of the application
def relative_url_root
ActionController::Base.respond_to?('relative_url_root') ?
ActionController::Base.relative_url_root.to_s :
if ActionController::Base.respond_to?('relative_url_root')
ActionController::Base.relative_url_root.to_s
else
ActionController::Base.config.relative_url_root.to_s
end
end
# Sets the relative root url of the application
@ -128,9 +132,7 @@ module Redmine
def next_working_date(date)
cwday = date.cwday
days = 0
while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
days += 1
end
days += 1 while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
date + days
end

View file

@ -1,10 +1,13 @@
require 'rexml/document'
# frozen_string_literal: true
require 'redmine/scm/adapters/subversion_adapter'
module Redmine
module VERSION #:nodoc:
MAJOR = 3
MINOR = 4
TINY = 13
# @private
module VERSION
MAJOR = 4
MINOR = 1
TINY = 1
# Branch values:
# * official release: nil
@ -17,7 +20,7 @@ module Redmine
if File.directory?(File.join(Rails.root, '.svn'))
begin
path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s)
if `svn info --xml #{path}` =~ /revision="(\d+)"/
if `#{Redmine::Scm::Adapters::SubversionAdapter.client_command} info --xml #{path}` =~ /commit\s+revision="(\d+)"/
return $1.to_i
end
rescue

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,16 +23,20 @@ require 'redmine/views/builders/xml'
module Redmine
module Views
module Builders
def self.for(format, request, response, &block)
builder = case format
when 'xml', :xml; Builders::Xml.new(request, response)
when 'json', :json; Builders::Json.new(request, response)
else; raise "No builder for format #{format}"
end
if block
block.call(builder)
else
builder
class << self
def for(format, request, response, &block)
builder =
case format
when 'xml', :xml then Builders::Xml.new(request, response)
when 'json', :json then Builders::Json.new(request, response)
else
raise "No builder for format #{format}"
end
if block_given?
yield(builder)
else
builder
end
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -35,7 +37,7 @@ module Redmine
json = @struct.first.to_json
if jsonp.present?
json = "#{jsonp}(#{json})"
response.content_type = 'application/javascript'
@response.content_type = 'application/javascript'
end
json
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -21,17 +23,16 @@ module Redmine
module Views
module Builders
class Structure < BlankSlate
attr_accessor :request, :response
def initialize(request, response)
@struct = [{}]
self.request = request
self.response = response
@request = request
@response = response
end
def array(tag, options={}, &block)
@struct << []
block.call(self)
yield(self)
ret = @struct.pop
@struct.last[tag] = ret
@struct.last.merge!(options) if options
@ -68,10 +69,9 @@ module Redmine
end
end
end
if block
if block_given?
@struct << (args.first.is_a?(Hash) ? args.first : {})
block.call(self)
yield(self)
ret = @struct.pop
if @struct.last.is_a?(Array)
@struct.last << ret

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -55,10 +57,10 @@ class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder
def label_for_field(field, options = {})
return ''.html_safe if options.delete(:no_label)
text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label]
text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
text ||= @object.class.human_attribute_name(field)
text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
@template.content_tag("label", text.html_safe,
:class => (@object && @object.errors[field].present? ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
:class => (@object && @object.errors[field].present? ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,7 +21,7 @@ require 'digest/md5'
module Redmine
module WikiFormatting
class StaleSectionError < Exception; end
class StaleSectionError < StandardError; end
@@formatters = {}
@ -32,13 +34,13 @@ module Redmine
options = args.last.is_a?(Hash) ? args.pop : {}
name = name.to_s
raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
formatter, helper, parser = args.any? ?
args :
%w(Formatter Helper HtmlParser).map {|m| "Redmine::WikiFormatting::#{name.classify}::#{m}".constantize rescue nil}
formatter, helper, parser =
if args.any?
args
else
%w(Formatter Helper HtmlParser).map {|m| "Redmine::WikiFormatting::#{name.classify}::#{m}".constantize rescue nil}
end
raise "A formatter class is required" if formatter.nil?
@@formatters[name] = {
:formatter => formatter,
:helper => helper,
@ -79,15 +81,17 @@ module Redmine
end
def to_html(format, text, options = {})
text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute])
# Text retrieved from the cache store may be frozen
# We need to dup it so we can do in-place substitutions with gsub!
cache_store.fetch cache_key do
text =
if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store &&
cache_key = cache_key_for(format, text, options[:object], options[:attribute])
# Text retrieved from the cache store may be frozen
# We need to dup it so we can do in-place substitutions with gsub!
cache_store.fetch cache_key do
formatter_for(format).new(text).to_html
end.dup
else
formatter_for(format).new(text).to_html
end.dup
else
formatter_for(format).new(text).to_html
end
end
text
end
@ -125,7 +129,7 @@ module Redmine
([^<]\S*?) # url
(\/)? # slash
)
((?:&gt;)?|[^[:alnum:]_\=\/;\(\)]*?) # post
((?:&gt;)?|[^[:alnum:]_\=\/;\(\)\-]*?) # post
(?=<|\s|$)
}x unless const_defined?(:AUTO_LINK_RE)
@ -133,16 +137,16 @@ module Redmine
def auto_link!(text)
text.gsub!(AUTO_LINK_RE) do
all, leading, proto, url, post = $&, $1, $2, $3, $6
if leading =~ /<a\s/i || leading =~ /![<>=]?/
if /<a\s/i.match?(leading) || /![<>=]?/.match?(leading)
# don't replace URLs that are already linked
# and URLs prefixed with ! !> !< != (textile images)
all
else
# Idea below : an URL with unbalanced parenthesis and
# ending by ')' is put into external parenthesis
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
url=url[0..-2] # discard closing parenthesis from url
post = ")"+post # add closing parenthesis to post
if url[-1] == ")" and ((url.count("(") - url.count(")")) < 0)
url = url[0..-2] # discard closing parenthesis from url
post = ")" + post # add closing parenthesis to post
end
content = proto + url
href = "#{proto=="www."?"http://www.":proto}#{url}"
@ -153,15 +157,43 @@ module Redmine
# Destructively replaces email addresses into clickable links
def auto_mailto!(text)
text.gsub!(/((?<!@)\b[\w\.!#\$%\-+.\/]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
text.gsub!(/([\w\.!#\$%\-+.\/]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
mail = $1
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
if /<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/.match?(text)
mail
else
%(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
end
end
end
def restore_redmine_links(html)
# restore wiki links eg. [[Foo]]
html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
"[[#{$2}]]"
end
# restore Redmine links with double-quotes, eg. version:"1.0"
html.gsub!(/(\w):&quot;(.+?)&quot;/) do
"#{$1}:\"#{$2}\""
end
# restore user links with @ in login name eg. [@jsmith@somenet.foo]
html.gsub!(%r{[@\A]<a(\sclass="email")? href="mailto:(.*?)">(.*?)</a>}) do
"@#{$2}"
end
# restore user links with @ in login name eg. [user:jsmith@somenet.foo]
html.gsub!(%r{\buser:<a(\sclass="email")? href="mailto:(.*?)">(.*?)<\/a>}) do
"user:#{$2}"
end
# restore attachments links with @ in file name eg. [attachment:image@2x.png]
html.gsub!(%r{\battachment:<a(\sclass="email")? href="mailto:(.*?)">(.*?)</a>}) do
"attachment:#{$2}"
end
# restore hires images which are misrecognized as email address eg. [printscreen@2x.png]
html.gsub!(%r{<a(\sclass="email")? href="mailto:[^"]+@\dx\.(bmp|gif|jpg|jpe|jpeg|png)">(.*?)</a>}) do
"#{$3}"
end
html
end
end
# Default formatter module
@ -180,12 +212,13 @@ module Redmine
t = CGI::escapeHTML(@text)
auto_link!(t)
auto_mailto!(t)
restore_redmine_links(t)
simple_format(t, {}, :sanitize => false)
end
end
module Helper
def wikitoolbar_for(field_id)
def wikitoolbar_for(field_id, preview_url = preview_text_path)
end
def heads_for_wiki_formatter

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -28,13 +30,13 @@ module Redmine
}
def self.to_text(html)
html = html.gsub(/[\n\r]/, '').squeeze(' ')
html = html.gsub(/[\n\r]/, ' ')
doc = Loofah.document(html)
doc.scrub!(WikiTags.new(tags))
doc.scrub!(:newline_block_elements)
Loofah::Helpers.remove_extraneous_whitespace(doc.text).strip
Loofah.remove_extraneous_whitespace(doc.text(:encode_special_chars => false)).strip.squeeze(' ').gsub(/^ +/, '')
end
class WikiTags < ::Loofah::Scrubber
@ -42,7 +44,7 @@ module Redmine
@direction = :bottom_up
@tags_to_text = tags_to_text || {}
end
def scrub(node)
formatting = @tags_to_text[node.name]
case formatting
@ -52,6 +54,9 @@ module Redmine
when String
node.add_next_sibling Nokogiri::XML::Text.new(formatting, node.document)
node.remove
when Proc
node.add_next_sibling formatting.call(node)
node.remove
else
CONTINUE
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -24,10 +26,16 @@ module Redmine
Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
end
def exec_macro(name, obj, args, text)
def exec_macro(name, obj, args, text, options={})
macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
return unless macro_options
if options[:inline_attachments] == false
Redmine::WikiFormatting::Macros.inline_attachments = false
else
Redmine::WikiFormatting::Macros.inline_attachments = true
end
method_name = "macro_#{name}"
unless macro_options[:parse_args] == false
args = args.split(',').map(&:strip)
@ -57,7 +65,9 @@ module Redmine
end
@@available_macros = {}
@@inline_attachments = true
mattr_accessor :available_macros
mattr_accessor :inline_attachments
class << self
# Plugins can use this method to define new macros:
@ -67,7 +77,7 @@ module Redmine
# macro :my_macro do |obj, args|
# "My macro output"
# end
#
#
# desc "This is my macro that accepts a block of text"
# macro :my_macro do |obj, args, text|
# "My macro output"
@ -81,7 +91,7 @@ module Redmine
#
# Options:
# * :desc - A description of the macro
# * :parse_args => false - Disables arguments parsing (the whole arguments
# * :parse_args => false - Disables arguments parsing (the whole arguments
# string is passed to the macro)
#
# Macro blocks accept 2 or 3 arguments:
@ -89,7 +99,7 @@ module Redmine
# * args: macro arguments
# * text: the block of text given to the macro (should be present only if the
# macro accepts a block of text). text is a String or nil if the macro is
# invoked without a block of text.
# invoked without a block of text.
#
# Examples:
# By default, when the macro is invoked, the comma separated list of arguments
@ -141,7 +151,7 @@ module Redmine
# If a block of text is given, the closing tag }} must be at the start of a new line.
def macro(name, options={}, &block)
options.assert_valid_keys(:desc, :parse_args)
unless name.to_s.match(/\A\w+\z/)
unless /\A\w+\z/.match?(name.to_s)
raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
end
unless block_given?
@ -162,7 +172,7 @@ module Redmine
# Builtin macros
desc "Sample macro."
macro :hello_world do |obj, args, text|
h("Hello world! Object: #{obj.class.name}, " +
h("Hello world! Object: #{obj.class.name}, " +
(args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
" and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
)
@ -209,7 +219,7 @@ module Redmine
@included_wiki_pages ||= []
raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.id)
@included_wiki_pages << page.id
out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false, :inline_attachments => @@inline_attachments)
@included_wiki_pages.pop
out
end
@ -223,13 +233,14 @@ module Redmine
hide_label = args[1] || args[0] || l(:button_hide)
js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
out = ''.html_safe
out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
out << content_tag('div', textilizable(text, :object => obj, :headings => false), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible')
out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'icon icon-expended collapsible', :style => 'display:none;')
out << content_tag('div', textilizable(text, :object => obj, :headings => false, :inline_attachments => @@inline_attachments), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
out
end
desc "Displays a clickable thumbnail of an attached image. Examples:\n\n" +
desc "Displays a clickable thumbnail of an attached image.\n" +
"Default size is 200 pixels. Examples:\n\n" +
"{{thumbnail(image.png)}}\n" +
"{{thumbnail(image.png, size=300, title=Thumbnail)}} -- with custom title and size"
macro :thumbnail do |obj, args|
@ -237,9 +248,9 @@ module Redmine
filename = args.first
raise 'Filename required' unless filename.present?
size = options[:size]
raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
raise 'Invalid size parameter' unless size.nil? || /^\d+$/.match?(size)
size = size.to_i
size = nil unless size > 0
size = 200 unless size > 0
if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
title = options[:title] || attachment.title
thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => @only_path)
@ -251,6 +262,33 @@ module Redmine
raise "Attachment #{filename} not found"
end
end
desc "Displays an issue link including additional information. Examples:\n\n" +
"{{issue(123)}} -- Issue #123: Enhance macro capabilities\n" +
"{{issue(123, project=true)}} -- Andromeda - Issue #123: Enhance macro capabilities\n" +
"{{issue(123, tracker=false)}} -- #123: Enhance macro capabilities\n" +
"{{issue(123, subject=false, project=true)}} -- Andromeda - Issue #123\n"
macro :issue do |obj, args|
args, options = extract_macro_options(args, :project, :subject, :tracker)
id = args.first
issue = Issue.visible.find_by(id: id)
if issue
# remove invalid options
options.delete_if { |k,v| v != 'true' && v != 'false' }
# turn string values into boolean
options.each do |k, v|
options[k] = v == 'true'
end
link_to_issue(issue, options)
else
# Fall back to regular issue link format to indicate, that there
# should have been something.
"##{id}"
end
end
end
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -52,24 +54,16 @@ module Redmine
end
class Formatter
include Redmine::WikiFormatting::LinksHelper
alias :inline_restore_redmine_links :restore_redmine_links
def initialize(text)
@text = text
end
def to_html(*args)
html = formatter.render(@text)
# restore wiki links eg. [[Foo]]
html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
"[[#{$2}]]"
end
# restore Redmine links with double-quotes, eg. version:"1.0"
html.gsub!(/(\w):&quot;(.+?)&quot;/) do
"#{$1}:\"#{$2}\""
end
# restore user links with @ in login name eg. [@jsmith@somenet.foo]
html.gsub!(%r{[@\A]<a href="mailto:(.*?)">(.*?)</a>}) do
"@#{$2}"
end
html = inline_restore_redmine_links(html)
html
end
@ -89,14 +83,14 @@ module Redmine
end
def extract_sections(index)
sections = ['', '', '']
sections = [+'', +'', +'']
offset = 0
i = 0
l = 1
inside_pre = false
@text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|(?:~~~|```).*)\s*$)/).each do |part|
level = nil
if part =~ /\A(~{3,}|`{3,})(\S+)?\s*$/
if part =~ /\A(~{3,}|`{3,})(\s*\S+)?\s*$/
if !inside_pre
inside_pre = true
elsif !$2
@ -141,7 +135,8 @@ module Redmine
:superscript => true,
:no_intra_emphasis => true,
:footnotes => true,
:lax_spacing => true
:lax_spacing => true,
:underline => true
)
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,10 +21,10 @@ module Redmine
module WikiFormatting
module Markdown
module Helper
def wikitoolbar_for(field_id)
def wikitoolbar_for(field_id, preview_url = preview_text_path)
heads_for_wiki_formatter
url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax_markdown.html"
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();")
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.setPreviewUrl('#{escape_javascript preview_url}'); wikiToolbar.draw();")
end
def initial_page_content(page)

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -23,15 +25,27 @@ module Redmine
self.tags = tags.merge(
'b' => {:pre => '**', :post => '**'},
'strong' => {:pre => '**', :post => '**'},
'i' => {:pre => '_', :post => '_'},
'em' => {:pre => '_', :post => '_'},
'i' => {:pre => '*', :post => '*'},
'em' => {:pre => '*', :post => '*'},
'u' => {:pre => '_', :post => '_'},
'strike' => {:pre => '~~', :post => '~~'},
'h1' => {:pre => "\n\n# ", :post => "\n\n"},
'h2' => {:pre => "\n\n## ", :post => "\n\n"},
'h3' => {:pre => "\n\n### ", :post => "\n\n"},
'h4' => {:pre => "\n\n#### ", :post => "\n\n"},
'h5' => {:pre => "\n\n##### ", :post => "\n\n"},
'h6' => {:pre => "\n\n###### ", :post => "\n\n"}
'h6' => {:pre => "\n\n###### ", :post => "\n\n"},
'th' => {:pre => '*', :post => "*\n"},
'td' => {:pre => '', :post => "\n"},
'a' => lambda do |node|
if node.content.present? && node.attributes.key?('href')
%| [#{node.content}](#{node.attributes['href'].value}) |
elsif node.attributes.key?('href')
%| #{node.attributes['href'].value} |
else
node.content
end
end
)
end
end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -27,9 +29,10 @@ module Redmine
alias :inline_auto_link :auto_link!
alias :inline_auto_mailto :auto_mailto!
alias :inline_restore_redmine_links :restore_redmine_links
# auto_link rule after textile rules so that it doesn't break !image_url! tags
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_restore_redmine_links]
def initialize(*args)
super
@ -62,9 +65,9 @@ module Redmine
@pre_list = []
text = self.dup
rip_offtags text, false, false
before = ''
s = ''
after = ''
before = +''
s = +''
after = +''
i = 0
l = 1
started = false
@ -105,7 +108,7 @@ module Redmine
sections
end
private
private
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
@ -125,6 +128,7 @@ module Redmine
language = $1 || $2
text = $3
if Redmine::SyntaxHighlighting.language_supported?(language)
text.gsub!(/x%x%/, '&')
content = "<code class=\"#{language} syntaxhl\">" +
Redmine::SyntaxHighlighting.highlight_by_language(text, language)
else

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -19,11 +21,11 @@ module Redmine
module WikiFormatting
module Textile
module Helper
def wikitoolbar_for(field_id)
def wikitoolbar_for(field_id, preview_url = preview_text_path)
heads_for_wiki_formatter
# Is there a simple way to link to a public resource?
url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax_textile.html"
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();")
javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.setPreviewUrl('#{escape_javascript preview_url}'); wikiToolbar.draw();")
end
def initial_page_content(page)
@ -33,7 +35,8 @@ module Redmine
def heads_for_wiki_formatter
unless @heads_for_wiki_formatter_included
content_for :header_tags do
javascript_include_tag('jstoolbar/jstoolbar-textile.min') +
javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag('jstoolbar/textile') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
javascript_tag("var wikiImageMimeTypes = #{Redmine::MimeType.by_type('image').to_json};") +
stylesheet_link_tag('jstoolbar')

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -32,7 +34,18 @@ module Redmine
'h3' => {:pre => "\n\nh3. ", :post => "\n\n"},
'h4' => {:pre => "\n\nh4. ", :post => "\n\n"},
'h5' => {:pre => "\n\nh5. ", :post => "\n\n"},
'h6' => {:pre => "\n\nh6. ", :post => "\n\n"}
'h6' => {:pre => "\n\nh6. ", :post => "\n\n"},
'th' => {:pre => '*', :post => "*\n"},
'td' => {:pre => '', :post => "\n"},
'a' => lambda do |node|
if node.content.present? && node.attributes.key?('href')
%| "#{node.content}":#{node.attributes['href'].value} |
elsif node.attributes.key?('href')
%| #{node.attributes['href'].value} |
else
node.content
end
end
)
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# vim:ts=4:sw=4:
# = RedCloth - Textile and Markdown Hybrid for Ruby
#
@ -71,33 +73,33 @@
#
# == Links
#
# To make a hypertext link, put the link text in "quotation
# To make a hypertext link, put the link text in "quotation
# marks" followed immediately by a colon and the URL of the link.
#
# Optional: text in (parentheses) following the link text,
# but before the closing quotation mark, will become a Title
#
# Optional: text in (parentheses) following the link text,
# but before the closing quotation mark, will become a Title
# attribute for the link, visible as a tool tip when a cursor is above it.
#
#
# Example:
#
# "This is a link (This is a title) ":http://www.textism.com
#
#
# Will become:
#
#
# <a href="http://www.textism.com" title="This is a title">This is a link</a>
#
# == Images
#
# To insert an image, put the URL for the image inside exclamation marks.
#
# Optional: text that immediately follows the URL in (parentheses) will
# be used as the Alt text for the image. Images on the web should always
# have descriptive Alt text for the benefit of readers using non-graphical
# Optional: text that immediately follows the URL in (parentheses) will
# be used as the Alt text for the image. Images on the web should always
# have descriptive Alt text for the benefit of readers using non-graphical
# browsers.
#
# Optional: place a colon followed by a URL immediately after the
# Optional: place a colon followed by a URL immediately after the
# closing ! to make the image into a link.
#
#
# Example:
#
# !http://www.textism.com/common/textist.gif(Textist)!
@ -116,13 +118,13 @@
#
# == Defining Acronyms
#
# HTML allows authors to define acronyms via the tag. The definition appears as a
# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
# HTML allows authors to define acronyms via the tag. The definition appears as a
# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
# this should be used at least once for each acronym in documents where they appear.
#
# To quickly define an acronym in Textile, place the full text in (parentheses)
# To quickly define an acronym in Textile, place the full text in (parentheses)
# immediately following the acronym.
#
#
# Example:
#
# ACLU(American Civil Liberties Union)
@ -145,7 +147,7 @@
# (background:#ddd;color:red). |{}| | | |
#
# == Using RedCloth
#
#
# RedCloth is simply an extension of the String class, which can handle
# Textile formatting. Use it like a String and output HTML with its
# RedCloth#to_html method.
@ -253,7 +255,7 @@ class RedCloth3 < String
# #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
#
def initialize( string, restrictions = [] )
restrictions.each { |r| method( "#{ r }=" ).call( true ) }
restrictions.each { |r| method( "#{r}=" ).call( true ) }
super( string )
end
@ -268,14 +270,14 @@ class RedCloth3 < String
rules = DEFAULT_RULES if rules.empty?
# make our working copy
text = self.dup
@urlrefs = {}
@shelf = []
textile_rules = [:block_textile_table, :block_textile_lists,
:block_textile_prefix, :inline_textile_image, :inline_textile_link,
:inline_textile_code, :inline_textile_span, :glyphs_textile]
:block_textile_prefix, :inline_textile_image, :inline_textile_code,
:inline_textile_span, :inline_textile_link, :glyphs_textile]
markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
:block_markdown_bq, :block_markdown_lists,
:block_markdown_bq, :block_markdown_lists,
:inline_markdown_reflink, :inline_markdown_link]
@rules = rules.collect do |rule|
case rule
@ -289,8 +291,8 @@ class RedCloth3 < String
end.flatten
# standard clean up
incoming_entities text
clean_white_space text
incoming_entities text
clean_white_space text
# start processor
@pre_list = []
@ -299,7 +301,7 @@ class RedCloth3 < String
escape_html_tags text
# need to do this before #hard_break and #blocks
block_textile_quotes text unless @lite_mode
hard_break text
hard_break text
unless @lite_mode
refs text
blocks text
@ -314,28 +316,23 @@ class RedCloth3 < String
clean_html text if filter_html
text.strip!
text
end
#######
private
#######
private
#
# Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
# (from PyTextile)
#
TEXTILE_TAGS =
[[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
[134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
[140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
[147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
[153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
collect! do |a, b|
[a.chr, ( b.zero? and "" or "&#{ b };" )]
end
[[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
[134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
[140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
[147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
[153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
collect! do |a, b|
[a.chr, ( b.zero? and "" or "&#{b};" )]
end
#
# Regular expressions to convert to HTML.
#
@ -357,7 +354,7 @@ class RedCloth3 < String
# Text markup tags, don't conflict with block tags
SIMPLE_HTML_TAGS = [
'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
]
@ -373,14 +370,14 @@ class RedCloth3 < String
['+', 'ins', :limit],
['^', 'sup', :limit],
['~', 'sub', :limit]
]
]
QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
QTAGS.collect! do |rc, ht, rtype|
rcq = Regexp::quote rc
re =
case rtype
when :limit
case rtype
when :limit
/(^|[>\s\(]) # sta
(?!\-\-)
(#{QTAGS_JOIN}|) # oqs
@ -390,38 +387,38 @@ class RedCloth3 < String
#{rcq}
(#{QTAGS_JOIN}|) # oqa
(?=[[:punct:]]|<|\s|\)|$)/x
else
else
/(#{rcq})
(#{C})
(?::(\S+))?
([[:word:]]|[^\s\-].*?[^\s\-])
#{rcq}/xm
end
#{rcq}/xm
end
[rc, ht, re, rtype]
end
# Elements to handle
GLYPHS = [
# [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
# [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
# [ /\'/, '&#8216;' ], # single opening
# [ /</, '&lt;' ], # less-than
# [ />/, '&gt;' ], # greater-than
# [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
# [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
# [ /"/, '&#8220;' ], # double opening
# [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
# [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
# [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
# [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
# [ /\s->\s/, ' &rarr; ' ], # right arrow
# [ /\s-\s/, ' &#8211; ' ], # en dash
# [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
# [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
# [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
# [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
# [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
# [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
# [ /\'/, '&#8216;' ], # single opening
# [ /</, '&lt;' ], # less-than
# [ />/, '&gt;' ], # greater-than
# [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
# [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
# [ /"/, '&#8220;' ], # double opening
# [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
# [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
# [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
# [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
# [ /\s->\s/, ' &rarr; ' ], # right arrow
# [ /\s-\s/, ' &#8211; ' ], # en dash
# [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
# [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
# [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
# [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
]
H_ALGN_VALS = {
@ -453,10 +450,10 @@ class RedCloth3 < String
# Search and replace for Textile glyphs (quotes, dashes, other symbols)
def pgl( text )
#GLYPHS.each do |re, resub, tog|
# GLYPHS.each do |re, resub, tog|
# next if tog and method( tog ).call
# text.gsub! re, resub
#end
# end
text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
"<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
end
@ -464,20 +461,19 @@ class RedCloth3 < String
# Parses Textile attribute lists and builds an HTML attribute string
def pba( text_in, element = "" )
return '' unless text_in
return +'' unless text_in
style = []
text = text_in.dup
if element == 'td'
colspan = $1 if text =~ /\\(\d+)/
rowspan = $1 if text =~ /\/(\d+)/
style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
style << "vertical-align:#{v_align($&)};" if text =~ A_VLGN
end
if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
sanitized = sanitize_styles($1)
style << "#{ sanitized };" unless sanitized.blank?
style << "#{sanitized};" unless sanitized.blank?
end
lang = $1 if
@ -485,13 +481,13 @@ class RedCloth3 < String
cls = $1 if
text.sub!( /\(([^()]+?)\)/, '' )
style << "padding-left:#{ $1.length }em;" if
style << "padding-left:#{$1.length}em;" if
text.sub!( /([(]+)/, '' )
style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
style << "padding-right:#{$1.length}em;" if text.sub!( /([)]+)/, '' )
style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
style << "text-align:#{h_align($&)};" if text =~ A_HLGN
cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
@ -503,18 +499,18 @@ class RedCloth3 < String
id = id.starts_with?('wiki-id-') ? id : "wiki-id-#{id}" if id
atts = ''
atts << " style=\"#{ style.join }\"" unless style.empty?
atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
atts << " lang=\"#{ lang }\"" if lang
atts << " id=\"#{ id }\"" if id
atts << " colspan=\"#{ colspan }\"" if colspan
atts << " rowspan=\"#{ rowspan }\"" if rowspan
atts = +''
atts << " style=\"#{style.join}\"" unless style.empty?
atts << " class=\"#{cls}\"" unless cls.to_s.empty?
atts << " lang=\"#{lang}\"" if lang
atts << " id=\"#{id}\"" if id
atts << " colspan=\"#{colspan}\"" if colspan
atts << " rowspan=\"#{rowspan}\"" if rowspan
atts
end
STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
STYLES_RE = /^(color|(min-|max-)?+(width|height)|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
def sanitize_styles(str)
styles = str.split(";").map(&:strip)
@ -525,11 +521,10 @@ class RedCloth3 < String
end
TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
# Parses a Textile table block, building HTML from the result.
def block_textile_table( text )
text.gsub!( TABLE_RE ) do |matches|
# Parses a Textile table block, building HTML from the result.
def block_textile_table( text )
text.gsub!( TABLE_RE ) do |matches|
tatts, fullrow = $~[1..2]
tatts = pba( tatts, 'table' )
tatts = shelve( tatts ) if tatts
@ -538,7 +533,7 @@ class RedCloth3 < String
fullrow.each_line do |row|
ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
cells = []
# the regexp prevents wiki links with a | from being cut as cells
# the regexp prevents wiki links with a | from being cut as cells
row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
ctyp = 'd'
ctyp = 'h' if modifiers && modifiers =~ /^_/
@ -547,12 +542,12 @@ class RedCloth3 < String
catts = pba( modifiers, 'td' ) if modifiers
catts = shelve( catts ) if catts
cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
cells << "\t\t\t<t#{ctyp}#{catts}>#{cell}</t#{ctyp}>"
end
ratts = shelve( ratts ) if ratts
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
rows << "\t\t<tr#{ratts}>\n#{cells.join("\n")}\n\t\t</tr>"
end
"\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
"\t<table#{tatts}>\n#{rows.join("\n")}\n\t</table>\n\n"
end
end
@ -560,19 +555,19 @@ class RedCloth3 < String
LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
# Parses Textile lists and generates HTML
def block_textile_lists( text )
def block_textile_lists( text )
text.gsub!( LISTS_RE ) do |match|
lines = match.split( /\n/ )
last_line = -1
depth = []
lines.each_with_index do |line, line_id|
if line =~ LISTS_CONTENT_RE
if line =~ LISTS_CONTENT_RE
tl,atts,content = $~[1..3]
if depth.last
if depth.last.length > tl.length
(depth.length - 1).downto(0) do |i|
break if depth[i].length == tl.length
lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
lines[line_id - 1] << "</li>\n\t</#{lT(depth[i])}l>\n\t"
depth.pop
end
end
@ -580,39 +575,38 @@ class RedCloth3 < String
lines[line_id - 1] << '</li>'
end
end
unless depth.last == tl
if depth.last != tl
depth << tl
atts = pba( atts )
atts = shelve( atts ) if atts
lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
lines[line_id] = +"\t<#{lT(tl)}l#{atts}>\n\t<li>#{content}"
else
lines[line_id] = "\t\t<li>#{ content }"
lines[line_id] = +"\t\t<li>#{content}"
end
last_line = line_id
else
last_line = line_id
end
if line_id - last_line > 1 or line_id == lines.length - 1
while v = depth.pop
lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
lines[last_line] << "</li>\n\t</#{lT(v)}l>"
end
end
end
lines.join( "\n" )
end
end
QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
def block_textile_quotes( text )
text.gsub!( QUOTES_RE ) do |match|
lines = match.split( /\n/ )
quotes = ''
quotes = +''
indent = 0
lines.each do |line|
line =~ QUOTES_CONTENT_RE
line =~ QUOTES_CONTENT_RE
bq,content = $1, $2
l = bq.count('>')
if l != indent
@ -633,15 +627,15 @@ class RedCloth3 < String
@
(?=\W)/x
def inline_textile_code( text )
def inline_textile_code( text )
text.gsub!( CODE_RE ) do |m|
before,lang,code,after = $~[1..4]
lang = " lang=\"#{ lang }\"" if lang
rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
lang = " lang=\"#{lang}\"" if lang
rip_offtags( +"#{before}<code#{lang}>#{code}</code>#{after}", false )
end
end
def lT( text )
def lT( text )
text =~ /\#$/ ? 'o' : 'u'
end
@ -676,35 +670,34 @@ class RedCloth3 < String
end
end
block_applied = 0
block_applied = 0
@rules.each do |rule_name|
block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
block_applied += 1 if rule_name.to_s.match /^block_/ and method(rule_name).call(blk)
end
if block_applied.zero?
if deep_code
blk = "\t<pre><code>#{ blk }</code></pre>"
blk = "\t<pre><code>#{blk}</code></pre>"
else
blk = "\t<p>#{ blk }</p>"
blk = "\t<p>#{blk}</p>"
end
end
# hard_break blk
blk + "\n#{ code_blk }"
blk + "\n#{code_blk}"
end
end
end.join( "\n\n" ) )
end
def textile_bq( tag, atts, cite, content )
cite, cite_title = check_refs( cite )
cite = " cite=\"#{ cite }\"" if cite
cite = " cite=\"#{cite}\"" if cite
atts = shelve( atts ) if atts
"\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
"\t<blockquote#{cite}>\n\t\t<p#{atts}>#{content}</p>\n\t</blockquote>"
end
def textile_p( tag, atts, cite, content )
atts = shelve( atts ) if atts
"\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
"\t<#{tag}#{atts}>#{content}</#{tag}>"
end
alias textile_h1 textile_p
@ -715,35 +708,35 @@ class RedCloth3 < String
alias textile_h6 textile_p
def textile_fn_( tag, num, atts, cite, content )
atts << " id=\"fn#{ num }\" class=\"footnote\""
content = "<sup>#{ num }</sup> #{ content }"
atts << " id=\"fn#{num}\" class=\"footnote\""
content = "<sup>#{num}</sup> #{content}"
atts = shelve( atts ) if atts
"\t<p#{ atts }>#{ content }</p>"
"\t<p#{atts}>#{content}</p>"
end
BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
def block_textile_prefix( text )
def block_textile_prefix( text )
if text =~ BLOCK_RE
tag,tagpre,num,atts,cite,content = $~[1..6]
atts = pba( atts )
# pass to prefix handler
replacement = nil
if respond_to? "textile_#{ tag }", true
replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
elsif respond_to? "textile_#{ tagpre }_", true
replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
if respond_to? "textile_#{tag}", true
replacement = method( "textile_#{tag}" ).call( tag, atts, cite, content )
elsif respond_to? "textile_#{tagpre}_", true
replacement = method( "textile_#{tagpre}_" ).call( tagpre, num, atts, cite, content )
end
text.gsub!( $& ) { replacement } if replacement
end
end
SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
def block_markdown_setext( text )
if text =~ SETEXT_RE
tag = if $2 == "="; "h1"; else; "h2"; end
blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
tag = ($2 == "=" ? "h1" : "h2")
blk, cont = "<#{tag}>#{$1}</#{tag}>", $'
blocks cont
text.replace( blk + cont )
end
@ -757,8 +750,8 @@ class RedCloth3 < String
$/x
def block_markdown_atx( text )
if text =~ ATX_RE
tag = "h#{ $1.length }"
blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
tag = "h#{$1.length}"
blk, cont = "<#{tag}>#{$2}</#{tag}>\n\n", $'
blocks cont
text.replace( blk + cont )
end
@ -772,7 +765,7 @@ class RedCloth3 < String
flush_left blk
blocks blk
blk.gsub!( /^(\S)/, "\t\\1" )
"<blockquote>\n#{ blk }\n</blockquote>\n\n"
"<blockquote>\n#{blk}\n</blockquote>\n\n"
end
end
@ -790,10 +783,9 @@ class RedCloth3 < String
def block_markdown_lists( text )
end
def inline_textile_span( text )
def inline_textile_span( text )
QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
text.gsub!( qtag_re ) do |m|
case rtype
when :limit
sta,oqs,qtag,content,oqa = $~[1..6]
@ -808,8 +800,7 @@ class RedCloth3 < String
atts = pba( atts )
atts = shelve( atts ) if atts
"#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
"#{sta}#{oqs}<#{ht}#{atts}>#{content}</#{ht}>#{oqa}"
end
end
end
@ -826,36 +817,37 @@ class RedCloth3 < String
( # $url
(\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
[[:alnum:]_\/]\S+?
)
)
(\/)? # $slash
([^[:alnum:]_\=\/;\(\)]*?) # $post
([^[:alnum:]_\=\/;\(\)\-]*?) # $post
)
(?=<|\s|$)
/x
#"
def inline_textile_link( text )
/x
def inline_textile_link( text )
text.gsub!( LINK_RE ) do |m|
all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
if text.include?('<br />')
all
else
url, url_title = check_refs( url )
url, url_title = check_refs(url)
title ||= url_title
# Idea below : an URL with unbalanced parethesis and
# ending by ')' is put into external parenthesis
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
url=url[0..-2] # discard closing parenth from url
post = ")"+post # add closing parenth to post
if url[-1] == ")" and ((url.count("(") - url.count(")")) < 0)
url = url[0..-2] # discard closing parenth from url
post = ")" + post # add closing parenth to post
end
atts = pba( atts )
atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
atts << " title=\"#{ htmlesc title }\"" if title
atts = shelve( atts ) if atts
url = htmlesc(url.dup)
next all if url.downcase.start_with?('javascript:')
atts = pba(atts)
atts = +" href=\"#{url}#{slash}\"#{atts}"
atts << " title=\"#{htmlesc title}\"" if title
atts = shelve(atts) if atts
external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
"#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
"#{pre}<a#{atts}#{external}>#{text}</a>#{post}"
end
end
end
@ -865,9 +857,9 @@ class RedCloth3 < String
[ ]? # opt. space
(?:\n[ ]*)? # one optional newline followed by spaces
\[(.*?)\] # $id
/x
/x
def inline_markdown_reflink( text )
def inline_markdown_reflink( text )
text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
text, id = $~[1..2]
@ -876,12 +868,12 @@ class RedCloth3 < String
else
url, title = check_refs( id )
end
atts = " href=\"#{ url }\""
atts << " title=\"#{ title }\"" if title
atts = " href=\"#{url}\""
atts << " title=\"#{title}\"" if title
atts = shelve( atts )
"<a#{ atts }>#{ text }</a>"
"<a#{atts}>#{text}</a>"
end
end
@ -897,17 +889,17 @@ class RedCloth3 < String
\3 # matching quote
)? # title is optional
\)
/x
/x
def inline_markdown_link( text )
def inline_markdown_link( text )
text.gsub!( MARKDOWN_LINK_RE ) do |m|
text, url, quote, title = $~[1..4]
atts = " href=\"#{ url }\""
atts << " title=\"#{ title }\"" if title
atts = " href=\"#{url}\""
atts << " title=\"#{title}\"" if title
atts = shelve( atts )
"<a#{ atts }>#{ text }</a>"
"<a#{atts}>#{text}</a>"
end
end
@ -920,14 +912,14 @@ class RedCloth3 < String
end
end
def refs_textile( text )
def refs_textile( text )
text.gsub!( TEXTILE_REFS_RE ) do |m|
flag, url = $~[2..3]
@urlrefs[flag.downcase] = [url, nil]
nil
end
end
def refs_markdown( text )
text.gsub!( MARKDOWN_REFS_RE ) do |m|
flag, url = $~[2..3]
@ -937,7 +929,7 @@ class RedCloth3 < String
end
end
def check_refs( text )
def check_refs( text )
ret = @urlrefs[text.downcase] if text
ret || [text, nil]
end
@ -952,17 +944,17 @@ class RedCloth3 < String
\s? # optional space
(?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
\! # closing
(?::#{ HYPERLINK })? # optional href
/x
(?::#{HYPERLINK})? # optional href
/x
def inline_textile_image( text )
def inline_textile_image( text )
text.gsub!( IMAGE_RE ) do |m|
stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
htmlesc title
atts = pba( atts )
atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
atts << " title=\"#{ title }\"" if title
atts << " alt=\"#{ title }\""
atts = +" src=\"#{htmlesc url.dup}\"#{atts}"
atts << " title=\"#{title}\"" if title
atts << " alt=\"#{title}\""
# size = @getimagesize($url);
# if($size) $atts.= " $size[3]";
@ -970,18 +962,22 @@ class RedCloth3 < String
url, url_title = check_refs( url )
next m unless uri_with_safe_scheme?(url)
if href
href = htmlesc(href.dup)
next m if href.downcase.start_with?('javascript:')
end
out = ''
out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
out << "<img#{ shelve( atts ) } />"
out << "</a>#{ href_a1 }#{ href_a2 }" if href
if algn
out = +''
out << "<a#{shelve(" href=\"#{href}\"")}>" if href
out << "<img#{shelve(atts)} />"
out << "</a>#{href_a1}#{href_a2}" if href
if algn
algn = h_align( algn )
if stln == "<p>"
out = "<p style=\"float:#{ algn }\">#{ out }"
out = "<p style=\"float:#{algn}\">#{out}"
else
out = "#{ stln }<span style=\"float:#{ algn }\">#{ out }</span>"
out = "#{stln}<span style=\"float:#{algn}\">#{out}</span>"
end
else
out = stln + out
@ -991,18 +987,18 @@ class RedCloth3 < String
end
end
def shelve( val )
def shelve( val )
@shelf << val
" :redsh##{ @shelf.length }:"
" :redsh##{@shelf.length}:"
end
def retrieve( text )
def retrieve( text )
text.gsub!(/ :redsh#(\d+):/) do
@shelf[$1.to_i - 1] || $&
end
end
def incoming_entities( text )
def incoming_entities( text )
## turn any incoming ampersands into a dummy character for now.
## This uses a negative lookahead for alphanumerics followed by a semicolon,
## implying an incoming html entity, to be skipped
@ -1010,14 +1006,14 @@ class RedCloth3 < String
text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
end
def no_textile( text )
text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
'\1<notextile>\2</notextile>\3' )
text.gsub!( /^ *==([^=]+.*?)==/m,
'\1<notextile>\2</notextile>\3' )
def no_textile( text )
text.gsub!(/(^|\s)==([^=]+.*?)==(\s|$)?/,
'\1<notextile>\2</notextile>\3')
text.gsub!(/^ *==([^=]+.*?)==/m,
'\1<notextile>\2</notextile>\3')
end
def clean_white_space( text )
def clean_white_space( text )
# normalize line breaks
text.gsub!( /\r\n/, "\n" )
text.gsub!( /\r/, "\n" )
@ -1032,26 +1028,27 @@ class RedCloth3 < String
end
def flush_left( text )
indt = 0
if text =~ /^ /
while text !~ /^ {#{indt}}[^ ]/
indt += 1
end unless text.empty?
if /(?![\r\n\t ])[[:cntrl:]]/.match?(text)
text.gsub!(/(?![\r\n\t ])[[:cntrl:]]/, '')
end
if /^ +\S/.match?(text)
indt = 0
indt += 1 until /^ {#{indt}}\S/.match?(text)
if indt.nonzero?
text.gsub!( /^ {#{indt}}/, '' )
end
end
end
def footnote_ref( text )
text.gsub!( /\b\[([0-9]+?)\](\s)?/,
'<sup><a href="#fn\1">\1</a></sup>\2' )
def footnote_ref( text )
text.gsub!(/\b\[([0-9]+?)\](\s)?/,
'<sup><a href="#fn\1">\1</a></sup>\2')
end
OFFTAGS = /(code|pre|kbd|notextile)/
OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }\b>)|(<#{ OFFTAGS }\b[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\b\W|\Z)/mi
OFFTAG_OPEN = /<#{ OFFTAGS }/
OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
OFFTAG_MATCH = /(?:(<\/#{OFFTAGS}\b>)|(<#{OFFTAGS}\b[^>]*>))(.*?)(?=<\/?#{OFFTAGS}\b\W|\Z)/mi
OFFTAG_OPEN = /<#{OFFTAGS}/
OFFTAG_CLOSE = /<\/?#{OFFTAGS}/
HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
@ -1069,7 +1066,7 @@ class RedCloth3 < String
elsif line =~ OFFTAG_CLOSE
codepre -= 1
codepre = 0 if codepre < 0
end
end
elsif codepre.zero?
glyphs_textile( line, level + 1 )
else
@ -1094,29 +1091,29 @@ class RedCloth3 < String
if codepre - used_offtags.length > 0
htmlesc( line, :NoQuotes ) if escape_line
@pre_list.last << line
line = ""
line = +""
else
### htmlesc is disabled between CODE tags which will be parsed with highlighter
### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
### NB: some changes were made not to use $N variables, because we use "match"
### and it breaks following lines
htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
line = "<redpre##{ @pre_list.length }>"
first.match(/<#{ OFFTAGS }([^>]*)>/)
line = +"<redpre##{@pre_list.length}>"
first.match(/<#{OFFTAGS}([^>]*)>/)
tag = $1
$2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
tag << " #{$1}" if $1 && tag == 'code'
@pre_list << "<#{ tag }>#{ aftertag }"
@pre_list << +"<#{tag}>#{aftertag}"
end
elsif $1 and codepre > 0
if codepre - used_offtags.length > 0
htmlesc( line, :NoQuotes ) if escape_line
@pre_list.last << line
line = ""
line = +""
end
codepre -= 1 unless codepre.zero?
used_offtags = {} if codepre.zero?
end
end
line
end
end
@ -1130,7 +1127,7 @@ class RedCloth3 < String
end
end
def inline( text )
def inline( text )
[/^inline_/, /^glyphs_/].each do |meth_re|
@rules.each do |rule_name|
method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
@ -1138,11 +1135,11 @@ class RedCloth3 < String
end
end
def h_align( text )
def h_align( text )
H_ALGN_VALS[text]
end
def v_align( text )
def v_align( text )
V_ALGN_VALS[text]
end
@ -1156,7 +1153,7 @@ class RedCloth3 < String
'img' => ['src', 'alt', 'title'],
'br' => [],
'i' => nil,
'u' => nil,
'u' => nil,
'b' => nil,
'pre' => nil,
'kbd' => nil,
@ -1181,7 +1178,7 @@ class RedCloth3 < String
'h3' => nil,
'h4' => nil,
'h5' => nil,
'h6' => nil,
'h6' => nil,
'blockquote' => ['cite']
}
@ -1209,8 +1206,7 @@ class RedCloth3 < String
end
end
end
ALLOWED_TAGS = %w(redpre pre code kbd notextile)
def escape_html_tags(text)
text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) do |m|
@ -1222,4 +1218,3 @@ class RedCloth3 < String
end
end
end