To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / lib / redmine @ 1568:bc47b68a9487

1 909:cbb26bc654de Chris
# Redmine - project management software
2 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
3 0:513646585e45 Chris
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8 909:cbb26bc654de Chris
#
9 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13 909:cbb26bc654de Chris
#
14 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17
18
module Redmine
19
  module AccessControl
20 909:cbb26bc654de Chris
21 0:513646585e45 Chris
    class << self
22
      def map
23
        mapper = Mapper.new
24
        yield mapper
25
        @permissions ||= []
26
        @permissions += mapper.mapped_permissions
27
      end
28 909:cbb26bc654de Chris
29 0:513646585e45 Chris
      def permissions
30
        @permissions
31
      end
32 909:cbb26bc654de Chris
33 0:513646585e45 Chris
      # Returns the permission of given name or nil if it wasn't found
34
      # Argument should be a symbol
35
      def permission(name)
36
        permissions.detect {|p| p.name == name}
37
      end
38 909:cbb26bc654de Chris
39 0:513646585e45 Chris
      # Returns the actions that are allowed by the permission of given name
40
      def allowed_actions(permission_name)
41
        perm = permission(permission_name)
42
        perm ? perm.actions : []
43
      end
44 909:cbb26bc654de Chris
45 0:513646585e45 Chris
      def public_permissions
46
        @public_permissions ||= @permissions.select {|p| p.public?}
47
      end
48 909:cbb26bc654de Chris
49 0:513646585e45 Chris
      def members_only_permissions
50
        @members_only_permissions ||= @permissions.select {|p| p.require_member?}
51
      end
52 909:cbb26bc654de Chris
53 0:513646585e45 Chris
      def loggedin_only_permissions
54
        @loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
55
      end
56 909:cbb26bc654de Chris
57 1115:433d4f72a19b Chris
      def read_action?(action)
58
        if action.is_a?(Symbol)
59
          perm = permission(action)
60
          !perm.nil? && perm.read?
61
        else
62
          s = "#{action[:controller]}/#{action[:action]}"
63 1517:dffacf8a6908 Chris
          permissions.detect {|p| p.actions.include?(s) && p.read?}.present?
64 1115:433d4f72a19b Chris
        end
65
      end
66
67 0:513646585e45 Chris
      def available_project_modules
68
        @available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
69
      end
70 909:cbb26bc654de Chris
71 0:513646585e45 Chris
      def modules_permissions(modules)
72
        @permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
73
      end
74
    end
75 909:cbb26bc654de Chris
76 0:513646585e45 Chris
    class Mapper
77
      def initialize
78
        @project_module = nil
79
      end
80 909:cbb26bc654de Chris
81 0:513646585e45 Chris
      def permission(name, hash, options={})
82
        @permissions ||= []
83
        options.merge!(:project_module => @project_module)
84
        @permissions << Permission.new(name, hash, options)
85
      end
86 909:cbb26bc654de Chris
87 0:513646585e45 Chris
      def project_module(name, options={})
88
        @project_module = name
89
        yield self
90
        @project_module = nil
91
      end
92 909:cbb26bc654de Chris
93 0:513646585e45 Chris
      def mapped_permissions
94
        @permissions
95
      end
96
    end
97 909:cbb26bc654de Chris
98 0:513646585e45 Chris
    class Permission
99
      attr_reader :name, :actions, :project_module
100 909:cbb26bc654de Chris
101 0:513646585e45 Chris
      def initialize(name, hash, options)
102
        @name = name
103
        @actions = []
104
        @public = options[:public] || false
105
        @require = options[:require]
106 1115:433d4f72a19b Chris
        @read = options[:read] || false
107 0:513646585e45 Chris
        @project_module = options[:project_module]
108
        hash.each do |controller, actions|
109
          if actions.is_a? Array
110
            @actions << actions.collect {|action| "#{controller}/#{action}"}
111
          else
112
            @actions << "#{controller}/#{actions}"
113
          end
114
        end
115
        @actions.flatten!
116
      end
117 909:cbb26bc654de Chris
118 0:513646585e45 Chris
      def public?
119
        @public
120
      end
121 909:cbb26bc654de Chris
122 0:513646585e45 Chris
      def require_member?
123
        @require && @require == :member
124
      end
125 909:cbb26bc654de Chris
126 0:513646585e45 Chris
      def require_loggedin?
127
        @require && (@require == :member || @require == :loggedin)
128
      end
129 1115:433d4f72a19b Chris
130
      def read?
131
        @read
132
      end
133 909:cbb26bc654de Chris
    end
134 0:513646585e45 Chris
  end
135
end
136 909:cbb26bc654de Chris
# Redmine - project management software
137 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
138 0:513646585e45 Chris
#
139
# This program is free software; you can redistribute it and/or
140
# modify it under the terms of the GNU General Public License
141
# as published by the Free Software Foundation; either version 2
142
# of the License, or (at your option) any later version.
143 909:cbb26bc654de Chris
#
144 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
145
# but WITHOUT ANY WARRANTY; without even the implied warranty of
146
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
147
# GNU General Public License for more details.
148 909:cbb26bc654de Chris
#
149 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
150
# along with this program; if not, write to the Free Software
151
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
152
153
module Redmine
154
  module AccessKeys
155
    ACCESSKEYS = {:edit => 'e',
156
                  :preview => 'r',
157
                  :quick_search => 'f',
158
                  :search => '4',
159
                  :new_issue => '7'
160
                 }.freeze unless const_defined?(:ACCESSKEYS)
161 909:cbb26bc654de Chris
162 0:513646585e45 Chris
    def self.key_for(action)
163
      ACCESSKEYS[action]
164
    end
165
  end
166
end
167
# Redmine - project management software
168 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
169 0:513646585e45 Chris
#
170
# This program is free software; you can redistribute it and/or
171
# modify it under the terms of the GNU General Public License
172
# as published by the Free Software Foundation; either version 2
173
# of the License, or (at your option) any later version.
174 909:cbb26bc654de Chris
#
175 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
176
# but WITHOUT ANY WARRANTY; without even the implied warranty of
177
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
178
# GNU General Public License for more details.
179 909:cbb26bc654de Chris
#
180 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
181
# along with this program; if not, write to the Free Software
182
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
183
184
module Redmine
185
  module Activity
186 909:cbb26bc654de Chris
187 0:513646585e45 Chris
    mattr_accessor :available_event_types, :default_event_types, :providers
188 909:cbb26bc654de Chris
189 0:513646585e45 Chris
    @@available_event_types = []
190
    @@default_event_types = []
191
    @@providers = Hash.new {|h,k| h[k]=[] }
192
193
    class << self
194
      def map(&block)
195
        yield self
196
      end
197 909:cbb26bc654de Chris
198 0:513646585e45 Chris
      # Registers an activity provider
199
      def register(event_type, options={})
200
        options.assert_valid_keys(:class_name, :default)
201 909:cbb26bc654de Chris
202 0:513646585e45 Chris
        event_type = event_type.to_s
203
        providers = options[:class_name] || event_type.classify
204
        providers = ([] << providers) unless providers.is_a?(Array)
205 909:cbb26bc654de Chris
206 0:513646585e45 Chris
        @@available_event_types << event_type unless @@available_event_types.include?(event_type)
207
        @@default_event_types << event_type unless options[:default] == false
208
        @@providers[event_type] += providers
209
      end
210
    end
211
  end
212
end
213
# Redmine - project management software
214 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
215 0:513646585e45 Chris
#
216
# This program is free software; you can redistribute it and/or
217
# modify it under the terms of the GNU General Public License
218
# as published by the Free Software Foundation; either version 2
219
# of the License, or (at your option) any later version.
220 909:cbb26bc654de Chris
#
221 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
222
# but WITHOUT ANY WARRANTY; without even the implied warranty of
223
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
224
# GNU General Public License for more details.
225 909:cbb26bc654de Chris
#
226 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
227
# along with this program; if not, write to the Free Software
228
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
229
230
module Redmine
231
  module Activity
232
    # Class used to retrieve activity events
233
    class Fetcher
234
      attr_reader :user, :project, :scope
235 909:cbb26bc654de Chris
236 0:513646585e45 Chris
      # Needs to be unloaded in development mode
237
      @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
238 909:cbb26bc654de Chris
239 0:513646585e45 Chris
      def initialize(user, options={})
240
        options.assert_valid_keys(:project, :with_subprojects, :author)
241
        @user = user
242
        @project = options[:project]
243
        @options = options
244 909:cbb26bc654de Chris
245 0:513646585e45 Chris
        @scope = event_types
246
      end
247 909:cbb26bc654de Chris
248 0:513646585e45 Chris
      # Returns an array of available event types
249
      def event_types
250
        return @event_types unless @event_types.nil?
251 909:cbb26bc654de Chris
252 0:513646585e45 Chris
        @event_types = Redmine::Activity.available_event_types
253
        @event_types = @event_types.select {|o| @project.self_and_descendants.detect {|p| @user.allowed_to?("view_#{o}".to_sym, p)}} if @project
254
        @event_types
255
      end
256 909:cbb26bc654de Chris
257 0:513646585e45 Chris
      # Yields to filter the activity scope
258
      def scope_select(&block)
259
        @scope = @scope.select {|t| yield t }
260
      end
261 909:cbb26bc654de Chris
262 0:513646585e45 Chris
      # Sets the scope
263
      # Argument can be :all, :default or an array of event types
264
      def scope=(s)
265
        case s
266
        when :all
267
          @scope = event_types
268
        when :default
269
          default_scope!
270
        else
271
          @scope = s & event_types
272
        end
273
      end
274 909:cbb26bc654de Chris
275 0:513646585e45 Chris
      # Resets the scope to the default scope
276
      def default_scope!
277
        @scope = Redmine::Activity.default_event_types
278
      end
279 909:cbb26bc654de Chris
280 0:513646585e45 Chris
      # Returns an array of events for the given date range
281
      # sorted in reverse chronological order
282
      def events(from = nil, to = nil, options={})
283
        e = []
284
        @options[:limit] = options[:limit]
285 909:cbb26bc654de Chris
286 0:513646585e45 Chris
        @scope.each do |event_type|
287
          constantized_providers(event_type).each do |provider|
288
            e += provider.find_events(event_type, @user, from, to, @options)
289
          end
290
        end
291 909:cbb26bc654de Chris
292 0:513646585e45 Chris
        e.sort! {|a,b| b.event_datetime <=> a.event_datetime}
293 909:cbb26bc654de Chris
294 0:513646585e45 Chris
        if options[:limit]
295
          e = e.slice(0, options[:limit])
296
        end
297
        e
298
      end
299 909:cbb26bc654de Chris
300 0:513646585e45 Chris
      private
301 909:cbb26bc654de Chris
302 0:513646585e45 Chris
      def constantized_providers(event_type)
303
        @@constantized_providers[event_type]
304
      end
305
    end
306
  end
307
end
308 245:051f544170fe Chris
# Redmine - project management software
309 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
310 245:051f544170fe Chris
#
311
# This program is free software; you can redistribute it and/or
312
# modify it under the terms of the GNU General Public License
313
# as published by the Free Software Foundation; either version 2
314
# of the License, or (at your option) any later version.
315
#
316
# This program is distributed in the hope that it will be useful,
317
# but WITHOUT ANY WARRANTY; without even the implied warranty of
318
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
319
# GNU General Public License for more details.
320
#
321
# You should have received a copy of the GNU General Public License
322
# along with this program; if not, write to the Free Software
323
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
324
325
module Redmine
326
  module Ciphering
327 909:cbb26bc654de Chris
    def self.included(base)
328 245:051f544170fe Chris
      base.extend ClassMethods
329
    end
330 909:cbb26bc654de Chris
331 245:051f544170fe Chris
    class << self
332
      def encrypt_text(text)
333 909:cbb26bc654de Chris
        if cipher_key.blank? || text.blank?
334 245:051f544170fe Chris
          text
335
        else
336
          c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
337
          iv = c.random_iv
338
          c.encrypt
339
          c.key = cipher_key
340
          c.iv = iv
341
          e = c.update(text.to_s)
342
          e << c.final
343
          "aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--')
344
        end
345
      end
346 909:cbb26bc654de Chris
347 245:051f544170fe Chris
      def decrypt_text(text)
348
        if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/)
349 909:cbb26bc654de Chris
          if cipher_key.blank?
350
            logger.error "Attempt to decrypt a ciphered text with no cipher key configured in config/configuration.yml" if logger
351
            return text
352
          end
353 245:051f544170fe Chris
          text = match[1]
354
          c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
355
          e, iv = text.split("--").map {|s| Base64.decode64(s)}
356
          c.decrypt
357
          c.key = cipher_key
358
          c.iv = iv
359
          d = c.update(e)
360
          d << c.final
361
        else
362
          text
363
        end
364
      end
365 909:cbb26bc654de Chris
366 245:051f544170fe Chris
      def cipher_key
367
        key = Redmine::Configuration['database_cipher_key'].to_s
368
        key.blank? ? nil : Digest::SHA256.hexdigest(key)
369
      end
370 909:cbb26bc654de Chris
371
      def logger
372
        Rails.logger
373
      end
374 245:051f544170fe Chris
    end
375 909:cbb26bc654de Chris
376 245:051f544170fe Chris
    module ClassMethods
377
      def encrypt_all(attribute)
378
        transaction do
379
          all.each do |object|
380
            clear = object.send(attribute)
381
            object.send "#{attribute}=", clear
382 1115:433d4f72a19b Chris
            raise(ActiveRecord::Rollback) unless object.save(:validation => false)
383 245:051f544170fe Chris
          end
384
        end ? true : false
385
      end
386 909:cbb26bc654de Chris
387 245:051f544170fe Chris
      def decrypt_all(attribute)
388
        transaction do
389
          all.each do |object|
390
            clear = object.send(attribute)
391 1115:433d4f72a19b Chris
            object.send :write_attribute, attribute, clear
392
            raise(ActiveRecord::Rollback) unless object.save(:validation => false)
393 245:051f544170fe Chris
          end
394
        end
395
      end ? true : false
396
    end
397 909:cbb26bc654de Chris
398 245:051f544170fe Chris
    private
399 909:cbb26bc654de Chris
400 245:051f544170fe Chris
    # Returns the value of the given ciphered attribute
401
    def read_ciphered_attribute(attribute)
402
      Redmine::Ciphering.decrypt_text(read_attribute(attribute))
403
    end
404 909:cbb26bc654de Chris
405 245:051f544170fe Chris
    # Sets the value of the given ciphered attribute
406
    def write_ciphered_attribute(attribute, value)
407
      write_attribute(attribute, Redmine::Ciphering.encrypt_text(value))
408
    end
409
  end
410
end
411 1464:261b3d9a4903 Chris
if RUBY_VERSION < '1.9'
412
  require 'iconv'
413
end
414 441:cbce1fd3b1b7 Chris
415
module Redmine
416
  module CodesetUtil
417
418
    def self.replace_invalid_utf8(str)
419
      return str if str.nil?
420
      if str.respond_to?(:force_encoding)
421
        str.force_encoding('UTF-8')
422
        if ! str.valid_encoding?
423
          str = str.encode("US-ASCII", :invalid => :replace,
424
                :undef => :replace, :replace => '?').encode("UTF-8")
425
        end
426 909:cbb26bc654de Chris
      elsif RUBY_PLATFORM == 'java'
427
        begin
428
          ic = Iconv.new('UTF-8', 'UTF-8')
429
          str = ic.iconv(str)
430
        rescue
431
          str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
432
        end
433 441:cbce1fd3b1b7 Chris
      else
434
        ic = Iconv.new('UTF-8', 'UTF-8')
435
        txtar = ""
436
        begin
437
          txtar += ic.iconv(str)
438
        rescue Iconv::IllegalSequence
439
          txtar += $!.success
440
          str = '?' + $!.failed[1,$!.failed.length]
441
          retry
442
        rescue
443
          txtar += $!.success
444
        end
445
        str = txtar
446
      end
447
      str
448
    end
449 909:cbb26bc654de Chris
450
    def self.to_utf8(str, encoding)
451
      return str if str.nil?
452
      str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
453
      if str.empty?
454
        str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
455
        return str
456
      end
457
      enc = encoding.blank? ? "UTF-8" : encoding
458
      if str.respond_to?(:force_encoding)
459
        if enc.upcase != "UTF-8"
460
          str.force_encoding(enc)
461
          str = str.encode("UTF-8", :invalid => :replace,
462
                :undef => :replace, :replace => '?')
463
        else
464
          str.force_encoding("UTF-8")
465
          if ! str.valid_encoding?
466
            str = str.encode("US-ASCII", :invalid => :replace,
467
                  :undef => :replace, :replace => '?').encode("UTF-8")
468
          end
469
        end
470
      elsif RUBY_PLATFORM == 'java'
471
        begin
472
          ic = Iconv.new('UTF-8', enc)
473
          str = ic.iconv(str)
474
        rescue
475
          str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
476
        end
477
      else
478
        ic = Iconv.new('UTF-8', enc)
479
        txtar = ""
480
        begin
481
          txtar += ic.iconv(str)
482
        rescue Iconv::IllegalSequence
483
          txtar += $!.success
484
          str = '?' + $!.failed[1,$!.failed.length]
485
          retry
486
        rescue
487
          txtar += $!.success
488
        end
489
        str = txtar
490
      end
491
      str
492
    end
493
494
    def self.to_utf8_by_setting(str)
495
      return str if str.nil?
496
      str = self.to_utf8_by_setting_internal(str)
497
      if str.respond_to?(:force_encoding)
498
        str.force_encoding('UTF-8')
499
      end
500
      str
501
    end
502
503
    def self.to_utf8_by_setting_internal(str)
504
      return str if str.nil?
505
      if str.respond_to?(:force_encoding)
506
        str.force_encoding('ASCII-8BIT')
507
      end
508
      return str if str.empty?
509
      return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
510
      if str.respond_to?(:force_encoding)
511
        str.force_encoding('UTF-8')
512
      end
513
      encodings = Setting.repositories_encodings.split(',').collect(&:strip)
514
      encodings.each do |encoding|
515 1464:261b3d9a4903 Chris
        if str.respond_to?(:force_encoding)
516
          begin
517
            str.force_encoding(encoding)
518
            utf8 = str.encode('UTF-8')
519
            return utf8 if utf8.valid_encoding?
520
          rescue
521
            # do nothing here and try the next encoding
522
          end
523
        else
524
          begin
525
            return Iconv.conv('UTF-8', encoding, str)
526
          rescue Iconv::Failure
527
            # do nothing here and try the next encoding
528
          end
529 909:cbb26bc654de Chris
        end
530
      end
531
      str = self.replace_invalid_utf8(str)
532
      if str.respond_to?(:force_encoding)
533
        str.force_encoding('UTF-8')
534
      end
535
      str
536
    end
537
538
    def self.from_utf8(str, encoding)
539
      str ||= ''
540
      if str.respond_to?(:force_encoding)
541
        str.force_encoding('UTF-8')
542
        if encoding.upcase != 'UTF-8'
543
          str = str.encode(encoding, :invalid => :replace,
544
                           :undef => :replace, :replace => '?')
545
        else
546
          str = self.replace_invalid_utf8(str)
547
        end
548
      elsif RUBY_PLATFORM == 'java'
549
        begin
550
          ic = Iconv.new(encoding, 'UTF-8')
551
          str = ic.iconv(str)
552
        rescue
553
          str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
554
        end
555
      else
556
        ic = Iconv.new(encoding, 'UTF-8')
557
        txtar = ""
558
        begin
559
          txtar += ic.iconv(str)
560
        rescue Iconv::IllegalSequence
561
          txtar += $!.success
562
          str = '?' + $!.failed[1, $!.failed.length]
563
          retry
564
        rescue
565
          txtar += $!.success
566
        end
567
        str = txtar
568
      end
569
    end
570 441:cbce1fd3b1b7 Chris
  end
571
end
572 210:0579821a129a Chris
# Redmine - project management software
573 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
574 210:0579821a129a Chris
#
575
# This program is free software; you can redistribute it and/or
576
# modify it under the terms of the GNU General Public License
577
# as published by the Free Software Foundation; either version 2
578
# of the License, or (at your option) any later version.
579 909:cbb26bc654de Chris
#
580 210:0579821a129a Chris
# This program is distributed in the hope that it will be useful,
581
# but WITHOUT ANY WARRANTY; without even the implied warranty of
582
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
583
# GNU General Public License for more details.
584 909:cbb26bc654de Chris
#
585 210:0579821a129a Chris
# You should have received a copy of the GNU General Public License
586
# along with this program; if not, write to the Free Software
587
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
588
589
module Redmine
590
  module Configuration
591 909:cbb26bc654de Chris
592 210:0579821a129a Chris
    # Configuration default values
593
    @defaults = {
594 1464:261b3d9a4903 Chris
      'email_delivery' => nil,
595
      'max_concurrent_ajax_uploads' => 2
596 210:0579821a129a Chris
    }
597 909:cbb26bc654de Chris
598 210:0579821a129a Chris
    @config = nil
599 909:cbb26bc654de Chris
600 210:0579821a129a Chris
    class << self
601
      # Loads the Redmine configuration file
602
      # Valid options:
603
      # * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml)
604 909:cbb26bc654de Chris
      # * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env)
605 210:0579821a129a Chris
      def load(options={})
606
        filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml')
607
        env = options[:env] || Rails.env
608 909:cbb26bc654de Chris
609 210:0579821a129a Chris
        @config = @defaults.dup
610 909:cbb26bc654de Chris
611 210:0579821a129a Chris
        load_deprecated_email_configuration(env)
612
        if File.file?(filename)
613
          @config.merge!(load_from_yaml(filename, env))
614
        end
615 909:cbb26bc654de Chris
616 210:0579821a129a Chris
        # Compatibility mode for those who copy email.yml over configuration.yml
617
        %w(delivery_method smtp_settings sendmail_settings).each do |key|
618
          if value = @config.delete(key)
619
            @config['email_delivery'] ||= {}
620
            @config['email_delivery'][key] = value
621
          end
622
        end
623 909:cbb26bc654de Chris
624 210:0579821a129a Chris
        if @config['email_delivery']
625
          ActionMailer::Base.perform_deliveries = true
626
          @config['email_delivery'].each do |k, v|
627
            v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
628
            ActionMailer::Base.send("#{k}=", v)
629
          end
630
        end
631 909:cbb26bc654de Chris
632 210:0579821a129a Chris
        @config
633
      end
634 909:cbb26bc654de Chris
635 210:0579821a129a Chris
      # Returns a configuration setting
636
      def [](name)
637
        load unless @config
638
        @config[name]
639
      end
640 909:cbb26bc654de Chris
641 245:051f544170fe Chris
      # Yields a block with the specified hash configuration settings
642
      def with(settings)
643
        settings.stringify_keys!
644
        load unless @config
645
        was = settings.keys.inject({}) {|h,v| h[v] = @config[v]; h}
646
        @config.merge! settings
647
        yield if block_given?
648
        @config.merge! was
649
      end
650 909:cbb26bc654de Chris
651 210:0579821a129a Chris
      private
652 909:cbb26bc654de Chris
653 210:0579821a129a Chris
      def load_from_yaml(filename, env)
654 909:cbb26bc654de Chris
        yaml = nil
655
        begin
656
          yaml = YAML::load_file(filename)
657
        rescue ArgumentError
658
          $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid YAML file and could not be loaded."
659
          exit 1
660
        end
661 210:0579821a129a Chris
        conf = {}
662
        if yaml.is_a?(Hash)
663
          if yaml['default']
664
            conf.merge!(yaml['default'])
665
          end
666
          if yaml[env]
667
            conf.merge!(yaml[env])
668
          end
669
        else
670 909:cbb26bc654de Chris
          $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid Redmine configuration file."
671 210:0579821a129a Chris
          exit 1
672
        end
673
        conf
674
      end
675 909:cbb26bc654de Chris
676 210:0579821a129a Chris
      def load_deprecated_email_configuration(env)
677
        deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml')
678
        if File.file?(deprecated_email_conf)
679
          warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting."
680
          @config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)})
681
        end
682
      end
683
    end
684
  end
685
end
686 0:513646585e45 Chris
Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
687 1115:433d4f72a19b Chris
# Redmine - project management software
688 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
689 1115:433d4f72a19b Chris
#
690
# This program is free software; you can redistribute it and/or
691
# modify it under the terms of the GNU General Public License
692
# as published by the Free Software Foundation; either version 2
693
# of the License, or (at your option) any later version.
694
#
695
# This program is distributed in the hope that it will be useful,
696
# but WITHOUT ANY WARRANTY; without even the implied warranty of
697
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
698
# GNU General Public License for more details.
699
#
700
# You should have received a copy of the GNU General Public License
701
# along with this program; if not, write to the Free Software
702
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
703
704
module ActiveRecord
705
  module FinderMethods
706
    def find_ids(*args)
707
      find_ids_with_associations
708
    end
709
710
    private
711
712
    def find_ids_with_associations
713
        join_dependency = construct_join_dependency_for_association_find
714
        relation = construct_relation_for_association_find_ids(join_dependency)
715
        rows = connection.select_all(relation, 'SQL', relation.bind_values)
716
        rows.map {|row| row["id"].to_i}
717
      rescue ThrowResult
718
        []
719
    end
720
721
    def construct_relation_for_association_find_ids(join_dependency)
722
      relation = except(:includes, :eager_load, :preload, :select).select("#{table_name}.id")
723
      apply_join_dependency(relation, join_dependency)
724
    end
725
  end
726
end
727 1464:261b3d9a4903 Chris
728
class DateValidator < ActiveModel::EachValidator
729
  def validate_each(record, attribute, value)
730
    before_type_cast = record.attributes_before_type_cast[attribute.to_s]
731
    if before_type_cast.is_a?(String) && before_type_cast.present?
732
      # TODO: #*_date_before_type_cast returns a Mysql::Time with ruby1.8+mysql gem
733
      unless before_type_cast =~ /\A\d{4}-\d{2}-\d{2}( 00:00:00)?\z/ && value
734
        record.errors.add attribute, :not_a_date
735
      end
736
    end
737
  end
738
end
739 1115:433d4f72a19b Chris
require File.dirname(__FILE__) + '/date/calculations'
740
741
class Date #:nodoc:
742
  include Redmine::CoreExtensions::Date::Calculations
743
end
744
# Redmine - project management software
745 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
746 1115:433d4f72a19b Chris
#
747
# This program is free software; you can redistribute it and/or
748
# modify it under the terms of the GNU General Public License
749
# as published by the Free Software Foundation; either version 2
750
# of the License, or (at your option) any later version.
751
#
752
# This program is distributed in the hope that it will be useful,
753
# but WITHOUT ANY WARRANTY; without even the implied warranty of
754
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
755
# GNU General Public License for more details.
756
#
757
# You should have received a copy of the GNU General Public License
758
# along with this program; if not, write to the Free Software
759
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
760
761
module Redmine #:nodoc:
762
  module CoreExtensions #:nodoc:
763
    module Date #:nodoc:
764
      # Custom date calculations
765
      module Calculations
766
        # Returns difference with specified date in months
767
        def months_ago(date = self.class.today)
768
          (date.year - self.year)*12 + (date.month - self.month)
769
        end
770
771
        # Returns difference with specified date in weeks
772
        def weeks_ago(date = self.class.today)
773
          (date.year - self.year)*52 + (date.cweek - self.cweek)
774
        end
775
      end
776
    end
777
  end
778
end
779 0:513646585e45 Chris
require File.dirname(__FILE__) + '/string/conversions'
780
require File.dirname(__FILE__) + '/string/inflections'
781
782
class String #:nodoc:
783
  include Redmine::CoreExtensions::String::Conversions
784
  include Redmine::CoreExtensions::String::Inflections
785 1115:433d4f72a19b Chris
786
  def is_binary_data?
787
    ( self.count( "^ -~", "^\r\n" ).fdiv(self.size) > 0.3 || self.index( "\x00" ) ) unless empty?
788
  end
789 0:513646585e45 Chris
end
790 909:cbb26bc654de Chris
# Redmine - project management software
791 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
792 0:513646585e45 Chris
#
793
# This program is free software; you can redistribute it and/or
794
# modify it under the terms of the GNU General Public License
795
# as published by the Free Software Foundation; either version 2
796
# of the License, or (at your option) any later version.
797 909:cbb26bc654de Chris
#
798 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
799
# but WITHOUT ANY WARRANTY; without even the implied warranty of
800
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
801
# GNU General Public License for more details.
802 909:cbb26bc654de Chris
#
803 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
804
# along with this program; if not, write to the Free Software
805
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
806
807
module Redmine #:nodoc:
808
  module CoreExtensions #:nodoc:
809
    module String #:nodoc:
810
      # Custom string conversions
811
      module Conversions
812
        # Parses hours format and returns a float
813
        def to_hours
814
          s = self.dup
815
          s.strip!
816
          if s =~ %r{^(\d+([.,]\d+)?)h?$}
817
            s = $1
818
          else
819
            # 2:30 => 2.5
820
            s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
821
            # 2h30, 2h, 30m => 2.5, 2, 0.5
822 1115:433d4f72a19b Chris
            s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}i) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
823 0:513646585e45 Chris
          end
824
          # 2,5 => 2.5
825
          s.gsub!(',', '.')
826
          begin; Kernel.Float(s); rescue; nil; end
827
        end
828 909:cbb26bc654de Chris
829 0:513646585e45 Chris
        # Object#to_a removed in ruby1.9
830
        if RUBY_VERSION > '1.9'
831
          def to_a
832
            [self.dup]
833
          end
834
        end
835
      end
836
    end
837
  end
838
end
839
# Redmine - project management software
840 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
841 0:513646585e45 Chris
#
842
# This program is free software; you can redistribute it and/or
843
# modify it under the terms of the GNU General Public License
844
# as published by the Free Software Foundation; either version 2
845
# of the License, or (at your option) any later version.
846 909:cbb26bc654de Chris
#
847 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
848
# but WITHOUT ANY WARRANTY; without even the implied warranty of
849
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
850
# GNU General Public License for more details.
851 909:cbb26bc654de Chris
#
852 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
853
# along with this program; if not, write to the Free Software
854
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
855
856
module Redmine #:nodoc:
857
  module CoreExtensions #:nodoc:
858
    module String #:nodoc:
859
      # Custom string inflections
860
      module Inflections
861
        def with_leading_slash
862
          starts_with?('/') ? self : "/#{ self }"
863
        end
864
      end
865
    end
866
  end
867
end
868 441:cbce1fd3b1b7 Chris
# Redmine - project management software
869 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
870 0:513646585e45 Chris
#
871
# This program is free software; you can redistribute it and/or
872
# modify it under the terms of the GNU General Public License
873
# as published by the Free Software Foundation; either version 2
874
# of the License, or (at your option) any later version.
875 909:cbb26bc654de Chris
#
876 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
877
# but WITHOUT ANY WARRANTY; without even the implied warranty of
878
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
879
# GNU General Public License for more details.
880 909:cbb26bc654de Chris
#
881 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
882
# along with this program; if not, write to the Free Software
883
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
884
885
module Redmine
886
  module DefaultData
887
    class DataAlreadyLoaded < Exception; end
888
889
    module Loader
890
      include Redmine::I18n
891 909:cbb26bc654de Chris
892 0:513646585e45 Chris
      class << self
893
        # Returns true if no data is already loaded in the database
894
        # otherwise false
895
        def no_data?
896 1464:261b3d9a4903 Chris
          !Role.where(:builtin => 0).exists? &&
897
            !Tracker.exists? &&
898
            !IssueStatus.exists? &&
899
            !Enumeration.exists?
900 0:513646585e45 Chris
        end
901 909:cbb26bc654de Chris
902 0:513646585e45 Chris
        # Loads the default data
903
        # Raises a RecordNotSaved exception if something goes wrong
904
        def load(lang=nil)
905
          raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
906
          set_language_if_valid(lang)
907 909:cbb26bc654de Chris
908 0:513646585e45 Chris
          Role.transaction do
909
            # Roles
910 909:cbb26bc654de Chris
            manager = Role.create! :name => l(:default_role_manager),
911 441:cbce1fd3b1b7 Chris
                                   :issues_visibility => 'all',
912 0:513646585e45 Chris
                                   :position => 1
913
            manager.permissions = manager.setable_permissions.collect {|p| p.name}
914
            manager.save!
915 909:cbb26bc654de Chris
916
            developer = Role.create!  :name => l(:default_role_developer),
917
                                      :position => 2,
918
                                      :permissions => [:manage_versions,
919 0:513646585e45 Chris
                                                      :manage_categories,
920
                                                      :view_issues,
921
                                                      :add_issues,
922
                                                      :edit_issues,
923 1115:433d4f72a19b Chris
                                                      :view_private_notes,
924
                                                      :set_notes_private,
925 0:513646585e45 Chris
                                                      :manage_issue_relations,
926
                                                      :manage_subtasks,
927
                                                      :add_issue_notes,
928
                                                      :save_queries,
929
                                                      :view_gantt,
930
                                                      :view_calendar,
931
                                                      :log_time,
932
                                                      :view_time_entries,
933
                                                      :comment_news,
934
                                                      :view_documents,
935
                                                      :view_wiki_pages,
936
                                                      :view_wiki_edits,
937
                                                      :edit_wiki_pages,
938
                                                      :delete_wiki_pages,
939
                                                      :add_messages,
940
                                                      :edit_own_messages,
941
                                                      :view_files,
942
                                                      :manage_files,
943
                                                      :browse_repository,
944
                                                      :view_changesets,
945 1115:433d4f72a19b Chris
                                                      :commit_access,
946
                                                      :manage_related_issues]
947 909:cbb26bc654de Chris
948 0:513646585e45 Chris
            reporter = Role.create! :name => l(:default_role_reporter),
949
                                    :position => 3,
950
                                    :permissions => [:view_issues,
951
                                                    :add_issues,
952
                                                    :add_issue_notes,
953
                                                    :save_queries,
954
                                                    :view_gantt,
955
                                                    :view_calendar,
956
                                                    :log_time,
957
                                                    :view_time_entries,
958
                                                    :comment_news,
959
                                                    :view_documents,
960
                                                    :view_wiki_pages,
961
                                                    :view_wiki_edits,
962
                                                    :add_messages,
963
                                                    :edit_own_messages,
964
                                                    :view_files,
965
                                                    :browse_repository,
966
                                                    :view_changesets]
967 909:cbb26bc654de Chris
968 0:513646585e45 Chris
            Role.non_member.update_attribute :permissions, [:view_issues,
969
                                                            :add_issues,
970
                                                            :add_issue_notes,
971
                                                            :save_queries,
972
                                                            :view_gantt,
973
                                                            :view_calendar,
974
                                                            :view_time_entries,
975
                                                            :comment_news,
976
                                                            :view_documents,
977
                                                            :view_wiki_pages,
978
                                                            :view_wiki_edits,
979
                                                            :add_messages,
980
                                                            :view_files,
981
                                                            :browse_repository,
982
                                                            :view_changesets]
983 909:cbb26bc654de Chris
984 0:513646585e45 Chris
            Role.anonymous.update_attribute :permissions, [:view_issues,
985
                                                           :view_gantt,
986
                                                           :view_calendar,
987
                                                           :view_time_entries,
988
                                                           :view_documents,
989
                                                           :view_wiki_pages,
990
                                                           :view_wiki_edits,
991
                                                           :view_files,
992
                                                           :browse_repository,
993
                                                           :view_changesets]
994 909:cbb26bc654de Chris
995 0:513646585e45 Chris
            # Trackers
996
            Tracker.create!(:name => l(:default_tracker_bug),     :is_in_chlog => true,  :is_in_roadmap => false, :position => 1)
997
            Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true,  :is_in_roadmap => true,  :position => 2)
998
            Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
999 909:cbb26bc654de Chris
1000 0:513646585e45 Chris
            # Issue statuses
1001
            new       = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
1002
            in_progress  = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
1003
            resolved  = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
1004
            feedback  = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
1005
            closed    = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
1006
            rejected  = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
1007 909:cbb26bc654de Chris
1008 0:513646585e45 Chris
            # Workflow
1009 1464:261b3d9a4903 Chris
            Tracker.all.each { |t|
1010
              IssueStatus.all.each { |os|
1011
                IssueStatus.all.each { |ns|
1012 1115:433d4f72a19b Chris
                  WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
1013 909:cbb26bc654de Chris
                }
1014
              }
1015 0:513646585e45 Chris
            }
1016 909:cbb26bc654de Chris
1017 1464:261b3d9a4903 Chris
            Tracker.all.each { |t|
1018 0:513646585e45 Chris
              [new, in_progress, resolved, feedback].each { |os|
1019
                [in_progress, resolved, feedback, closed].each { |ns|
1020 1115:433d4f72a19b Chris
                  WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
1021 909:cbb26bc654de Chris
                }
1022
              }
1023 0:513646585e45 Chris
            }
1024 909:cbb26bc654de Chris
1025 1464:261b3d9a4903 Chris
            Tracker.all.each { |t|
1026 0:513646585e45 Chris
              [new, in_progress, resolved, feedback].each { |os|
1027
                [closed].each { |ns|
1028 1115:433d4f72a19b Chris
                  WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
1029 909:cbb26bc654de Chris
                }
1030 0:513646585e45 Chris
              }
1031 1115:433d4f72a19b Chris
              WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
1032 0:513646585e45 Chris
            }
1033 909:cbb26bc654de Chris
1034 0:513646585e45 Chris
            # Enumerations
1035
            IssuePriority.create!(:name => l(:default_priority_low), :position => 1)
1036
            IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true)
1037
            IssuePriority.create!(:name => l(:default_priority_high), :position => 3)
1038
            IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4)
1039
            IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5)
1040 909:cbb26bc654de Chris
1041 1115:433d4f72a19b Chris
            DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1)
1042
            DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2)
1043
1044 0:513646585e45 Chris
            TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1)
1045
            TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2)
1046
          end
1047
          true
1048
        end
1049
      end
1050
    end
1051
  end
1052
end
1053
# encoding: utf-8
1054
#
1055
# Redmine - project management software
1056 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
1057 0:513646585e45 Chris
#
1058
# This program is free software; you can redistribute it and/or
1059
# modify it under the terms of the GNU General Public License
1060
# as published by the Free Software Foundation; either version 2
1061
# of the License, or (at your option) any later version.
1062 441:cbce1fd3b1b7 Chris
#
1063 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
1064
# but WITHOUT ANY WARRANTY; without even the implied warranty of
1065
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1066
# GNU General Public License for more details.
1067 441:cbce1fd3b1b7 Chris
#
1068 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
1069
# along with this program; if not, write to the Free Software
1070
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
1071
1072 1115:433d4f72a19b Chris
require 'tcpdf'
1073 441:cbce1fd3b1b7 Chris
require 'fpdf/chinese'
1074
require 'fpdf/japanese'
1075
require 'fpdf/korean'
1076 0:513646585e45 Chris
1077 1464:261b3d9a4903 Chris
if RUBY_VERSION < '1.9'
1078
  require 'iconv'
1079
end
1080
1081 0:513646585e45 Chris
module Redmine
1082
  module Export
1083
    module PDF
1084
      include ActionView::Helpers::TextHelper
1085
      include ActionView::Helpers::NumberHelper
1086 909:cbb26bc654de Chris
      include IssuesHelper
1087 441:cbce1fd3b1b7 Chris
1088
      class ITCPDF < TCPDF
1089 0:513646585e45 Chris
        include Redmine::I18n
1090
        attr_accessor :footer_date
1091 441:cbce1fd3b1b7 Chris
1092 1115:433d4f72a19b Chris
        def initialize(lang, orientation='P')
1093 909:cbb26bc654de Chris
          @@k_path_cache = Rails.root.join('tmp', 'pdf')
1094
          FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
1095 0:513646585e45 Chris
          set_language_if_valid lang
1096 441:cbce1fd3b1b7 Chris
          pdf_encoding = l(:general_pdf_encoding).upcase
1097 1115:433d4f72a19b Chris
          super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding)
1098 507:0c939c159af4 Chris
          case current_language.to_s.downcase
1099
          when 'vi'
1100
            @font_for_content = 'DejaVuSans'
1101
            @font_for_footer  = 'DejaVuSans'
1102 0:513646585e45 Chris
          else
1103 507:0c939c159af4 Chris
            case pdf_encoding
1104
            when 'UTF-8'
1105
              @font_for_content = 'FreeSans'
1106
              @font_for_footer  = 'FreeSans'
1107
            when 'CP949'
1108
              extend(PDF_Korean)
1109
              AddUHCFont()
1110
              @font_for_content = 'UHC'
1111
              @font_for_footer  = 'UHC'
1112
            when 'CP932', 'SJIS', 'SHIFT_JIS'
1113
              extend(PDF_Japanese)
1114
              AddSJISFont()
1115
              @font_for_content = 'SJIS'
1116
              @font_for_footer  = 'SJIS'
1117
            when 'GB18030'
1118
              extend(PDF_Chinese)
1119
              AddGBFont()
1120
              @font_for_content = 'GB'
1121
              @font_for_footer  = 'GB'
1122
            when 'BIG5'
1123
              extend(PDF_Chinese)
1124
              AddBig5Font()
1125
              @font_for_content = 'Big5'
1126
              @font_for_footer  = 'Big5'
1127
            else
1128
              @font_for_content = 'Arial'
1129
              @font_for_footer  = 'Helvetica'
1130
            end
1131 0:513646585e45 Chris
          end
1132
          SetCreator(Redmine::Info.app_name)
1133
          SetFont(@font_for_content)
1134 1115:433d4f72a19b Chris
          @outlines = []
1135
          @outlineRoot = nil
1136 0:513646585e45 Chris
        end
1137 441:cbce1fd3b1b7 Chris
1138 0:513646585e45 Chris
        def SetFontStyle(style, size)
1139
          SetFont(@font_for_content, style, size)
1140
        end
1141 441:cbce1fd3b1b7 Chris
1142 0:513646585e45 Chris
        def SetTitle(txt)
1143
          txt = begin
1144 1464:261b3d9a4903 Chris
            utf16txt = to_utf16(txt)
1145 0:513646585e45 Chris
            hextxt = "<FEFF"  # FEFF is BOM
1146
            hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
1147
            hextxt << ">"
1148
          rescue
1149
            txt
1150
          end || ''
1151
          super(txt)
1152
        end
1153 441:cbce1fd3b1b7 Chris
1154 0:513646585e45 Chris
        def textstring(s)
1155
          # Format a text string
1156
          if s =~ /^</  # This means the string is hex-dumped.
1157
            return s
1158
          else
1159
            return '('+escape(s)+')'
1160
          end
1161
        end
1162 441:cbce1fd3b1b7 Chris
1163
        def fix_text_encoding(txt)
1164 909:cbb26bc654de Chris
          RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding))
1165 0:513646585e45 Chris
        end
1166 441:cbce1fd3b1b7 Chris
1167 1294:3e4c3460b6ca Chris
        def formatted_text(text)
1168
          html = Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
1169
          # Strip {{toc}} tags
1170
          html.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '')
1171
          html
1172
        end
1173
1174 1464:261b3d9a4903 Chris
        # Encodes an UTF-8 string to UTF-16BE
1175
        def to_utf16(str)
1176
          if str.respond_to?(:encode)
1177
            str.encode('UTF-16BE')
1178
          else
1179
            Iconv.conv('UTF-16BE', 'UTF-8', str)
1180
          end
1181
        end
1182
1183 507:0c939c159af4 Chris
        def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
1184
          Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link)
1185 441:cbce1fd3b1b7 Chris
        end
1186
1187 507:0c939c159af4 Chris
        def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
1188
          MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln)
1189 441:cbce1fd3b1b7 Chris
        end
1190
1191 909:cbb26bc654de Chris
        def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
1192
          @attachments = attachments
1193
          writeHTMLCell(w, h, x, y,
1194 1294:3e4c3460b6ca Chris
            fix_text_encoding(formatted_text(txt)),
1195 909:cbb26bc654de Chris
            border, ln, fill)
1196
        end
1197
1198
        def getImageFilename(attrname)
1199
          # attrname: general_pdf_encoding string file/uri name
1200
          atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
1201
          if atta
1202
            return atta.diskfile
1203
          else
1204
            return nil
1205
          end
1206
        end
1207
1208 0:513646585e45 Chris
        def Footer
1209
          SetFont(@font_for_footer, 'I', 8)
1210
          SetY(-15)
1211
          SetX(15)
1212 441:cbce1fd3b1b7 Chris
          RDMCell(0, 5, @footer_date, 0, 0, 'L')
1213 0:513646585e45 Chris
          SetY(-15)
1214
          SetX(-30)
1215 441:cbce1fd3b1b7 Chris
          RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
1216 0:513646585e45 Chris
        end
1217 1115:433d4f72a19b Chris
1218
        def Bookmark(txt, level=0, y=0)
1219
          if (y == -1)
1220
            y = GetY()
1221
          end
1222
          @outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k}
1223
        end
1224
1225
        def bookmark_title(txt)
1226
          txt = begin
1227 1464:261b3d9a4903 Chris
            utf16txt = to_utf16(txt)
1228 1115:433d4f72a19b Chris
            hextxt = "<FEFF"  # FEFF is BOM
1229
            hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
1230
            hextxt << ">"
1231
          rescue
1232
            txt
1233
          end || ''
1234
        end
1235
1236
        def putbookmarks
1237
          nb=@outlines.size
1238
          return if (nb==0)
1239
          lru=[]
1240
          level=0
1241
          @outlines.each_with_index do |o, i|
1242
            if(o[:l]>0)
1243
              parent=lru[o[:l]-1]
1244
              #Set parent and last pointers
1245
              @outlines[i][:parent]=parent
1246
              @outlines[parent][:last]=i
1247
              if (o[:l]>level)
1248
                #Level increasing: set first pointer
1249
                @outlines[parent][:first]=i
1250
              end
1251
            else
1252
              @outlines[i][:parent]=nb
1253
            end
1254
            if (o[:l]<=level && i>0)
1255
              #Set prev and next pointers
1256
              prev=lru[o[:l]]
1257
              @outlines[prev][:next]=i
1258
              @outlines[i][:prev]=prev
1259
            end
1260
            lru[o[:l]]=i
1261
            level=o[:l]
1262
          end
1263
          #Outline items
1264
          n=self.n+1
1265
          @outlines.each_with_index do |o, i|
1266
            newobj()
1267
            out('<</Title '+bookmark_title(o[:t]))
1268
            out("/Parent #{n+o[:parent]} 0 R")
1269
            if (o[:prev])
1270
              out("/Prev #{n+o[:prev]} 0 R")
1271
            end
1272
            if (o[:next])
1273
              out("/Next #{n+o[:next]} 0 R")
1274
            end
1275
            if (o[:first])
1276
              out("/First #{n+o[:first]} 0 R")
1277
            end
1278
            if (o[:last])
1279
              out("/Last #{n+o[:last]} 0 R")
1280
            end
1281
            out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]])
1282
            out('/Count 0>>')
1283
            out('endobj')
1284
          end
1285
          #Outline root
1286
          newobj()
1287
          @outlineRoot=self.n
1288
          out("<</Type /Outlines /First #{n} 0 R");
1289
          out("/Last #{n+lru[0]} 0 R>>");
1290
          out('endobj');
1291
        end
1292
1293
        def putresources()
1294
          super
1295
          putbookmarks()
1296
        end
1297
1298
        def putcatalog()
1299
          super
1300
          if(@outlines.size > 0)
1301
            out("/Outlines #{@outlineRoot} 0 R");
1302
            out('/PageMode /UseOutlines');
1303
          end
1304
        end
1305
      end
1306
1307
      # fetch row values
1308
      def fetch_row_values(issue, query, level)
1309
        query.inline_columns.collect do |column|
1310
          s = if column.is_a?(QueryCustomFieldColumn)
1311 1464:261b3d9a4903 Chris
            cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
1312 1517:dffacf8a6908 Chris
            show_value(cv, false)
1313 1115:433d4f72a19b Chris
          else
1314
            value = issue.send(column.name)
1315
            if column.name == :subject
1316
              value = "  " * level + value
1317
            end
1318
            if value.is_a?(Date)
1319
              format_date(value)
1320
            elsif value.is_a?(Time)
1321
              format_time(value)
1322
            else
1323
              value
1324
            end
1325
          end
1326
          s.to_s
1327
        end
1328
      end
1329
1330
      # calculate columns width
1331
      def calc_col_width(issues, query, table_width, pdf)
1332
        # calculate statistics
1333
        #  by captions
1334
        pdf.SetFontStyle('B',8)
1335
        col_padding = pdf.GetStringWidth('OO')
1336
        col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding}
1337
        col_width_max = Array.new(col_width_min)
1338
        col_width_avg = Array.new(col_width_min)
1339
        word_width_max = query.inline_columns.map {|c|
1340
          n = 10
1341
          c.caption.split.each {|w|
1342
            x = pdf.GetStringWidth(w) + col_padding
1343
            n = x if n < x
1344
          }
1345
          n
1346
        }
1347
1348
        #  by properties of issues
1349
        pdf.SetFontStyle('',8)
1350
        col_padding = pdf.GetStringWidth('OO')
1351
        k = 1
1352
        issue_list(issues) {|issue, level|
1353
          k += 1
1354
          values = fetch_row_values(issue, query, level)
1355
          values.each_with_index {|v,i|
1356
            n = pdf.GetStringWidth(v) + col_padding
1357
            col_width_max[i] = n if col_width_max[i] < n
1358
            col_width_min[i] = n if col_width_min[i] > n
1359
            col_width_avg[i] += n
1360
            v.split.each {|w|
1361
              x = pdf.GetStringWidth(w) + col_padding
1362
              word_width_max[i] = x if word_width_max[i] < x
1363
            }
1364
          }
1365
        }
1366
        col_width_avg.map! {|x| x / k}
1367
1368
        # calculate columns width
1369 1464:261b3d9a4903 Chris
        ratio = table_width / col_width_avg.inject(0, :+)
1370 1115:433d4f72a19b Chris
        col_width = col_width_avg.map {|w| w * ratio}
1371
1372
        # correct max word width if too many columns
1373 1464:261b3d9a4903 Chris
        ratio = table_width / word_width_max.inject(0, :+)
1374 1115:433d4f72a19b Chris
        word_width_max.map! {|v| v * ratio} if ratio < 1
1375
1376
        # correct and lock width of some columns
1377
        done = 1
1378
        col_fix = []
1379
        col_width.each_with_index do |w,i|
1380
          if w > col_width_max[i]
1381
            col_width[i] = col_width_max[i]
1382
            col_fix[i] = 1
1383
            done = 0
1384
          elsif w < word_width_max[i]
1385
            col_width[i] = word_width_max[i]
1386
            col_fix[i] = 1
1387
            done = 0
1388
          else
1389
            col_fix[i] = 0
1390
          end
1391
        end
1392
1393
        # iterate while need to correct and lock coluns width
1394
        while done == 0
1395
          # calculate free & locked columns width
1396
          done = 1
1397
          fix_col_width = 0
1398
          free_col_width = 0
1399
          col_width.each_with_index do |w,i|
1400
            if col_fix[i] == 1
1401
              fix_col_width += w
1402
            else
1403
              free_col_width += w
1404
            end
1405
          end
1406
1407
          # calculate column normalizing ratio
1408
          if free_col_width == 0
1409 1464:261b3d9a4903 Chris
            ratio = table_width / col_width.inject(0, :+)
1410 1115:433d4f72a19b Chris
          else
1411
            ratio = (table_width - fix_col_width) / free_col_width
1412
          end
1413
1414
          # correct columns width
1415
          col_width.each_with_index do |w,i|
1416
            if col_fix[i] == 0
1417
              col_width[i] = w * ratio
1418
1419
              # check if column width less then max word width
1420
              if col_width[i] < word_width_max[i]
1421
                col_width[i] = word_width_max[i]
1422
                col_fix[i] = 1
1423
                done = 0
1424
              elsif col_width[i] > col_width_max[i]
1425
                col_width[i] = col_width_max[i]
1426
                col_fix[i] = 1
1427
                done = 0
1428
              end
1429
            end
1430
          end
1431
        end
1432
        col_width
1433
      end
1434
1435 1464:261b3d9a4903 Chris
      def render_table_header(pdf, query, col_width, row_height, table_width)
1436 1115:433d4f72a19b Chris
        # headers
1437
        pdf.SetFontStyle('B',8)
1438
        pdf.SetFillColor(230, 230, 230)
1439
1440
        # render it background to find the max height used
1441
        base_x = pdf.GetX
1442
        base_y = pdf.GetY
1443
        max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true)
1444 1464:261b3d9a4903 Chris
        pdf.Rect(base_x, base_y, table_width, max_height, 'FD');
1445 1115:433d4f72a19b Chris
        pdf.SetXY(base_x, base_y);
1446
1447
        # write the cells on page
1448
        issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true)
1449 1464:261b3d9a4903 Chris
        issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width)
1450 1115:433d4f72a19b Chris
        pdf.SetY(base_y + max_height);
1451
1452
        # rows
1453
        pdf.SetFontStyle('',8)
1454
        pdf.SetFillColor(255, 255, 255)
1455 0:513646585e45 Chris
      end
1456 441:cbce1fd3b1b7 Chris
1457 0:513646585e45 Chris
      # Returns a PDF string of a list of issues
1458
      def issues_to_pdf(issues, project, query)
1459 1115:433d4f72a19b Chris
        pdf = ITCPDF.new(current_language, "L")
1460 0:513646585e45 Chris
        title = query.new_record? ? l(:label_issue_plural) : query.name
1461
        title = "#{project} - #{title}" if project
1462
        pdf.SetTitle(title)
1463 441:cbce1fd3b1b7 Chris
        pdf.alias_nb_pages
1464 0:513646585e45 Chris
        pdf.footer_date = format_date(Date.today)
1465 441:cbce1fd3b1b7 Chris
        pdf.SetAutoPageBreak(false)
1466 0:513646585e45 Chris
        pdf.AddPage("L")
1467 441:cbce1fd3b1b7 Chris
1468
        # Landscape A4 = 210 x 297 mm
1469
        page_height   = 210
1470
        page_width    = 297
1471 1464:261b3d9a4903 Chris
        left_margin   = 10
1472 441:cbce1fd3b1b7 Chris
        right_margin  = 10
1473
        bottom_margin = 20
1474 1115:433d4f72a19b Chris
        row_height    = 4
1475 441:cbce1fd3b1b7 Chris
1476
        # column widths
1477 1464:261b3d9a4903 Chris
        table_width = page_width - right_margin - left_margin
1478 0:513646585e45 Chris
        col_width = []
1479 1115:433d4f72a19b Chris
        unless query.inline_columns.empty?
1480 1464:261b3d9a4903 Chris
          col_width = calc_col_width(issues, query, table_width, pdf)
1481
          table_width = col_width.inject(0, :+)
1482 1115:433d4f72a19b Chris
        end
1483
1484 1464:261b3d9a4903 Chris
        # use full width if the description is displayed
1485 1115:433d4f72a19b Chris
        if table_width > 0 && query.has_column?(:description)
1486 1464:261b3d9a4903 Chris
          col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
1487
          table_width = col_width.inject(0, :+)
1488 0:513646585e45 Chris
        end
1489 441:cbce1fd3b1b7 Chris
1490 0:513646585e45 Chris
        # title
1491 441:cbce1fd3b1b7 Chris
        pdf.SetFontStyle('B',11)
1492
        pdf.RDMCell(190,10, title)
1493 0:513646585e45 Chris
        pdf.Ln
1494 1464:261b3d9a4903 Chris
        render_table_header(pdf, query, col_width, row_height, table_width)
1495 0:513646585e45 Chris
        previous_group = false
1496 909:cbb26bc654de Chris
        issue_list(issues) do |issue, level|
1497 441:cbce1fd3b1b7 Chris
          if query.grouped? &&
1498
               (group = query.group_by_column.value(issue)) != previous_group
1499 1115:433d4f72a19b Chris
            pdf.SetFontStyle('B',10)
1500
            group_label = group.blank? ? 'None' : group.to_s.dup
1501
            group_label << " (#{query.issue_count_by_group[group]})"
1502
            pdf.Bookmark group_label, 0, -1
1503 1464:261b3d9a4903 Chris
            pdf.RDMCell(table_width, row_height * 2, group_label, 1, 1, 'L')
1504 0:513646585e45 Chris
            pdf.SetFontStyle('',8)
1505
            previous_group = group
1506
          end
1507 1115:433d4f72a19b Chris
1508
          # fetch row values
1509
          col_values = fetch_row_values(issue, query, level)
1510 441:cbce1fd3b1b7 Chris
1511
          # render it off-page to find the max height used
1512
          base_x = pdf.GetX
1513
          base_y = pdf.GetY
1514
          pdf.SetY(2 * page_height)
1515
          max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
1516
          pdf.SetXY(base_x, base_y)
1517
1518
          # make new page if it doesn't fit on the current one
1519
          space_left = page_height - base_y - bottom_margin
1520
          if max_height > space_left
1521
            pdf.AddPage("L")
1522 1464:261b3d9a4903 Chris
            render_table_header(pdf, query, col_width, row_height, table_width)
1523 441:cbce1fd3b1b7 Chris
            base_x = pdf.GetX
1524
            base_y = pdf.GetY
1525
          end
1526
1527
          # write the cells on page
1528
          issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
1529 1464:261b3d9a4903 Chris
          issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width)
1530 441:cbce1fd3b1b7 Chris
          pdf.SetY(base_y + max_height);
1531 1115:433d4f72a19b Chris
1532
          if query.has_column?(:description) && issue.description?
1533
            pdf.SetX(10)
1534
            pdf.SetAutoPageBreak(true, 20)
1535
            pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT")
1536
            pdf.SetAutoPageBreak(false)
1537
          end
1538 0:513646585e45 Chris
        end
1539 441:cbce1fd3b1b7 Chris
1540 0:513646585e45 Chris
        if issues.size == Setting.issues_export_limit.to_i
1541
          pdf.SetFontStyle('B',10)
1542 441:cbce1fd3b1b7 Chris
          pdf.RDMCell(0, row_height, '...')
1543 0:513646585e45 Chris
        end
1544
        pdf.Output
1545
      end
1546 22:40f7cfd4df19 chris
1547 441:cbce1fd3b1b7 Chris
      # Renders MultiCells and returns the maximum height used
1548 1464:261b3d9a4903 Chris
      def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false)
1549 441:cbce1fd3b1b7 Chris
        base_y = pdf.GetY
1550
        max_height = row_height
1551
        col_values.each_with_index do |column, i|
1552
          col_x = pdf.GetX
1553
          if head == true
1554
            pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
1555
          else
1556
            pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
1557
          end
1558
          max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
1559
          pdf.SetXY(col_x + col_widths[i], base_y);
1560
        end
1561
        return max_height
1562
      end
1563
1564
      # Draw lines to close the row (MultiCell border drawing in not uniform)
1565 1464:261b3d9a4903 Chris
      #
1566
      #  parameter "col_id_width" is not used. it is kept for compatibility.
1567 441:cbce1fd3b1b7 Chris
      def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
1568 1464:261b3d9a4903 Chris
                                     col_id_width, col_widths)
1569
        col_x = top_x
1570 441:cbce1fd3b1b7 Chris
        pdf.Line(col_x, top_y, col_x, lower_y)    # id right border
1571
        col_widths.each do |width|
1572
          col_x += width
1573
          pdf.Line(col_x, top_y, col_x, lower_y)  # columns right border
1574
        end
1575
        pdf.Line(top_x, top_y, top_x, lower_y)    # left border
1576
        pdf.Line(top_x, lower_y, col_x, lower_y)  # bottom border
1577
      end
1578
1579 0:513646585e45 Chris
      # Returns a PDF string of a single issue
1580 1115:433d4f72a19b Chris
      def issue_to_pdf(issue, assoc={})
1581 441:cbce1fd3b1b7 Chris
        pdf = ITCPDF.new(current_language)
1582 1115:433d4f72a19b Chris
        pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}")
1583 441:cbce1fd3b1b7 Chris
        pdf.alias_nb_pages
1584 0:513646585e45 Chris
        pdf.footer_date = format_date(Date.today)
1585
        pdf.AddPage
1586 441:cbce1fd3b1b7 Chris
        pdf.SetFontStyle('B',11)
1587 1115:433d4f72a19b Chris
        buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
1588 909:cbb26bc654de Chris
        pdf.RDMMultiCell(190, 5, buf)
1589
        pdf.SetFontStyle('',8)
1590
        base_x = pdf.GetX
1591
        i = 1
1592 1115:433d4f72a19b Chris
        issue.ancestors.visible.each do |ancestor|
1593 909:cbb26bc654de Chris
          pdf.SetX(base_x + i)
1594
          buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
1595
          pdf.RDMMultiCell(190 - i, 5, buf)
1596
          i += 1 if i < 35
1597
        end
1598 1115:433d4f72a19b Chris
        pdf.SetFontStyle('B',11)
1599
        pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
1600
        pdf.SetFontStyle('',8)
1601
        pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
1602 0:513646585e45 Chris
        pdf.Ln
1603 441:cbce1fd3b1b7 Chris
1604 1115:433d4f72a19b Chris
        left = []
1605
        left << [l(:field_status), issue.status]
1606
        left << [l(:field_priority), issue.priority]
1607
        left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
1608
        left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
1609
        left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
1610 441:cbce1fd3b1b7 Chris
1611 1115:433d4f72a19b Chris
        right = []
1612
        right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
1613
        right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
1614
        right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
1615
        right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
1616
        right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
1617 441:cbce1fd3b1b7 Chris
1618 1115:433d4f72a19b Chris
        rows = left.size > right.size ? left.size : right.size
1619
        while left.size < rows
1620
          left << nil
1621
        end
1622
        while right.size < rows
1623
          right << nil
1624 0:513646585e45 Chris
        end
1625 441:cbce1fd3b1b7 Chris
1626 1464:261b3d9a4903 Chris
        half = (issue.visible_custom_field_values.size / 2.0).ceil
1627
        issue.visible_custom_field_values.each_with_index do |custom_value, i|
1628 1517:dffacf8a6908 Chris
          (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
1629 1115:433d4f72a19b Chris
        end
1630 507:0c939c159af4 Chris
1631 1115:433d4f72a19b Chris
        rows = left.size > right.size ? left.size : right.size
1632
        rows.times do |i|
1633
          item = left[i]
1634
          pdf.SetFontStyle('B',9)
1635
          pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
1636
          pdf.SetFontStyle('',9)
1637
          pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
1638
1639
          item = right[i]
1640
          pdf.SetFontStyle('B',9)
1641
          pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
1642
          pdf.SetFontStyle('',9)
1643
          pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
1644
          pdf.Ln
1645
        end
1646 441:cbce1fd3b1b7 Chris
1647 0:513646585e45 Chris
        pdf.SetFontStyle('B',9)
1648 507:0c939c159af4 Chris
        pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
1649 0:513646585e45 Chris
        pdf.SetFontStyle('',9)
1650 909:cbb26bc654de Chris
1651
        # Set resize image scale
1652
        pdf.SetImageScale(1.6)
1653
        pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
1654
              issue.description.to_s, issue.attachments, "LRB")
1655
1656
        unless issue.leaf?
1657
          # for CJK
1658
          truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
1659
          pdf.SetFontStyle('B',9)
1660
          pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
1661
          pdf.Ln
1662 1115:433d4f72a19b Chris
          issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
1663 1517:dffacf8a6908 Chris
            buf = "#{child.tracker} # #{child.id}: #{child.subject}".
1664
                    truncate(truncate_length)
1665 909:cbb26bc654de Chris
            level = 10 if level >= 10
1666
            pdf.SetFontStyle('',8)
1667
            pdf.RDMCell(35+135,5, (level >=1 ? "  " * level : "") + buf, "L")
1668
            pdf.SetFontStyle('B',8)
1669
            pdf.RDMCell(20,5, child.status.to_s, "R")
1670
            pdf.Ln
1671
          end
1672
        end
1673
1674
        relations = issue.relations.select { |r| r.other_issue(issue).visible? }
1675
        unless relations.empty?
1676
          # for CJK
1677
          truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
1678
          pdf.SetFontStyle('B',9)
1679
          pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
1680
          pdf.Ln
1681
          relations.each do |relation|
1682
            buf = ""
1683
            buf += "#{l(relation.label_for(issue))} "
1684
            if relation.delay && relation.delay != 0
1685
              buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
1686
            end
1687
            if Setting.cross_project_issue_relations?
1688
              buf += "#{relation.other_issue(issue).project} - "
1689
            end
1690
            buf += "#{relation.other_issue(issue).tracker}" +
1691
                   " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
1692 1517:dffacf8a6908 Chris
            buf = buf.truncate(truncate_length)
1693 909:cbb26bc654de Chris
            pdf.SetFontStyle('', 8)
1694
            pdf.RDMCell(35+155-60, 5, buf, "L")
1695
            pdf.SetFontStyle('B',8)
1696
            pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
1697
            pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
1698
            pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
1699
            pdf.Ln
1700
          end
1701
        end
1702
        pdf.RDMCell(190,5, "", "T")
1703 0:513646585e45 Chris
        pdf.Ln
1704 441:cbce1fd3b1b7 Chris
1705
        if issue.changesets.any? &&
1706
             User.current.allowed_to?(:view_changesets, issue.project)
1707 0:513646585e45 Chris
          pdf.SetFontStyle('B',9)
1708 441:cbce1fd3b1b7 Chris
          pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
1709 0:513646585e45 Chris
          pdf.Ln
1710
          for changeset in issue.changesets
1711
            pdf.SetFontStyle('B',8)
1712 507:0c939c159af4 Chris
            csstr  = "#{l(:label_revision)} #{changeset.format_identifier} - "
1713
            csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
1714
            pdf.RDMCell(190, 5, csstr)
1715 0:513646585e45 Chris
            pdf.Ln
1716
            unless changeset.comments.blank?
1717
              pdf.SetFontStyle('',8)
1718 909:cbb26bc654de Chris
              pdf.RDMwriteHTMLCell(190,5,0,0,
1719
                    changeset.comments.to_s, issue.attachments, "")
1720 441:cbce1fd3b1b7 Chris
            end
1721 0:513646585e45 Chris
            pdf.Ln
1722
          end
1723
        end
1724 441:cbce1fd3b1b7 Chris
1725 1115:433d4f72a19b Chris
        if assoc[:journals].present?
1726
          pdf.SetFontStyle('B',9)
1727
          pdf.RDMCell(190,5, l(:label_history), "B")
1728 0:513646585e45 Chris
          pdf.Ln
1729 1115:433d4f72a19b Chris
          assoc[:journals].each do |journal|
1730
            pdf.SetFontStyle('B',8)
1731
            title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
1732
            title << " (#{l(:field_private_notes)})" if journal.private_notes?
1733
            pdf.RDMCell(190,5, title)
1734
            pdf.Ln
1735
            pdf.SetFontStyle('I',8)
1736 1464:261b3d9a4903 Chris
            details_to_strings(journal.visible_details, true).each do |string|
1737 1115:433d4f72a19b Chris
              pdf.RDMMultiCell(190,5, "- " + string)
1738
            end
1739
            if journal.notes?
1740
              pdf.Ln unless journal.details.empty?
1741
              pdf.SetFontStyle('',8)
1742
              pdf.RDMwriteHTMLCell(190,5,0,0,
1743
                    journal.notes.to_s, issue.attachments, "")
1744
            end
1745
            pdf.Ln
1746 0:513646585e45 Chris
          end
1747
        end
1748 441:cbce1fd3b1b7 Chris
1749 0:513646585e45 Chris
        if issue.attachments.any?
1750
          pdf.SetFontStyle('B',9)
1751 441:cbce1fd3b1b7 Chris
          pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
1752 0:513646585e45 Chris
          pdf.Ln
1753
          for attachment in issue.attachments
1754
            pdf.SetFontStyle('',8)
1755 441:cbce1fd3b1b7 Chris
            pdf.RDMCell(80,5, attachment.filename)
1756
            pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
1757
            pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
1758
            pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
1759 0:513646585e45 Chris
            pdf.Ln
1760
          end
1761
        end
1762
        pdf.Output
1763
      end
1764 22:40f7cfd4df19 chris
1765 1115:433d4f72a19b Chris
      # Returns a PDF string of a set of wiki pages
1766
      def wiki_pages_to_pdf(pages, project)
1767
        pdf = ITCPDF.new(current_language)
1768
        pdf.SetTitle(project.name)
1769
        pdf.alias_nb_pages
1770
        pdf.footer_date = format_date(Date.today)
1771
        pdf.AddPage
1772
        pdf.SetFontStyle('B',11)
1773
        pdf.RDMMultiCell(190,5, project.name)
1774
        pdf.Ln
1775
        # Set resize image scale
1776
        pdf.SetImageScale(1.6)
1777
        pdf.SetFontStyle('',9)
1778
        write_page_hierarchy(pdf, pages.group_by(&:parent_id))
1779
        pdf.Output
1780
      end
1781
1782 909:cbb26bc654de Chris
      # Returns a PDF string of a single wiki page
1783 1115:433d4f72a19b Chris
      def wiki_page_to_pdf(page, project)
1784 909:cbb26bc654de Chris
        pdf = ITCPDF.new(current_language)
1785
        pdf.SetTitle("#{project} - #{page.title}")
1786
        pdf.alias_nb_pages
1787
        pdf.footer_date = format_date(Date.today)
1788
        pdf.AddPage
1789
        pdf.SetFontStyle('B',11)
1790
        pdf.RDMMultiCell(190,5,
1791
             "#{project} - #{page.title} - # #{page.content.version}")
1792
        pdf.Ln
1793
        # Set resize image scale
1794
        pdf.SetImageScale(1.6)
1795
        pdf.SetFontStyle('',9)
1796 1115:433d4f72a19b Chris
        write_wiki_page(pdf, page)
1797
        pdf.Output
1798
      end
1799
1800
      def write_page_hierarchy(pdf, pages, node=nil, level=0)
1801
        if pages[node]
1802
          pages[node].each do |page|
1803
            if @new_page
1804
              pdf.AddPage
1805
            else
1806
              @new_page = true
1807
            end
1808
            pdf.Bookmark page.title, level
1809
            write_wiki_page(pdf, page)
1810
            write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
1811
          end
1812
        end
1813
      end
1814
1815
      def write_wiki_page(pdf, page)
1816 909:cbb26bc654de Chris
        pdf.RDMwriteHTMLCell(190,5,0,0,
1817 1115:433d4f72a19b Chris
              page.content.text.to_s, page.attachments, 0)
1818 909:cbb26bc654de Chris
        if page.attachments.any?
1819
          pdf.Ln
1820
          pdf.SetFontStyle('B',9)
1821
          pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
1822
          pdf.Ln
1823
          for attachment in page.attachments
1824
            pdf.SetFontStyle('',8)
1825
            pdf.RDMCell(80,5, attachment.filename)
1826
            pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
1827
            pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
1828
            pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
1829
            pdf.Ln
1830
          end
1831
        end
1832
      end
1833
1834 441:cbce1fd3b1b7 Chris
      class RDMPdfEncoding
1835 909:cbb26bc654de Chris
        def self.rdm_from_utf8(txt, encoding)
1836 441:cbce1fd3b1b7 Chris
          txt ||= ''
1837 909:cbb26bc654de Chris
          txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
1838 441:cbce1fd3b1b7 Chris
          if txt.respond_to?(:force_encoding)
1839
            txt.force_encoding('ASCII-8BIT')
1840
          end
1841
          txt
1842
        end
1843 909:cbb26bc654de Chris
1844
        def self.attach(attachments, filename, encoding)
1845
          filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
1846
          atta = nil
1847
          if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
1848
            atta = Attachment.latest_attach(attachments, filename_utf8)
1849
          end
1850
          if atta && atta.readable? && atta.visible?
1851
            return atta
1852
          else
1853
            return nil
1854
          end
1855
        end
1856 441:cbce1fd3b1b7 Chris
      end
1857 0:513646585e45 Chris
    end
1858
  end
1859
end
1860 1517:dffacf8a6908 Chris
# Redmine - project management software
1861
# Copyright (C) 2006-2014  Jean-Philippe Lang
1862
#
1863
# This program is free software; you can redistribute it and/or
1864
# modify it under the terms of the GNU General Public License
1865
# as published by the Free Software Foundation; either version 2
1866
# of the License, or (at your option) any later version.
1867
#
1868
# This program is distributed in the hope that it will be useful,
1869
# but WITHOUT ANY WARRANTY; without even the implied warranty of
1870
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1871
# GNU General Public License for more details.
1872
#
1873
# You should have received a copy of the GNU General Public License
1874
# along with this program; if not, write to the Free Software
1875
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
1876
1877
module Redmine
1878
  module FieldFormat
1879
    def self.add(name, klass)
1880
      all[name.to_s] = klass.instance
1881
    end
1882
1883
    def self.delete(name)
1884
      all.delete(name.to_s)
1885
    end
1886
1887
    def self.all
1888
      @formats ||= Hash.new(Base.instance)
1889
    end
1890
1891
    def self.available_formats
1892
      all.keys
1893
    end
1894
1895
    def self.find(name)
1896
      all[name.to_s]
1897
    end
1898
1899
    # Return an array of custom field formats which can be used in select_tag
1900
    def self.as_select(class_name=nil)
1901
      formats = all.values.select do |format|
1902
        format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
1903
      end
1904
      formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
1905
    end
1906
1907
    class Base
1908
      include Singleton
1909
      include Redmine::I18n
1910
      include ERB::Util
1911
1912
      class_attribute :format_name
1913
      self.format_name = nil
1914
1915
      # Set this to true if the format supports multiple values
1916
      class_attribute :multiple_supported
1917
      self.multiple_supported = false
1918
1919
      # Set this to true if the format supports textual search on custom values
1920
      class_attribute :searchable_supported
1921
      self.searchable_supported = false
1922
1923
      # Restricts the classes that the custom field can be added to
1924
      # Set to nil for no restrictions
1925
      class_attribute :customized_class_names
1926
      self.customized_class_names = nil
1927
1928
      # Name of the partial for editing the custom field
1929
      class_attribute :form_partial
1930
      self.form_partial = nil
1931
1932
      def self.add(name)
1933
        self.format_name = name
1934
        Redmine::FieldFormat.add(name, self)
1935
      end
1936
      private_class_method :add
1937
1938
      def self.field_attributes(*args)
1939
        CustomField.store_accessor :format_store, *args
1940
      end
1941
1942
      field_attributes :url_pattern
1943
1944
      def name
1945
        self.class.format_name
1946
      end
1947
1948
      def label
1949
        "label_#{name}"
1950
      end
1951
1952
      def cast_custom_value(custom_value)
1953
        cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
1954
      end
1955
1956
      def cast_value(custom_field, value, customized=nil)
1957
        if value.blank?
1958
          nil
1959
        elsif value.is_a?(Array)
1960
          casted = value.map do |v|
1961
            cast_single_value(custom_field, v, customized)
1962
          end
1963
          casted.compact.sort
1964
        else
1965
          cast_single_value(custom_field, value, customized)
1966
        end
1967
      end
1968
1969
      def cast_single_value(custom_field, value, customized=nil)
1970
        value.to_s
1971
      end
1972
1973
      def target_class
1974
        nil
1975
      end
1976
1977
      def possible_custom_value_options(custom_value)
1978
        possible_values_options(custom_value.custom_field, custom_value.customized)
1979
      end
1980
1981
      def possible_values_options(custom_field, object=nil)
1982
        []
1983
      end
1984
1985
      # Returns the validation errors for custom_field
1986
      # Should return an empty array if custom_field is valid
1987
      def validate_custom_field(custom_field)
1988
        []
1989
      end
1990
1991
      # Returns the validation error messages for custom_value
1992
      # Should return an empty array if custom_value is valid
1993
      def validate_custom_value(custom_value)
1994
        values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
1995
        errors = values.map do |value|
1996
          validate_single_value(custom_value.custom_field, value, custom_value.customized)
1997
        end
1998
        errors.flatten.uniq
1999
      end
2000
2001
      def validate_single_value(custom_field, value, customized=nil)
2002
        []
2003
      end
2004
2005
      def formatted_custom_value(view, custom_value, html=false)
2006
        formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
2007
      end
2008
2009
      def formatted_value(view, custom_field, value, customized=nil, html=false)
2010
        casted = cast_value(custom_field, value, customized)
2011
        if html && custom_field.url_pattern.present?
2012
          texts_and_urls = Array.wrap(casted).map do |single_value|
2013
            text = view.format_object(single_value, false).to_s
2014
            url = url_from_pattern(custom_field, single_value, customized)
2015
            [text, url]
2016
          end
2017
          links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
2018
          links.join(', ').html_safe
2019
        else
2020
          casted
2021
        end
2022
      end
2023
2024
      # Returns an URL generated with the custom field URL pattern
2025
      # and variables substitution:
2026
      # %value% => the custom field value
2027
      # %id% => id of the customized object
2028
      # %project_id% => id of the project of the customized object if defined
2029
      # %project_identifier% => identifier of the project of the customized object if defined
2030
      # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
2031
      def url_from_pattern(custom_field, value, customized)
2032
        url = custom_field.url_pattern.to_s.dup
2033
        url.gsub!('%value%') {value.to_s}
2034
        url.gsub!('%id%') {customized.id.to_s}
2035
        url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
2036
        url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
2037
        if custom_field.regexp.present?
2038
          url.gsub!(%r{%m(\d+)%}) do
2039
            m = $1.to_i
2040
            if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
2041
              matches[m].to_s
2042
            end
2043
          end
2044
        end
2045
        url
2046
      end
2047
      protected :url_from_pattern
2048
2049
      def edit_tag(view, tag_id, tag_name, custom_value, options={})
2050
        view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
2051
      end
2052
2053
      def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
2054
        view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
2055
          bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
2056
      end
2057
2058
      def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
2059
        if custom_field.is_required?
2060
          ''.html_safe
2061
        else
2062
          view.content_tag('label',
2063
            view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
2064
            :class => 'inline'
2065
          )
2066
        end
2067
      end
2068
      protected :bulk_clear_tag
2069
2070
      def query_filter_options(custom_field, query)
2071
        {:type => :string}
2072
      end
2073
2074
      def before_custom_field_save(custom_field)
2075
      end
2076
2077
      # Returns a ORDER BY clause that can used to sort customized
2078
      # objects by their value of the custom field.
2079
      # Returns nil if the custom field can not be used for sorting.
2080
      def order_statement(custom_field)
2081
        # COALESCE is here to make sure that blank and NULL values are sorted equally
2082
        "COALESCE(#{join_alias custom_field}.value, '')"
2083
      end
2084
2085
      # Returns a GROUP BY clause that can used to group by custom value
2086
      # Returns nil if the custom field can not be used for grouping.
2087
      def group_statement(custom_field)
2088
        nil
2089
      end
2090
2091
      # Returns a JOIN clause that is added to the query when sorting by custom values
2092
      def join_for_order_statement(custom_field)
2093
        alias_name = join_alias(custom_field)
2094
2095
        "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
2096
          " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
2097
          " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
2098
          " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
2099
          " AND (#{custom_field.visibility_by_project_condition})" +
2100
          " AND #{alias_name}.value <> ''" +
2101
          " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
2102
            " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
2103
            " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
2104
            " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
2105
      end
2106
2107
      def join_alias(custom_field)
2108
        "cf_#{custom_field.id}"
2109
      end
2110
      protected :join_alias
2111
    end
2112
2113
    class Unbounded < Base
2114
      def validate_single_value(custom_field, value, customized=nil)
2115
        errs = super
2116
        value = value.to_s
2117
        unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
2118
          errs << ::I18n.t('activerecord.errors.messages.invalid')
2119
        end
2120
        if custom_field.min_length && value.length < custom_field.min_length
2121
          errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
2122
        end
2123
        if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
2124
          errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
2125
        end
2126
        errs
2127
      end
2128
    end
2129
2130
    class StringFormat < Unbounded
2131
      add 'string'
2132
      self.searchable_supported = true
2133
      self.form_partial = 'custom_fields/formats/string'
2134
      field_attributes :text_formatting
2135
2136
      def formatted_value(view, custom_field, value, customized=nil, html=false)
2137
        if html
2138
          if custom_field.url_pattern.present?
2139
            super
2140
          elsif custom_field.text_formatting == 'full'
2141
            view.textilizable(value, :object => customized)
2142
          else
2143
            value.to_s
2144
          end
2145
        else
2146
          value.to_s
2147
        end
2148
      end
2149
    end
2150
2151
    class TextFormat < Unbounded
2152
      add 'text'
2153
      self.searchable_supported = true
2154
      self.form_partial = 'custom_fields/formats/text'
2155
2156
      def formatted_value(view, custom_field, value, customized=nil, html=false)
2157
        if html
2158
          if custom_field.text_formatting == 'full'
2159
            view.textilizable(value, :object => customized)
2160
          else
2161
            view.simple_format(html_escape(value))
2162
          end
2163
        else
2164
          value.to_s
2165
        end
2166
      end
2167
2168
      def edit_tag(view, tag_id, tag_name, custom_value, options={})
2169
        view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
2170
      end
2171
2172
      def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
2173
        view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
2174
          '<br />'.html_safe +
2175
          bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
2176
      end
2177
2178
      def query_filter_options(custom_field, query)
2179
        {:type => :text}
2180
      end
2181
    end
2182
2183
    class LinkFormat < StringFormat
2184
      add 'link'
2185
      self.searchable_supported = false
2186
      self.form_partial = 'custom_fields/formats/link'
2187
2188
      def formatted_value(view, custom_field, value, customized=nil, html=false)
2189
        if html
2190
          if custom_field.url_pattern.present?
2191
            url = url_from_pattern(custom_field, value, customized)
2192
          else
2193
            url = value.to_s
2194
            unless url =~ %r{\A[a-z]+://}i
2195
              # no protocol found, use http by default
2196
              url = "http://" + url
2197
            end
2198
          end
2199
          view.link_to value.to_s, url
2200
        else
2201
          value.to_s
2202
        end
2203
      end
2204
    end
2205
2206
    class Numeric < Unbounded
2207
      self.form_partial = 'custom_fields/formats/numeric'
2208
2209
      def order_statement(custom_field)
2210
        # Make the database cast values into numeric
2211
        # Postgresql will raise an error if a value can not be casted!
2212
        # CustomValue validations should ensure that it doesn't occur
2213
        "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
2214
      end
2215
    end
2216
2217
    class IntFormat < Numeric
2218
      add 'int'
2219
2220
      def label
2221
        "label_integer"
2222
      end
2223
2224
      def cast_single_value(custom_field, value, customized=nil)
2225
        value.to_i
2226
      end
2227
2228
      def validate_single_value(custom_field, value, customized=nil)
2229
        errs = super
2230
        errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
2231
        errs
2232
      end
2233
2234
      def query_filter_options(custom_field, query)
2235
        {:type => :integer}
2236
      end
2237
2238
      def group_statement(custom_field)
2239
        order_statement(custom_field)
2240
      end
2241
    end
2242
2243
    class FloatFormat < Numeric
2244
      add 'float'
2245
2246
      def cast_single_value(custom_field, value, customized=nil)
2247
        value.to_f
2248
      end
2249
2250
      def validate_single_value(custom_field, value, customized=nil)
2251
        errs = super
2252
        errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
2253
        errs
2254
      end
2255
2256
      def query_filter_options(custom_field, query)
2257
        {:type => :float}
2258
      end
2259
    end
2260
2261
    class DateFormat < Unbounded
2262
      add 'date'
2263
      self.form_partial = 'custom_fields/formats/date'
2264
2265
      def cast_single_value(custom_field, value, customized=nil)
2266
        value.to_date rescue nil
2267
      end
2268
2269
      def validate_single_value(custom_field, value, customized=nil)
2270
        if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
2271
          []
2272
        else
2273
          [::I18n.t('activerecord.errors.messages.not_a_date')]
2274
        end
2275
      end
2276
2277
      def edit_tag(view, tag_id, tag_name, custom_value, options={})
2278
        view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
2279
          view.calendar_for(tag_id)
2280
      end
2281
2282
      def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
2283
        view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
2284
          view.calendar_for(tag_id) +
2285
          bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
2286
      end
2287
2288
      def query_filter_options(custom_field, query)
2289
        {:type => :date}
2290
      end
2291
2292
      def group_statement(custom_field)
2293
        order_statement(custom_field)
2294
      end
2295
    end
2296
2297
    class List < Base
2298
      self.multiple_supported = true
2299
      field_attributes :edit_tag_style
2300
2301
      def edit_tag(view, tag_id, tag_name, custom_value, options={})
2302
        if custom_value.custom_field.edit_tag_style == 'check_box'
2303
          check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
2304
        else
2305
          select_edit_tag(view, tag_id, tag_name, custom_value, options)
2306
        end
2307
      end
2308
2309
      def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
2310
        opts = []
2311
        opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
2312
        opts << [l(:label_none), '__none__'] unless custom_field.is_required?
2313
        opts += possible_values_options(custom_field, objects)
2314
        view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
2315
      end
2316
2317
      def query_filter_options(custom_field, query)
2318
        {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
2319
      end
2320
2321
      protected
2322
2323
      # Renders the edit tag as a select tag
2324
      def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
2325
        blank_option = ''.html_safe
2326
        unless custom_value.custom_field.multiple?
2327
          if custom_value.custom_field.is_required?
2328
            unless custom_value.custom_field.default_value.present?
2329
              blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
2330
            end
2331
          else
2332
            blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
2333
          end
2334
        end
2335
        options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
2336
        s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
2337
        if custom_value.custom_field.multiple?
2338
          s << view.hidden_field_tag(tag_name, '')
2339
        end
2340
        s
2341
      end
2342
2343
      # Renders the edit tag as check box or radio tags
2344
      def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
2345
        opts = []
2346
        unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
2347
          opts << ["(#{l(:label_none)})", '']
2348
        end
2349
        opts += possible_custom_value_options(custom_value)
2350
        s = ''.html_safe
2351
        tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
2352
        opts.each do |label, value|
2353
          value ||= label
2354
          checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
2355
          tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
2356
          # set the id on the first tag only
2357
          tag_id = nil
2358
          s << view.content_tag('label', tag + ' ' + label)
2359
        end
2360
        if custom_value.custom_field.multiple?
2361
          s << view.hidden_field_tag(tag_name, '')
2362
        end
2363
        css = "#{options[:class]} check_box_group"
2364
        view.content_tag('span', s, options.merge(:class => css))
2365
      end
2366
    end
2367
2368
    class ListFormat < List
2369
      add 'list'
2370
      self.searchable_supported = true
2371
      self.form_partial = 'custom_fields/formats/list'
2372
2373
      def possible_custom_value_options(custom_value)
2374
        options = possible_values_options(custom_value.custom_field)
2375
        missing = [custom_value.value].flatten.reject(&:blank?) - options
2376
        if missing.any?
2377
          options += missing
2378
        end
2379
        options
2380
      end
2381
2382
      def possible_values_options(custom_field, object=nil)
2383
        custom_field.possible_values
2384
      end
2385
2386
      def validate_custom_field(custom_field)
2387
        errors = []
2388
        errors << [:possible_values, :blank] if custom_field.possible_values.blank?
2389
        errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
2390
        errors
2391
      end
2392
2393
      def validate_custom_value(custom_value)
2394
        values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
2395
        invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
2396
        if invalid_values.any?
2397
          [::I18n.t('activerecord.errors.messages.inclusion')]
2398
        else
2399
          []
2400
        end
2401
      end
2402
2403
      def group_statement(custom_field)
2404
        order_statement(custom_field)
2405
      end
2406
    end
2407
2408
    class BoolFormat < List
2409
      add 'bool'
2410
      self.multiple_supported = false
2411
      self.form_partial = 'custom_fields/formats/bool'
2412
2413
      def label
2414
        "label_boolean"
2415
      end
2416
2417
      def cast_single_value(custom_field, value, customized=nil)
2418
        value == '1' ? true : false
2419
      end
2420
2421
      def possible_values_options(custom_field, object=nil)
2422
        [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
2423
      end
2424
2425
      def group_statement(custom_field)
2426
        order_statement(custom_field)
2427
      end
2428
2429
      def edit_tag(view, tag_id, tag_name, custom_value, options={})
2430
        case custom_value.custom_field.edit_tag_style
2431
        when 'check_box'
2432
          single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
2433
        when 'radio'
2434
          check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
2435
        else
2436
          select_edit_tag(view, tag_id, tag_name, custom_value, options)
2437
        end
2438
      end
2439
2440
      # Renders the edit tag as a simple check box
2441
      def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
2442
        s = ''.html_safe
2443
        s << view.hidden_field_tag(tag_name, '0', :id => nil)
2444
        s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
2445
        view.content_tag('span', s, options)
2446
      end
2447
    end
2448
2449
    class RecordList < List
2450
      self.customized_class_names = %w(Issue TimeEntry Version Project)
2451
2452
      def cast_single_value(custom_field, value, customized=nil)
2453
        target_class.find_by_id(value.to_i) if value.present?
2454
      end
2455
2456
      def target_class
2457
        @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
2458
      end
2459
2460
      def possible_custom_value_options(custom_value)
2461
        options = possible_values_options(custom_value.custom_field, custom_value.customized)
2462
        missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
2463
        if missing.any?
2464
          options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
2465
          #TODO: use #sort_by! when ruby1.8 support is dropped
2466
          options = options.sort_by(&:first)
2467
        end
2468
        options
2469
      end
2470
2471
      def order_statement(custom_field)
2472
        if target_class.respond_to?(:fields_for_order_statement)
2473
          target_class.fields_for_order_statement(value_join_alias(custom_field))
2474
        end
2475
      end
2476
2477
      def group_statement(custom_field)
2478
        "COALESCE(#{join_alias custom_field}.value, '')"
2479
      end
2480
2481
      def join_for_order_statement(custom_field)
2482
        alias_name = join_alias(custom_field)
2483
2484
        "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
2485
          " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
2486
          " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
2487
          " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
2488
          " AND (#{custom_field.visibility_by_project_condition})" +
2489
          " AND #{alias_name}.value <> ''" +
2490
          " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
2491
            " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
2492
            " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
2493
            " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
2494
          " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
2495
          " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
2496
      end
2497
2498
      def value_join_alias(custom_field)
2499
        join_alias(custom_field) + "_" + custom_field.field_format
2500
      end
2501
      protected :value_join_alias
2502
    end
2503
2504
    class UserFormat < RecordList
2505
      add 'user'
2506
      self.form_partial = 'custom_fields/formats/user'
2507
      field_attributes :user_role
2508
2509
      def possible_values_options(custom_field, object=nil)
2510
        if object.is_a?(Array)
2511
          projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
2512
          projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
2513
        elsif object.respond_to?(:project) && object.project
2514
          scope = object.project.users
2515
          if custom_field.user_role.is_a?(Array)
2516
            role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
2517
            if role_ids.any?
2518
              scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
2519
            end
2520
          end
2521
          scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
2522
        else
2523
          []
2524
        end
2525
      end
2526
2527
      def before_custom_field_save(custom_field)
2528
        super
2529
        if custom_field.user_role.is_a?(Array)
2530
          custom_field.user_role.map!(&:to_s).reject!(&:blank?)
2531
        end
2532
      end
2533
    end
2534
2535
    class VersionFormat < RecordList
2536
      add 'version'
2537
      self.form_partial = 'custom_fields/formats/version'
2538
      field_attributes :version_status
2539
2540
      def possible_values_options(custom_field, object=nil)
2541
        if object.is_a?(Array)
2542
          projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
2543
          projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
2544
        elsif object.respond_to?(:project) && object.project
2545
          scope = object.project.shared_versions
2546
          if custom_field.version_status.is_a?(Array)
2547
            statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
2548
            if statuses.any?
2549
              scope = scope.where(:status => statuses.map(&:to_s))
2550
            end
2551
          end
2552
          scope.sort.collect {|u| [u.to_s, u.id.to_s]}
2553
        else
2554
          []
2555
        end
2556
      end
2557
2558
      def before_custom_field_save(custom_field)
2559
        super
2560
        if custom_field.version_status.is_a?(Array)
2561
          custom_field.version_status.map!(&:to_s).reject!(&:blank?)
2562
        end
2563
      end
2564
    end
2565
  end
2566
end
2567 909:cbb26bc654de Chris
# Redmine - project management software
2568 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
2569 0:513646585e45 Chris
#
2570
# This program is free software; you can redistribute it and/or
2571
# modify it under the terms of the GNU General Public License
2572
# as published by the Free Software Foundation; either version 2
2573
# of the License, or (at your option) any later version.
2574 909:cbb26bc654de Chris
#
2575 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
2576
# but WITHOUT ANY WARRANTY; without even the implied warranty of
2577
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
2578
# GNU General Public License for more details.
2579 909:cbb26bc654de Chris
#
2580 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
2581
# along with this program; if not, write to the Free Software
2582
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
2583
2584
module Redmine
2585
  module Helpers
2586 909:cbb26bc654de Chris
2587 0:513646585e45 Chris
    # Simple class to compute the start and end dates of a calendar
2588
    class Calendar
2589
      include Redmine::I18n
2590
      attr_reader :startdt, :enddt
2591 909:cbb26bc654de Chris
2592 0:513646585e45 Chris
      def initialize(date, lang = current_language, period = :month)
2593
        @date = date
2594
        @events = []
2595
        @ending_events_by_days = {}
2596
        @starting_events_by_days = {}
2597 909:cbb26bc654de Chris
        set_language_if_valid lang
2598 0:513646585e45 Chris
        case period
2599
        when :month
2600
          @startdt = Date.civil(date.year, date.month, 1)
2601
          @enddt = (@startdt >> 1)-1
2602
          # starts from the first day of the week
2603
          @startdt = @startdt - (@startdt.cwday - first_wday)%7
2604
          # ends on the last day of the week
2605
          @enddt = @enddt + (last_wday - @enddt.cwday)%7
2606
        when :week
2607
          @startdt = date - (date.cwday - first_wday)%7
2608
          @enddt = date + (last_wday - date.cwday)%7
2609
        else
2610
          raise 'Invalid period'
2611
        end
2612
      end
2613 909:cbb26bc654de Chris
2614 0:513646585e45 Chris
      # Sets calendar events
2615
      def events=(events)
2616
        @events = events
2617
        @ending_events_by_days = @events.group_by {|event| event.due_date}
2618
        @starting_events_by_days = @events.group_by {|event| event.start_date}
2619
      end
2620 909:cbb26bc654de Chris
2621 0:513646585e45 Chris
      # Returns events for the given day
2622
      def events_on(day)
2623
        ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq
2624
      end
2625 909:cbb26bc654de Chris
2626 0:513646585e45 Chris
      # Calendar current month
2627
      def month
2628
        @date.month
2629
      end
2630 909:cbb26bc654de Chris
2631 0:513646585e45 Chris
      # Return the first day of week
2632
      # 1 = Monday ... 7 = Sunday
2633
      def first_wday
2634
        case Setting.start_of_week.to_i
2635
        when 1
2636
          @first_dow ||= (1 - 1)%7 + 1
2637 441:cbce1fd3b1b7 Chris
        when 6
2638
          @first_dow ||= (6 - 1)%7 + 1
2639 0:513646585e45 Chris
        when 7
2640
          @first_dow ||= (7 - 1)%7 + 1
2641
        else
2642
          @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1
2643
        end
2644
      end
2645 909:cbb26bc654de Chris
2646 0:513646585e45 Chris
      def last_wday
2647
        @last_dow ||= (first_wday + 5)%7 + 1
2648
      end
2649 909:cbb26bc654de Chris
    end
2650 0:513646585e45 Chris
  end
2651
end
2652 245:051f544170fe Chris
# Redmine - project management software
2653 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
2654 245:051f544170fe Chris
#
2655
# This program is free software; you can redistribute it and/or
2656
# modify it under the terms of the GNU General Public License
2657
# as published by the Free Software Foundation; either version 2
2658
# of the License, or (at your option) any later version.
2659 909:cbb26bc654de Chris
#
2660 245:051f544170fe Chris
# This program is distributed in the hope that it will be useful,
2661
# but WITHOUT ANY WARRANTY; without even the implied warranty of
2662
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
2663
# GNU General Public License for more details.
2664 909:cbb26bc654de Chris
#
2665 245:051f544170fe Chris
# You should have received a copy of the GNU General Public License
2666
# along with this program; if not, write to the Free Software
2667
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
2668
2669 1464:261b3d9a4903 Chris
require 'diff'
2670
2671 245:051f544170fe Chris
module Redmine
2672
  module Helpers
2673
    class Diff
2674
      include ERB::Util
2675
      include ActionView::Helpers::TagHelper
2676
      include ActionView::Helpers::TextHelper
2677
      attr_reader :diff, :words
2678 909:cbb26bc654de Chris
2679 245:051f544170fe Chris
      def initialize(content_to, content_from)
2680
        @words = content_to.to_s.split(/(\s+)/)
2681
        @words = @words.select {|word| word != ' '}
2682
        words_from = content_from.to_s.split(/(\s+)/)
2683 909:cbb26bc654de Chris
        words_from = words_from.select {|word| word != ' '}
2684 245:051f544170fe Chris
        @diff = words_from.diff @words
2685
      end
2686 909:cbb26bc654de Chris
2687 245:051f544170fe Chris
      def to_html
2688
        words = self.words.collect{|word| h(word)}
2689
        words_add = 0
2690
        words_del = 0
2691
        dels = 0
2692
        del_off = 0
2693
        diff.diffs.each do |diff|
2694
          add_at = nil
2695
          add_to = nil
2696
          del_at = nil
2697 909:cbb26bc654de Chris
          deleted = ""
2698 245:051f544170fe Chris
          diff.each do |change|
2699
            pos = change[1]
2700
            if change[0] == "+"
2701
              add_at = pos + dels unless add_at
2702
              add_to = pos + dels
2703
              words_add += 1
2704
            else
2705
              del_at = pos unless del_at
2706 1115:433d4f72a19b Chris
              deleted << ' ' unless deleted.empty?
2707
              deleted << h(change[2])
2708 245:051f544170fe Chris
              words_del  += 1
2709
            end
2710
          end
2711
          if add_at
2712 1115:433d4f72a19b Chris
            words[add_at] = '<span class="diff_in">'.html_safe + words[add_at]
2713
            words[add_to] = words[add_to] + '</span>'.html_safe
2714 245:051f544170fe Chris
          end
2715
          if del_at
2716 1115:433d4f72a19b Chris
            words.insert del_at - del_off + dels + words_add, '<span class="diff_out">'.html_safe + deleted + '</span>'.html_safe
2717 245:051f544170fe Chris
            dels += 1
2718
            del_off += words_del
2719
            words_del = 0
2720
          end
2721
        end
2722 909:cbb26bc654de Chris
        words.join(' ').html_safe
2723 245:051f544170fe Chris
      end
2724
    end
2725
  end
2726
end
2727 0:513646585e45 Chris
# Redmine - project management software
2728 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
2729 0:513646585e45 Chris
#
2730
# This program is free software; you can redistribute it and/or
2731
# modify it under the terms of the GNU General Public License
2732
# as published by the Free Software Foundation; either version 2
2733
# of the License, or (at your option) any later version.
2734 441:cbce1fd3b1b7 Chris
#
2735 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
2736
# but WITHOUT ANY WARRANTY; without even the implied warranty of
2737
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
2738
# GNU General Public License for more details.
2739 441:cbce1fd3b1b7 Chris
#
2740 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
2741
# along with this program; if not, write to the Free Software
2742
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
2743
2744
module Redmine
2745
  module Helpers
2746
    # Simple class to handle gantt chart data
2747
    class Gantt
2748 22:40f7cfd4df19 chris
      include ERB::Util
2749
      include Redmine::I18n
2750 1115:433d4f72a19b Chris
      include Redmine::Utils::DateCalculation
2751 22:40f7cfd4df19 chris
2752 1464:261b3d9a4903 Chris
      # Relation types that are rendered
2753
      DRAW_TYPES = {
2754
        IssueRelation::TYPE_BLOCKS   => { :landscape_margin => 16, :color => '#F34F4F' },
2755
        IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
2756
      }.freeze
2757
2758 22:40f7cfd4df19 chris
      # :nodoc:
2759
      # Some utility methods for the PDF export
2760
      class PDF
2761
        MaxCharactorsForSubject = 45
2762
        TotalWidth = 280
2763
        LeftPaneWidth = 100
2764
2765
        def self.right_pane_width
2766
          TotalWidth - LeftPaneWidth
2767
        end
2768
      end
2769
2770 119:8661b858af72 Chris
      attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
2771 22:40f7cfd4df19 chris
      attr_accessor :query
2772
      attr_accessor :project
2773
      attr_accessor :view
2774 441:cbce1fd3b1b7 Chris
2775 0:513646585e45 Chris
      def initialize(options={})
2776
        options = options.dup
2777
        if options[:year] && options[:year].to_i >0
2778
          @year_from = options[:year].to_i
2779
          if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
2780
            @month_from = options[:month].to_i
2781
          else
2782
            @month_from = 1
2783
          end
2784
        else
2785
          @month_from ||= Date.today.month
2786
          @year_from ||= Date.today.year
2787
        end
2788
        zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
2789 441:cbce1fd3b1b7 Chris
        @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
2790 0:513646585e45 Chris
        months = (options[:months] || User.current.pref[:gantt_months]).to_i
2791
        @months = (months > 0 && months < 25) ? months : 6
2792
        # Save gantt parameters as user preference (zoom and months count)
2793 1115:433d4f72a19b Chris
        if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
2794
              @months != User.current.pref[:gantt_months]))
2795 0:513646585e45 Chris
          User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
2796
          User.current.preference.save
2797
        end
2798
        @date_from = Date.civil(@year_from, @month_from, 1)
2799
        @date_to = (@date_from >> @months) - 1
2800 119:8661b858af72 Chris
        @subjects = ''
2801
        @lines = ''
2802
        @number_of_rows = nil
2803
        @issue_ancestors = []
2804
        @truncated = false
2805
        if options.has_key?(:max_rows)
2806
          @max_rows = options[:max_rows]
2807
        else
2808
          @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
2809
        end
2810 0:513646585e45 Chris
      end
2811 22:40f7cfd4df19 chris
2812
      def common_params
2813
        { :controller => 'gantts', :action => 'show', :project_id => @project }
2814 0:513646585e45 Chris
      end
2815 441:cbce1fd3b1b7 Chris
2816 0:513646585e45 Chris
      def params
2817 1115:433d4f72a19b Chris
        common_params.merge({:zoom => zoom, :year => year_from,
2818
                             :month => month_from, :months => months})
2819 0:513646585e45 Chris
      end
2820 441:cbce1fd3b1b7 Chris
2821 0:513646585e45 Chris
      def params_previous
2822 1115:433d4f72a19b Chris
        common_params.merge({:year => (date_from << months).year,
2823
                             :month => (date_from << months).month,
2824
                             :zoom => zoom, :months => months})
2825 0:513646585e45 Chris
      end
2826 441:cbce1fd3b1b7 Chris
2827 0:513646585e45 Chris
      def params_next
2828 1115:433d4f72a19b Chris
        common_params.merge({:year => (date_from >> months).year,
2829
                             :month => (date_from >> months).month,
2830
                             :zoom => zoom, :months => months})
2831 0:513646585e45 Chris
      end
2832 22:40f7cfd4df19 chris
2833
      # Returns the number of rows that will be rendered on the Gantt chart
2834
      def number_of_rows
2835 119:8661b858af72 Chris
        return @number_of_rows if @number_of_rows
2836 441:cbce1fd3b1b7 Chris
        rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
2837 119:8661b858af72 Chris
        rows > @max_rows ? @max_rows : rows
2838 22:40f7cfd4df19 chris
      end
2839
2840
      # Returns the number of rows that will be used to list a project on
2841
      # the Gantt chart.  This will recurse for each subproject.
2842
      def number_of_rows_on_project(project)
2843 441:cbce1fd3b1b7 Chris
        return 0 unless projects.include?(project)
2844 22:40f7cfd4df19 chris
        count = 1
2845 441:cbce1fd3b1b7 Chris
        count += project_issues(project).size
2846
        count += project_versions(project).size
2847 22:40f7cfd4df19 chris
        count
2848
      end
2849
2850
      # Renders the subjects of the Gantt chart, the left side.
2851
      def subjects(options={})
2852 119:8661b858af72 Chris
        render(options.merge(:only => :subjects)) unless @subjects_rendered
2853
        @subjects
2854 22:40f7cfd4df19 chris
      end
2855
2856
      # Renders the lines of the Gantt chart, the right side
2857
      def lines(options={})
2858 119:8661b858af72 Chris
        render(options.merge(:only => :lines)) unless @lines_rendered
2859
        @lines
2860
      end
2861 441:cbce1fd3b1b7 Chris
2862
      # Returns issues that will be rendered
2863
      def issues
2864
        @issues ||= @query.issues(
2865
          :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
2866
          :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
2867
          :limit => @max_rows
2868
        )
2869
      end
2870
2871 1464:261b3d9a4903 Chris
      # Returns a hash of the relations between the issues that are present on the gantt
2872
      # and that should be displayed, grouped by issue ids.
2873
      def relations
2874
        return @relations if @relations
2875
        if issues.any?
2876
          issue_ids = issues.map(&:id)
2877
          @relations = IssueRelation.
2878
            where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys).
2879
            group_by(&:issue_from_id)
2880
        else
2881
          @relations = {}
2882
        end
2883
      end
2884
2885 441:cbce1fd3b1b7 Chris
      # Return all the project nodes that will be displayed
2886
      def projects
2887
        return @projects if @projects
2888
        ids = issues.collect(&:project).uniq.collect(&:id)
2889
        if ids.any?
2890
          # All issues projects and their visible ancestors
2891 1464:261b3d9a4903 Chris
          @projects = Project.visible.
2892
            joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt").
2893
            where("child.id IN (?)", ids).
2894
            order("#{Project.table_name}.lft ASC").
2895
            uniq.
2896
            all
2897 441:cbce1fd3b1b7 Chris
        else
2898
          @projects = []
2899
        end
2900
      end
2901
2902
      # Returns the issues that belong to +project+
2903
      def project_issues(project)
2904
        @issues_by_project ||= issues.group_by(&:project)
2905
        @issues_by_project[project] || []
2906
      end
2907
2908
      # Returns the distinct versions of the issues that belong to +project+
2909
      def project_versions(project)
2910
        project_issues(project).collect(&:fixed_version).compact.uniq
2911
      end
2912
2913
      # Returns the issues that belong to +project+ and are assigned to +version+
2914
      def version_issues(project, version)
2915
        project_issues(project).select {|issue| issue.fixed_version == version}
2916
      end
2917
2918 119:8661b858af72 Chris
      def render(options={})
2919 1115:433d4f72a19b Chris
        options = {:top => 0, :top_increment => 20,
2920
                   :indent_increment => 20, :render => :subject,
2921
                   :format => :html}.merge(options)
2922 441:cbce1fd3b1b7 Chris
        indent = options[:indent] || 4
2923 119:8661b858af72 Chris
        @subjects = '' unless options[:only] == :lines
2924
        @lines = '' unless options[:only] == :subjects
2925
        @number_of_rows = 0
2926 441:cbce1fd3b1b7 Chris
        Project.project_tree(projects) do |project, level|
2927
          options[:indent] = indent + level * options[:indent_increment]
2928
          render_project(project, options)
2929
          break if abort?
2930 22:40f7cfd4df19 chris
        end
2931 119:8661b858af72 Chris
        @subjects_rendered = true unless options[:only] == :lines
2932
        @lines_rendered = true unless options[:only] == :subjects
2933
        render_end(options)
2934 22:40f7cfd4df19 chris
      end
2935
2936
      def render_project(project, options={})
2937 119:8661b858af72 Chris
        subject_for_project(project, options) unless options[:only] == :lines
2938
        line_for_project(project, options) unless options[:only] == :subjects
2939 22:40f7cfd4df19 chris
        options[:top] += options[:top_increment]
2940
        options[:indent] += options[:indent_increment]
2941 119:8661b858af72 Chris
        @number_of_rows += 1
2942
        return if abort?
2943 441:cbce1fd3b1b7 Chris
        issues = project_issues(project).select {|i| i.fixed_version.nil?}
2944 1464:261b3d9a4903 Chris
        self.class.sort_issues!(issues)
2945 22:40f7cfd4df19 chris
        if issues
2946 119:8661b858af72 Chris
          render_issues(issues, options)
2947
          return if abort?
2948 22:40f7cfd4df19 chris
        end
2949 441:cbce1fd3b1b7 Chris
        versions = project_versions(project)
2950 1464:261b3d9a4903 Chris
        self.class.sort_versions!(versions)
2951 441:cbce1fd3b1b7 Chris
        versions.each do |version|
2952
          render_version(project, version, options)
2953 22:40f7cfd4df19 chris
        end
2954
        # Remove indent to hit the next sibling
2955
        options[:indent] -= options[:indent_increment]
2956
      end
2957
2958
      def render_issues(issues, options={})
2959 119:8661b858af72 Chris
        @issue_ancestors = []
2960 22:40f7cfd4df19 chris
        issues.each do |i|
2961 119:8661b858af72 Chris
          subject_for_issue(i, options) unless options[:only] == :lines
2962
          line_for_issue(i, options) unless options[:only] == :subjects
2963 22:40f7cfd4df19 chris
          options[:top] += options[:top_increment]
2964 119:8661b858af72 Chris
          @number_of_rows += 1
2965
          break if abort?
2966 22:40f7cfd4df19 chris
        end
2967 119:8661b858af72 Chris
        options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
2968 22:40f7cfd4df19 chris
      end
2969
2970 441:cbce1fd3b1b7 Chris
      def render_version(project, version, options={})
2971 22:40f7cfd4df19 chris
        # Version header
2972 119:8661b858af72 Chris
        subject_for_version(version, options) unless options[:only] == :lines
2973
        line_for_version(version, options) unless options[:only] == :subjects
2974 22:40f7cfd4df19 chris
        options[:top] += options[:top_increment]
2975 119:8661b858af72 Chris
        @number_of_rows += 1
2976
        return if abort?
2977 441:cbce1fd3b1b7 Chris
        issues = version_issues(project, version)
2978 22:40f7cfd4df19 chris
        if issues
2979 1464:261b3d9a4903 Chris
          self.class.sort_issues!(issues)
2980 22:40f7cfd4df19 chris
          # Indent issues
2981
          options[:indent] += options[:indent_increment]
2982 119:8661b858af72 Chris
          render_issues(issues, options)
2983 22:40f7cfd4df19 chris
          options[:indent] -= options[:indent_increment]
2984
        end
2985 119:8661b858af72 Chris
      end
2986 441:cbce1fd3b1b7 Chris
2987 119:8661b858af72 Chris
      def render_end(options={})
2988
        case options[:format]
2989 441:cbce1fd3b1b7 Chris
        when :pdf
2990 119:8661b858af72 Chris
          options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
2991
        end
2992 22:40f7cfd4df19 chris
      end
2993
2994
      def subject_for_project(project, options)
2995
        case options[:format]
2996
        when :html
2997 1115:433d4f72a19b Chris
          html_class = ""
2998
          html_class << 'icon icon-projects '
2999
          html_class << (project.overdue? ? 'project-overdue' : '')
3000
          s = view.link_to_project(project).html_safe
3001
          subject = view.content_tag(:span, s,
3002
                                     :class => html_class).html_safe
3003 119:8661b858af72 Chris
          html_subject(options, subject, :css => "project-name")
3004 22:40f7cfd4df19 chris
        when :image
3005 119:8661b858af72 Chris
          image_subject(options, project.name)
3006 22:40f7cfd4df19 chris
        when :pdf
3007 119:8661b858af72 Chris
          pdf_new_page?(options)
3008
          pdf_subject(options, project.name)
3009 22:40f7cfd4df19 chris
        end
3010
      end
3011
3012
      def line_for_project(project, options)
3013 37:94944d00e43c chris
        # Skip versions that don't have a start_date or due date
3014
        if project.is_a?(Project) && project.start_date && project.due_date
3015 22:40f7cfd4df19 chris
          options[:zoom] ||= 1
3016
          options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
3017 119:8661b858af72 Chris
          coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
3018
          label = h(project)
3019 22:40f7cfd4df19 chris
          case options[:format]
3020
          when :html
3021 119:8661b858af72 Chris
            html_task(options, coords, :css => "project task", :label => label, :markers => true)
3022 22:40f7cfd4df19 chris
          when :image
3023 119:8661b858af72 Chris
            image_task(options, coords, :label => label, :markers => true, :height => 3)
3024 22:40f7cfd4df19 chris
          when :pdf
3025 119:8661b858af72 Chris
            pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
3026 22:40f7cfd4df19 chris
          end
3027
        else
3028
          ''
3029
        end
3030
      end
3031
3032
      def subject_for_version(version, options)
3033
        case options[:format]
3034
        when :html
3035 1115:433d4f72a19b Chris
          html_class = ""
3036
          html_class << 'icon icon-package '
3037
          html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
3038
          html_class << (version.overdue? ? 'version-overdue' : '')
3039 1464:261b3d9a4903 Chris
          html_class << ' version-closed' unless version.open?
3040 1517:dffacf8a6908 Chris
          if version.start_date && version.due_date && version.completed_percent
3041 1464:261b3d9a4903 Chris
            progress_date = calc_progress_date(version.start_date,
3042 1517:dffacf8a6908 Chris
                                               version.due_date, version.completed_percent)
3043 1464:261b3d9a4903 Chris
            html_class << ' behind-start-date' if progress_date < self.date_from
3044
            html_class << ' over-end-date' if progress_date > self.date_to
3045
          end
3046 1115:433d4f72a19b Chris
          s = view.link_to_version(version).html_safe
3047
          subject = view.content_tag(:span, s,
3048
                                     :class => html_class).html_safe
3049 1464:261b3d9a4903 Chris
          html_subject(options, subject, :css => "version-name",
3050
                       :id => "version-#{version.id}")
3051 22:40f7cfd4df19 chris
        when :image
3052 119:8661b858af72 Chris
          image_subject(options, version.to_s_with_project)
3053 22:40f7cfd4df19 chris
        when :pdf
3054 119:8661b858af72 Chris
          pdf_new_page?(options)
3055
          pdf_subject(options, version.to_s_with_project)
3056 22:40f7cfd4df19 chris
        end
3057
      end
3058
3059
      def line_for_version(version, options)
3060
        # Skip versions that don't have a start_date
3061 1464:261b3d9a4903 Chris
        if version.is_a?(Version) && version.due_date && version.start_date
3062 22:40f7cfd4df19 chris
          options[:zoom] ||= 1
3063
          options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
3064 1115:433d4f72a19b Chris
          coords = coordinates(version.start_date,
3065 1464:261b3d9a4903 Chris
                               version.due_date, version.completed_percent,
3066 1115:433d4f72a19b Chris
                               options[:zoom])
3067 1464:261b3d9a4903 Chris
          label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
3068 119:8661b858af72 Chris
          label = h("#{version.project} -") + label unless @project && @project == version.project
3069 22:40f7cfd4df19 chris
          case options[:format]
3070
          when :html
3071 1464:261b3d9a4903 Chris
            html_task(options, coords, :css => "version task",
3072
                      :label => label, :markers => true, :version => version)
3073 22:40f7cfd4df19 chris
          when :image
3074 119:8661b858af72 Chris
            image_task(options, coords, :label => label, :markers => true, :height => 3)
3075 22:40f7cfd4df19 chris
          when :pdf
3076 119:8661b858af72 Chris
            pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
3077 22:40f7cfd4df19 chris
          end
3078
        else
3079
          ''
3080
        end
3081
      end
3082
3083
      def subject_for_issue(issue, options)
3084 119:8661b858af72 Chris
        while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
3085
          @issue_ancestors.pop
3086
          options[:indent] -= options[:indent_increment]
3087
        end
3088
        output = case options[:format]
3089 22:40f7cfd4df19 chris
        when :html
3090 119:8661b858af72 Chris
          css_classes = ''
3091
          css_classes << ' issue-overdue' if issue.overdue?
3092
          css_classes << ' issue-behind-schedule' if issue.behind_schedule?
3093
          css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
3094 1464:261b3d9a4903 Chris
          css_classes << ' issue-closed' if issue.closed?
3095
          if issue.start_date && issue.due_before && issue.done_ratio
3096
            progress_date = calc_progress_date(issue.start_date,
3097
                                               issue.due_before, issue.done_ratio)
3098
            css_classes << ' behind-start-date' if progress_date < self.date_from
3099
            css_classes << ' over-end-date' if progress_date > self.date_to
3100
          end
3101 1115:433d4f72a19b Chris
          s = "".html_safe
3102 119:8661b858af72 Chris
          if issue.assigned_to.present?
3103
            assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
3104 1115:433d4f72a19b Chris
            s << view.avatar(issue.assigned_to,
3105
                             :class => 'gravatar icon-gravatar',
3106
                             :size => 10,
3107
                             :title => assigned_string).to_s.html_safe
3108 119:8661b858af72 Chris
          end
3109 1115:433d4f72a19b Chris
          s << view.link_to_issue(issue).html_safe
3110
          subject = view.content_tag(:span, s, :class => css_classes).html_safe
3111
          html_subject(options, subject, :css => "issue-subject",
3112 1464:261b3d9a4903 Chris
                       :title => issue.subject, :id => "issue-#{issue.id}") + "\n"
3113 119:8661b858af72 Chris
        when :image
3114
          image_subject(options, issue.subject)
3115
        when :pdf
3116
          pdf_new_page?(options)
3117
          pdf_subject(options, issue.subject)
3118
        end
3119
        unless issue.leaf?
3120
          @issue_ancestors << issue
3121
          options[:indent] += options[:indent_increment]
3122
        end
3123
        output
3124 22:40f7cfd4df19 chris
      end
3125
3126
      def line_for_issue(issue, options)
3127
        # Skip issues that don't have a due_before (due_date or version's due_date)
3128
        if issue.is_a?(Issue) && issue.due_before
3129 119:8661b858af72 Chris
          coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
3130 1115:433d4f72a19b Chris
          label = "#{issue.status.name} #{issue.done_ratio}%"
3131 22:40f7cfd4df19 chris
          case options[:format]
3132
          when :html
3133 1115:433d4f72a19b Chris
            html_task(options, coords,
3134
                      :css => "task " + (issue.leaf? ? 'leaf' : 'parent'),
3135
                      :label => label, :issue => issue,
3136
                      :markers => !issue.leaf?)
3137 22:40f7cfd4df19 chris
          when :image
3138 119:8661b858af72 Chris
            image_task(options, coords, :label => label)
3139 22:40f7cfd4df19 chris
          when :pdf
3140 119:8661b858af72 Chris
            pdf_task(options, coords, :label => label)
3141
        end
3142 22:40f7cfd4df19 chris
        else
3143
          ''
3144
        end
3145
      end
3146
3147 0:513646585e45 Chris
      # Generates a gantt image
3148
      # Only defined if RMagick is avalaible
3149 22:40f7cfd4df19 chris
      def to_image(format='PNG')
3150 1115:433d4f72a19b Chris
        date_to = (@date_from >> @months) - 1
3151 0:513646585e45 Chris
        show_weeks = @zoom > 1
3152
        show_days = @zoom > 2
3153 1:cca12e1c1fd4 Chris
        subject_width = 400
3154 441:cbce1fd3b1b7 Chris
        header_height = 18
3155 0:513646585e45 Chris
        # width of one day in pixels
3156 1115:433d4f72a19b Chris
        zoom = @zoom * 2
3157
        g_width = (@date_to - @date_from + 1) * zoom
3158 22:40f7cfd4df19 chris
        g_height = 20 * number_of_rows + 30
3159 1115:433d4f72a19b Chris
        headers_height = (show_weeks ? 2 * header_height : header_height)
3160 441:cbce1fd3b1b7 Chris
        height = g_height + headers_height
3161 0:513646585e45 Chris
        imgl = Magick::ImageList.new
3162 1115:433d4f72a19b Chris
        imgl.new_image(subject_width + g_width + 1, height)
3163 0:513646585e45 Chris
        gc = Magick::Draw.new
3164 1115:433d4f72a19b Chris
        gc.font = Redmine::Configuration['rmagick_font_path'] || ""
3165 0:513646585e45 Chris
        # Subjects
3166 119:8661b858af72 Chris
        gc.stroke('transparent')
3167 441:cbce1fd3b1b7 Chris
        subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
3168 0:513646585e45 Chris
        # Months headers
3169
        month_f = @date_from
3170
        left = subject_width
3171 441:cbce1fd3b1b7 Chris
        @months.times do
3172 0:513646585e45 Chris
          width = ((month_f >> 1) - month_f) * zoom
3173
          gc.fill('white')
3174
          gc.stroke('grey')
3175
          gc.stroke_width(1)
3176
          gc.rectangle(left, 0, left + width, height)
3177
          gc.fill('black')
3178
          gc.stroke('transparent')
3179
          gc.stroke_width(1)
3180
          gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
3181
          left = left + width
3182
          month_f = month_f >> 1
3183
        end
3184
        # Weeks headers
3185
        if show_weeks
3186 1115:433d4f72a19b Chris
          left = subject_width
3187
          height = header_height
3188
          if @date_from.cwday == 1
3189
            # date_from is monday
3190
            week_f = date_from
3191
          else
3192
            # find next monday after date_from
3193
            week_f = @date_from + (7 - @date_from.cwday + 1)
3194
            width = (7 - @date_from.cwday + 1) * zoom
3195
            gc.fill('white')
3196
            gc.stroke('grey')
3197
            gc.stroke_width(1)
3198
            gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1)
3199
            left = left + width
3200
          end
3201
          while week_f <= date_to
3202
            width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
3203
            gc.fill('white')
3204
            gc.stroke('grey')
3205
            gc.stroke_width(1)
3206
            gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1)
3207
            gc.fill('black')
3208
            gc.stroke('transparent')
3209
            gc.stroke_width(1)
3210
            gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
3211
            left = left + width
3212
            week_f = week_f + 7
3213
          end
3214 0:513646585e45 Chris
        end
3215
        # Days details (week-end in grey)
3216
        if show_days
3217 1115:433d4f72a19b Chris
          left = subject_width
3218
          height = g_height + header_height - 1
3219
          wday = @date_from.cwday
3220
          (date_to - @date_from + 1).to_i.times do
3221
            width =  zoom
3222
            gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
3223
            gc.stroke('#ddd')
3224
            gc.stroke_width(1)
3225
            gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1)
3226
            left = left + width
3227
            wday = wday + 1
3228
            wday = 1 if wday > 7
3229
          end
3230 0:513646585e45 Chris
        end
3231
        # border
3232
        gc.fill('transparent')
3233
        gc.stroke('grey')
3234
        gc.stroke_width(1)
3235 1115:433d4f72a19b Chris
        gc.rectangle(0, 0, subject_width + g_width, headers_height)
3236 0:513646585e45 Chris
        gc.stroke('black')
3237 1115:433d4f72a19b Chris
        gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1)
3238 0:513646585e45 Chris
        # content
3239 441:cbce1fd3b1b7 Chris
        top = headers_height + 20
3240 119:8661b858af72 Chris
        gc.stroke('transparent')
3241 1115:433d4f72a19b Chris
        lines(:image => gc, :top => top, :zoom => zoom,
3242
              :subject_width => subject_width, :format => :image)
3243 0:513646585e45 Chris
        # today red line
3244
        if Date.today >= @date_from and Date.today <= date_to
3245
          gc.stroke('red')
3246 1115:433d4f72a19b Chris
          x = (Date.today - @date_from + 1) * zoom + subject_width
3247
          gc.line(x, headers_height, x, headers_height + g_height - 1)
3248 441:cbce1fd3b1b7 Chris
        end
3249 0:513646585e45 Chris
        gc.draw(imgl)
3250
        imgl.format = format
3251
        imgl.to_blob
3252
      end if Object.const_defined?(:Magick)
3253 22:40f7cfd4df19 chris
3254
      def to_pdf
3255 441:cbce1fd3b1b7 Chris
        pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
3256 22:40f7cfd4df19 chris
        pdf.SetTitle("#{l(:label_gantt)} #{project}")
3257 441:cbce1fd3b1b7 Chris
        pdf.alias_nb_pages
3258 22:40f7cfd4df19 chris
        pdf.footer_date = format_date(Date.today)
3259
        pdf.AddPage("L")
3260 1115:433d4f72a19b Chris
        pdf.SetFontStyle('B', 12)
3261 22:40f7cfd4df19 chris
        pdf.SetX(15)
3262 441:cbce1fd3b1b7 Chris
        pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
3263 22:40f7cfd4df19 chris
        pdf.Ln
3264 1115:433d4f72a19b Chris
        pdf.SetFontStyle('B', 9)
3265 22:40f7cfd4df19 chris
        subject_width = PDF::LeftPaneWidth
3266 441:cbce1fd3b1b7 Chris
        header_height = 5
3267
        headers_height = header_height
3268 22:40f7cfd4df19 chris
        show_weeks = false
3269
        show_days = false
3270
        if self.months < 7
3271
          show_weeks = true
3272 1115:433d4f72a19b Chris
          headers_height = 2 * header_height
3273 22:40f7cfd4df19 chris
          if self.months < 3
3274
            show_days = true
3275 1115:433d4f72a19b Chris
            headers_height = 3 * header_height
3276 22:40f7cfd4df19 chris
          end
3277
        end
3278
        g_width = PDF.right_pane_width
3279
        zoom = (g_width) / (self.date_to - self.date_from + 1)
3280
        g_height = 120
3281 441:cbce1fd3b1b7 Chris
        t_height = g_height + headers_height
3282 22:40f7cfd4df19 chris
        y_start = pdf.GetY
3283
        # Months headers
3284
        month_f = self.date_from
3285
        left = subject_width
3286 441:cbce1fd3b1b7 Chris
        height = header_height
3287
        self.months.times do
3288
          width = ((month_f >> 1) - month_f) * zoom
3289 22:40f7cfd4df19 chris
          pdf.SetY(y_start)
3290
          pdf.SetX(left)
3291 441:cbce1fd3b1b7 Chris
          pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
3292 22:40f7cfd4df19 chris
          left = left + width
3293
          month_f = month_f >> 1
3294 441:cbce1fd3b1b7 Chris
        end
3295 22:40f7cfd4df19 chris
        # Weeks headers
3296
        if show_weeks
3297
          left = subject_width
3298 441:cbce1fd3b1b7 Chris
          height = header_height
3299 22:40f7cfd4df19 chris
          if self.date_from.cwday == 1
3300
            # self.date_from is monday
3301
            week_f = self.date_from
3302
          else
3303
            # find next monday after self.date_from
3304
            week_f = self.date_from + (7 - self.date_from.cwday + 1)
3305
            width = (7 - self.date_from.cwday + 1) * zoom-1
3306 441:cbce1fd3b1b7 Chris
            pdf.SetY(y_start + header_height)
3307 22:40f7cfd4df19 chris
            pdf.SetX(left)
3308 441:cbce1fd3b1b7 Chris
            pdf.RDMCell(width + 1, height, "", "LTR")
3309 1115:433d4f72a19b Chris
            left = left + width + 1
3310 22:40f7cfd4df19 chris
          end
3311
          while week_f <= self.date_to
3312
            width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
3313 441:cbce1fd3b1b7 Chris
            pdf.SetY(y_start + header_height)
3314 22:40f7cfd4df19 chris
            pdf.SetX(left)
3315 441:cbce1fd3b1b7 Chris
            pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
3316 22:40f7cfd4df19 chris
            left = left + width
3317 1115:433d4f72a19b Chris
            week_f = week_f + 7
3318 22:40f7cfd4df19 chris
          end
3319
        end
3320
        # Days headers
3321
        if show_days
3322
          left = subject_width
3323 441:cbce1fd3b1b7 Chris
          height = header_height
3324 22:40f7cfd4df19 chris
          wday = self.date_from.cwday
3325 1115:433d4f72a19b Chris
          pdf.SetFontStyle('B', 7)
3326 441:cbce1fd3b1b7 Chris
          (self.date_to - self.date_from + 1).to_i.times do
3327 22:40f7cfd4df19 chris
            width = zoom
3328 441:cbce1fd3b1b7 Chris
            pdf.SetY(y_start + 2 * header_height)
3329 22:40f7cfd4df19 chris
            pdf.SetX(left)
3330 441:cbce1fd3b1b7 Chris
            pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
3331 22:40f7cfd4df19 chris
            left = left + width
3332
            wday = wday + 1
3333
            wday = 1 if wday > 7
3334
          end
3335
        end
3336
        pdf.SetY(y_start)
3337
        pdf.SetX(15)
3338 1115:433d4f72a19b Chris
        pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1)
3339 22:40f7cfd4df19 chris
        # Tasks
3340 441:cbce1fd3b1b7 Chris
        top = headers_height + y_start
3341 119:8661b858af72 Chris
        options = {
3342
          :top => top,
3343
          :zoom => zoom,
3344
          :subject_width => subject_width,
3345
          :g_width => g_width,
3346
          :indent => 0,
3347
          :indent_increment => 5,
3348
          :top_increment => 5,
3349
          :format => :pdf,
3350
          :pdf => pdf
3351
        }
3352
        render(options)
3353 22:40f7cfd4df19 chris
        pdf.Output
3354
      end
3355 441:cbce1fd3b1b7 Chris
3356 0:513646585e45 Chris
      private
3357 441:cbce1fd3b1b7 Chris
3358 119:8661b858af72 Chris
      def coordinates(start_date, end_date, progress, zoom=nil)
3359
        zoom ||= @zoom
3360
        coords = {}
3361
        if start_date && end_date && start_date < self.date_to && end_date > self.date_from
3362
          if start_date > self.date_from
3363
            coords[:start] = start_date - self.date_from
3364
            coords[:bar_start] = start_date - self.date_from
3365
          else
3366
            coords[:bar_start] = 0
3367
          end
3368
          if end_date < self.date_to
3369
            coords[:end] = end_date - self.date_from
3370
            coords[:bar_end] = end_date - self.date_from + 1
3371
          else
3372
            coords[:bar_end] = self.date_to - self.date_from + 1
3373
          end
3374
          if progress
3375 1464:261b3d9a4903 Chris
            progress_date = calc_progress_date(start_date, end_date, progress)
3376 119:8661b858af72 Chris
            if progress_date > self.date_from && progress_date > start_date
3377
              if progress_date < self.date_to
3378 441:cbce1fd3b1b7 Chris
                coords[:bar_progress_end] = progress_date - self.date_from
3379 119:8661b858af72 Chris
              else
3380
                coords[:bar_progress_end] = self.date_to - self.date_from + 1
3381
              end
3382
            end
3383
            if progress_date < Date.today
3384
              late_date = [Date.today, end_date].min
3385
              if late_date > self.date_from && late_date > start_date
3386
                if late_date < self.date_to
3387
                  coords[:bar_late_end] = late_date - self.date_from + 1
3388
                else
3389
                  coords[:bar_late_end] = self.date_to - self.date_from + 1
3390
                end
3391
              end
3392
            end
3393
          end
3394
        end
3395
        # Transforms dates into pixels witdh
3396
        coords.keys.each do |key|
3397
          coords[key] = (coords[key] * zoom).floor
3398
        end
3399
        coords
3400
      end
3401 22:40f7cfd4df19 chris
3402 1464:261b3d9a4903 Chris
      def calc_progress_date(start_date, end_date, progress)
3403
        start_date + (end_date - start_date + 1) * (progress / 100.0)
3404 119:8661b858af72 Chris
      end
3405 441:cbce1fd3b1b7 Chris
3406 1464:261b3d9a4903 Chris
      def self.sort_issues!(issues)
3407
        issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
3408
      end
3409
3410
      def self.sort_issue_logic(issue)
3411
        julian_date = Date.new()
3412
        ancesters_start_date = []
3413
        current_issue = issue
3414
        begin
3415
          ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
3416
          current_issue = current_issue.parent
3417
        end while (current_issue)
3418
        ancesters_start_date
3419
      end
3420
3421
      def self.sort_versions!(versions)
3422
        versions.sort!
3423 119:8661b858af72 Chris
      end
3424 441:cbce1fd3b1b7 Chris
3425 119:8661b858af72 Chris
      def current_limit
3426
        if @max_rows
3427
          @max_rows - @number_of_rows
3428
        else
3429
          nil
3430
        end
3431
      end
3432 441:cbce1fd3b1b7 Chris
3433 119:8661b858af72 Chris
      def abort?
3434
        if @max_rows && @number_of_rows >= @max_rows
3435
          @truncated = true
3436
        end
3437
      end
3438 441:cbce1fd3b1b7 Chris
3439 119:8661b858af72 Chris
      def pdf_new_page?(options)
3440
        if options[:top] > 180
3441
          options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
3442
          options[:pdf].AddPage("L")
3443
          options[:top] = 15
3444
          options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
3445
        end
3446
      end
3447 441:cbce1fd3b1b7 Chris
3448 119:8661b858af72 Chris
      def html_subject(params, subject, options={})
3449 245:051f544170fe Chris
        style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
3450
        style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
3451 1464:261b3d9a4903 Chris
        output = view.content_tag(:div, subject,
3452 1115:433d4f72a19b Chris
                                  :class => options[:css], :style => style,
3453 1464:261b3d9a4903 Chris
                                  :title => options[:title],
3454
                                  :id => options[:id])
3455 119:8661b858af72 Chris
        @subjects << output
3456
        output
3457
      end
3458 441:cbce1fd3b1b7 Chris
3459 119:8661b858af72 Chris
      def pdf_subject(params, subject, options={})
3460
        params[:pdf].SetY(params[:top])
3461
        params[:pdf].SetX(15)
3462
        char_limit = PDF::MaxCharactorsForSubject - params[:indent]
3463 1115:433d4f72a19b Chris
        params[:pdf].RDMCell(params[:subject_width] - 15, 5,
3464
                             (" " * params[:indent]) +
3465
                               subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
3466
                              "LR")
3467 119:8661b858af72 Chris
        params[:pdf].SetY(params[:top])
3468
        params[:pdf].SetX(params[:subject_width])
3469 441:cbce1fd3b1b7 Chris
        params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
3470 119:8661b858af72 Chris
      end
3471 441:cbce1fd3b1b7 Chris
3472 119:8661b858af72 Chris
      def image_subject(params, subject, options={})
3473
        params[:image].fill('black')
3474
        params[:image].stroke('transparent')
3475
        params[:image].stroke_width(1)
3476
        params[:image].text(params[:indent], params[:top] + 2, subject)
3477
      end
3478 441:cbce1fd3b1b7 Chris
3479 1464:261b3d9a4903 Chris
      def issue_relations(issue)
3480
        rels = {}
3481
        if relations[issue.id]
3482
          relations[issue.id].each do |relation|
3483
            (rels[relation.relation_type] ||= []) << relation.issue_to_id
3484
          end
3485
        end
3486
        rels
3487
      end
3488
3489 119:8661b858af72 Chris
      def html_task(params, coords, options={})
3490
        output = ''
3491
        # Renders the task bar, with progress and late
3492
        if coords[:bar_start] && coords[:bar_end]
3493 1115:433d4f72a19b Chris
          width = coords[:bar_end] - coords[:bar_start] - 2
3494
          style = ""
3495
          style << "top:#{params[:top]}px;"
3496
          style << "left:#{coords[:bar_start]}px;"
3497
          style << "width:#{width}px;"
3498 1464:261b3d9a4903 Chris
          html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
3499
          html_id = "task-todo-version-#{options[:version].id}" if options[:version]
3500
          content_opt = {:style => style,
3501
                         :class => "#{options[:css]} task_todo",
3502
                         :id => html_id}
3503
          if options[:issue]
3504
            rels = issue_relations(options[:issue])
3505
            if rels.present?
3506
              content_opt[:data] = {"rels" => rels.to_json}
3507
            end
3508
          end
3509
          output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
3510 119:8661b858af72 Chris
          if coords[:bar_late_end]
3511 1115:433d4f72a19b Chris
            width = coords[:bar_late_end] - coords[:bar_start] - 2
3512
            style = ""
3513
            style << "top:#{params[:top]}px;"
3514
            style << "left:#{coords[:bar_start]}px;"
3515
            style << "width:#{width}px;"
3516
            output << view.content_tag(:div, '&nbsp;'.html_safe,
3517
                                       :style => style,
3518
                                       :class => "#{options[:css]} task_late")
3519 0:513646585e45 Chris
          end
3520 119:8661b858af72 Chris
          if coords[:bar_progress_end]
3521 1115:433d4f72a19b Chris
            width = coords[:bar_progress_end] - coords[:bar_start] - 2
3522
            style = ""
3523
            style << "top:#{params[:top]}px;"
3524
            style << "left:#{coords[:bar_start]}px;"
3525
            style << "width:#{width}px;"
3526 1464:261b3d9a4903 Chris
            html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
3527
            html_id = "task-done-version-#{options[:version].id}" if options[:version]
3528 1115:433d4f72a19b Chris
            output << view.content_tag(:div, '&nbsp;'.html_safe,
3529
                                       :style => style,
3530 1464:261b3d9a4903 Chris
                                       :class => "#{options[:css]} task_done",
3531
                                       :id => html_id)
3532 119:8661b858af72 Chris
          end
3533
        end
3534
        # Renders the markers
3535
        if options[:markers]
3536
          if coords[:start]
3537 1115:433d4f72a19b Chris
            style = ""
3538
            style << "top:#{params[:top]}px;"
3539
            style << "left:#{coords[:start]}px;"
3540
            style << "width:15px;"
3541
            output << view.content_tag(:div, '&nbsp;'.html_safe,
3542
                                       :style => style,
3543
                                       :class => "#{options[:css]} marker starting")
3544 119:8661b858af72 Chris
          end
3545
          if coords[:end]
3546 1115:433d4f72a19b Chris
            style = ""
3547
            style << "top:#{params[:top]}px;"
3548
            style << "left:#{coords[:end] + params[:zoom]}px;"
3549
            style << "width:15px;"
3550
            output << view.content_tag(:div, '&nbsp;'.html_safe,
3551
                                       :style => style,
3552
                                       :class => "#{options[:css]} marker ending")
3553 119:8661b858af72 Chris
          end
3554
        end
3555
        # Renders the label on the right
3556
        if options[:label]
3557 1115:433d4f72a19b Chris
          style = ""
3558
          style << "top:#{params[:top]}px;"
3559
          style << "left:#{(coords[:bar_end] || 0) + 8}px;"
3560
          style << "width:15px;"
3561
          output << view.content_tag(:div, options[:label],
3562
                                     :style => style,
3563
                                     :class => "#{options[:css]} label")
3564 119:8661b858af72 Chris
        end
3565
        # Renders the tooltip
3566
        if options[:issue] && coords[:bar_start] && coords[:bar_end]
3567 1115:433d4f72a19b Chris
          s = view.content_tag(:span,
3568
                               view.render_issue_tooltip(options[:issue]).html_safe,
3569
                               :class => "tip")
3570
          style = ""
3571
          style << "position: absolute;"
3572
          style << "top:#{params[:top]}px;"
3573
          style << "left:#{coords[:bar_start]}px;"
3574
          style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
3575
          style << "height:12px;"
3576
          output << view.content_tag(:div, s.html_safe,
3577
                                     :style => style,
3578
                                     :class => "tooltip")
3579 119:8661b858af72 Chris
        end
3580
        @lines << output
3581
        output
3582
      end
3583 441:cbce1fd3b1b7 Chris
3584 119:8661b858af72 Chris
      def pdf_task(params, coords, options={})
3585
        height = options[:height] || 2
3586
        # Renders the task bar, with progress and late
3587
        if coords[:bar_start] && coords[:bar_end]
3588 1115:433d4f72a19b Chris
          params[:pdf].SetY(params[:top] + 1.5)
3589 119:8661b858af72 Chris
          params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
3590 1115:433d4f72a19b Chris
          params[:pdf].SetFillColor(200, 200, 200)
3591 441:cbce1fd3b1b7 Chris
          params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
3592 119:8661b858af72 Chris
          if coords[:bar_late_end]
3593 1115:433d4f72a19b Chris
            params[:pdf].SetY(params[:top] + 1.5)
3594 119:8661b858af72 Chris
            params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
3595 1115:433d4f72a19b Chris
            params[:pdf].SetFillColor(255, 100, 100)
3596 441:cbce1fd3b1b7 Chris
            params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
3597 119:8661b858af72 Chris
          end
3598
          if coords[:bar_progress_end]
3599 1115:433d4f72a19b Chris
            params[:pdf].SetY(params[:top] + 1.5)
3600 119:8661b858af72 Chris
            params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
3601 1115:433d4f72a19b Chris
            params[:pdf].SetFillColor(90, 200, 90)
3602 441:cbce1fd3b1b7 Chris
            params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
3603 119:8661b858af72 Chris
          end
3604
        end
3605
        # Renders the markers
3606
        if options[:markers]
3607
          if coords[:start]
3608
            params[:pdf].SetY(params[:top] + 1)
3609
            params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
3610 1115:433d4f72a19b Chris
            params[:pdf].SetFillColor(50, 50, 200)
3611 441:cbce1fd3b1b7 Chris
            params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
3612 119:8661b858af72 Chris
          end
3613
          if coords[:end]
3614
            params[:pdf].SetY(params[:top] + 1)
3615
            params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
3616 1115:433d4f72a19b Chris
            params[:pdf].SetFillColor(50, 50, 200)
3617 441:cbce1fd3b1b7 Chris
            params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
3618 119:8661b858af72 Chris
          end
3619
        end
3620
        # Renders the label on the right
3621
        if options[:label]
3622
          params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
3623 441:cbce1fd3b1b7 Chris
          params[:pdf].RDMCell(30, 2, options[:label])
3624 0:513646585e45 Chris
        end
3625
      end
3626 22:40f7cfd4df19 chris
3627 119:8661b858af72 Chris
      def image_task(params, coords, options={})
3628
        height = options[:height] || 6
3629
        # Renders the task bar, with progress and late
3630
        if coords[:bar_start] && coords[:bar_end]
3631
          params[:image].fill('#aaa')
3632 1115:433d4f72a19b Chris
          params[:image].rectangle(params[:subject_width] + coords[:bar_start],
3633
                                   params[:top],
3634
                                   params[:subject_width] + coords[:bar_end],
3635
                                   params[:top] - height)
3636 119:8661b858af72 Chris
          if coords[:bar_late_end]
3637
            params[:image].fill('#f66')
3638 1115:433d4f72a19b Chris
            params[:image].rectangle(params[:subject_width] + coords[:bar_start],
3639
                                     params[:top],
3640
                                     params[:subject_width] + coords[:bar_late_end],
3641
                                     params[:top] - height)
3642 119:8661b858af72 Chris
          end
3643
          if coords[:bar_progress_end]
3644
            params[:image].fill('#00c600')
3645 1115:433d4f72a19b Chris
            params[:image].rectangle(params[:subject_width] + coords[:bar_start],
3646
                                     params[:top],
3647
                                     params[:subject_width] + coords[:bar_progress_end],
3648
                                     params[:top] - height)
3649 119:8661b858af72 Chris
          end
3650
        end
3651
        # Renders the markers
3652
        if options[:markers]
3653
          if coords[:start]
3654
            x = params[:subject_width] + coords[:start]
3655
            y = params[:top] - height / 2
3656
            params[:image].fill('blue')
3657 1115:433d4f72a19b Chris
            params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
3658 119:8661b858af72 Chris
          end
3659
          if coords[:end]
3660
            x = params[:subject_width] + coords[:end] + params[:zoom]
3661
            y = params[:top] - height / 2
3662
            params[:image].fill('blue')
3663 1115:433d4f72a19b Chris
            params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
3664 119:8661b858af72 Chris
          end
3665
        end
3666
        # Renders the label on the right
3667
        if options[:label]
3668
          params[:image].fill('black')
3669 1115:433d4f72a19b Chris
          params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,
3670
                              params[:top] + 1,
3671
                              options[:label])
3672 119:8661b858af72 Chris
        end
3673
      end
3674 0:513646585e45 Chris
    end
3675
  end
3676
end
3677 1115:433d4f72a19b Chris
# Redmine - project management software
3678 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
3679 1115:433d4f72a19b Chris
#
3680
# This program is free software; you can redistribute it and/or
3681
# modify it under the terms of the GNU General Public License
3682
# as published by the Free Software Foundation; either version 2
3683
# of the License, or (at your option) any later version.
3684
#
3685
# This program is distributed in the hope that it will be useful,
3686
# but WITHOUT ANY WARRANTY; without even the implied warranty of
3687
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
3688
# GNU General Public License for more details.
3689
#
3690
# You should have received a copy of the GNU General Public License
3691
# along with this program; if not, write to the Free Software
3692
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
3693
3694
module Redmine
3695
  module Helpers
3696
    class TimeReport
3697 1464:261b3d9a4903 Chris
      attr_reader :criteria, :columns, :hours, :total_hours, :periods
3698 1115:433d4f72a19b Chris
3699 1464:261b3d9a4903 Chris
      def initialize(project, issue, criteria, columns, time_entry_scope)
3700 1115:433d4f72a19b Chris
        @project = project
3701
        @issue = issue
3702
3703
        @criteria = criteria || []
3704
        @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
3705
        @criteria.uniq!
3706
        @criteria = @criteria[0,3]
3707
3708
        @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
3709 1464:261b3d9a4903 Chris
        @scope = time_entry_scope
3710 1115:433d4f72a19b Chris
3711
        run
3712
      end
3713
3714
      def available_criteria
3715
        @available_criteria || load_available_criteria
3716
      end
3717
3718
      private
3719
3720
      def run
3721
        unless @criteria.empty?
3722
          time_columns = %w(tyear tmonth tweek spent_on)
3723
          @hours = []
3724 1517:dffacf8a6908 Chris
          @scope.includes(:issue, :activity).
3725
              group(@criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).
3726
              joins(@criteria.collect{|criteria| @available_criteria[criteria][:joins]}.compact).
3727
              sum(:hours).each do |hash, hours|
3728 1115:433d4f72a19b Chris
            h = {'hours' => hours}
3729
            (@criteria + time_columns).each_with_index do |name, i|
3730
              h[name] = hash[i]
3731
            end
3732
            @hours << h
3733
          end
3734
3735
          @hours.each do |row|
3736
            case @columns
3737
            when 'year'
3738
              row['year'] = row['tyear']
3739
            when 'month'
3740
              row['month'] = "#{row['tyear']}-#{row['tmonth']}"
3741
            when 'week'
3742 1464:261b3d9a4903 Chris
              row['week'] = "#{row['spent_on'].cwyear}-#{row['tweek']}"
3743 1115:433d4f72a19b Chris
            when 'day'
3744
              row['day'] = "#{row['spent_on']}"
3745
            end
3746
          end
3747
3748 1464:261b3d9a4903 Chris
          min = @hours.collect {|row| row['spent_on']}.min
3749
          @from = min ? min.to_date : Date.today
3750 1115:433d4f72a19b Chris
3751 1464:261b3d9a4903 Chris
          max = @hours.collect {|row| row['spent_on']}.max
3752
          @to = max ? max.to_date : Date.today
3753 1115:433d4f72a19b Chris
3754
          @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
3755
3756
          @periods = []
3757
          # Date#at_beginning_of_ not supported in Rails 1.2.x
3758
          date_from = @from.to_time
3759
          # 100 columns max
3760
          while date_from <= @to.to_time && @periods.length < 100
3761
            case @columns
3762
            when 'year'
3763
              @periods << "#{date_from.year}"
3764
              date_from = (date_from + 1.year).at_beginning_of_year
3765
            when 'month'
3766
              @periods << "#{date_from.year}-#{date_from.month}"
3767
              date_from = (date_from + 1.month).at_beginning_of_month
3768
            when 'week'
3769 1464:261b3d9a4903 Chris
              @periods << "#{date_from.to_date.cwyear}-#{date_from.to_date.cweek}"
3770 1115:433d4f72a19b Chris
              date_from = (date_from + 7.day).at_beginning_of_week
3771
            when 'day'
3772
              @periods << "#{date_from.to_date}"
3773
              date_from = date_from + 1.day
3774
            end
3775
          end
3776
        end
3777
      end
3778
3779
      def load_available_criteria
3780
        @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
3781
                                              :klass => Project,
3782
                                              :label => :label_project},
3783
                                 'status' => {:sql => "#{Issue.table_name}.status_id",
3784
                                              :klass => IssueStatus,
3785
                                              :label => :field_status},
3786
                                 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
3787
                                              :klass => Version,
3788
                                              :label => :label_version},
3789
                                 'category' => {:sql => "#{Issue.table_name}.category_id",
3790
                                                :klass => IssueCategory,
3791
                                                :label => :field_category},
3792 1464:261b3d9a4903 Chris
                                 'user' => {:sql => "#{TimeEntry.table_name}.user_id",
3793 1115:433d4f72a19b Chris
                                             :klass => User,
3794 1464:261b3d9a4903 Chris
                                             :label => :label_user},
3795 1115:433d4f72a19b Chris
                                 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
3796
                                              :klass => Tracker,
3797
                                              :label => :label_tracker},
3798
                                 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
3799
                                               :klass => TimeEntryActivity,
3800
                                               :label => :label_activity},
3801
                                 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
3802
                                             :klass => Issue,
3803
                                             :label => :label_issue}
3804
                               }
3805
3806 1464:261b3d9a4903 Chris
        # Add time entry custom fields
3807
        custom_fields = TimeEntryCustomField.all
3808
        # Add project custom fields
3809
        custom_fields += ProjectCustomField.all
3810
        # Add issue custom fields
3811
        custom_fields += (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
3812
        # Add time entry activity custom fields
3813
        custom_fields += TimeEntryActivityCustomField.all
3814
3815 1115:433d4f72a19b Chris
        # Add list and boolean custom fields as available criteria
3816 1517:dffacf8a6908 Chris
        custom_fields.select {|cf| %w(list bool).include?(cf.field_format) && !cf.multiple?}.each do |cf|
3817
          @available_criteria["cf_#{cf.id}"] = {:sql => cf.group_statement,
3818 1464:261b3d9a4903 Chris
                                                 :joins => cf.join_for_order_statement,
3819 1115:433d4f72a19b Chris
                                                 :format => cf.field_format,
3820 1517:dffacf8a6908 Chris
                                                 :custom_field => cf,
3821 1115:433d4f72a19b Chris
                                                 :label => cf.name}
3822
        end
3823
3824
        @available_criteria
3825
      end
3826
    end
3827
  end
3828
end
3829 0:513646585e45 Chris
# Redmine - project management software
3830 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
3831 0:513646585e45 Chris
#
3832
# This program is free software; you can redistribute it and/or
3833
# modify it under the terms of the GNU General Public License
3834
# as published by the Free Software Foundation; either version 2
3835
# of the License, or (at your option) any later version.
3836 909:cbb26bc654de Chris
#
3837 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
3838
# but WITHOUT ANY WARRANTY; without even the implied warranty of
3839
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
3840
# GNU General Public License for more details.
3841 909:cbb26bc654de Chris
#
3842 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
3843
# along with this program; if not, write to the Free Software
3844
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
3845
3846
module Redmine
3847
  module Hook
3848
    @@listener_classes = []
3849
    @@listeners = nil
3850
    @@hook_listeners = {}
3851 909:cbb26bc654de Chris
3852 0:513646585e45 Chris
    class << self
3853
      # Adds a listener class.
3854
      # Automatically called when a class inherits from Redmine::Hook::Listener.
3855
      def add_listener(klass)
3856
        raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
3857
        @@listener_classes << klass
3858
        clear_listeners_instances
3859
      end
3860 909:cbb26bc654de Chris
3861 0:513646585e45 Chris
      # Returns all the listerners instances.
3862
      def listeners
3863
        @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
3864
      end
3865 909:cbb26bc654de Chris
3866 0:513646585e45 Chris
      # Returns the listeners instances for the given hook.
3867
      def hook_listeners(hook)
3868
        @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
3869
      end
3870 909:cbb26bc654de Chris
3871 0:513646585e45 Chris
      # Clears all the listeners.
3872
      def clear_listeners
3873
        @@listener_classes = []
3874
        clear_listeners_instances
3875
      end
3876 909:cbb26bc654de Chris
3877 0:513646585e45 Chris
      # Clears all the listeners instances.
3878
      def clear_listeners_instances
3879
        @@listeners = nil
3880
        @@hook_listeners = {}
3881
      end
3882 909:cbb26bc654de Chris
3883 0:513646585e45 Chris
      # Calls a hook.
3884
      # Returns the listeners response.
3885
      def call_hook(hook, context={})
3886 37:94944d00e43c chris
        [].tap do |response|
3887 0:513646585e45 Chris
          hls = hook_listeners(hook)
3888
          if hls.any?
3889
            hls.each {|listener| response << listener.send(hook, context)}
3890
          end
3891
        end
3892
      end
3893
    end
3894
3895
    # Base class for hook listeners.
3896
    class Listener
3897
      include Singleton
3898
      include Redmine::I18n
3899
3900
      # Registers the listener
3901
      def self.inherited(child)
3902
        Redmine::Hook.add_listener(child)
3903
        super
3904
      end
3905
3906
    end
3907
3908
    # Listener class used for views hooks.
3909
    # Listeners that inherit this class will include various helpers by default.
3910
    class ViewListener < Listener
3911
      include ERB::Util
3912
      include ActionView::Helpers::TagHelper
3913
      include ActionView::Helpers::FormHelper
3914
      include ActionView::Helpers::FormTagHelper
3915
      include ActionView::Helpers::FormOptionsHelper
3916
      include ActionView::Helpers::JavaScriptHelper
3917
      include ActionView::Helpers::NumberHelper
3918
      include ActionView::Helpers::UrlHelper
3919
      include ActionView::Helpers::AssetTagHelper
3920
      include ActionView::Helpers::TextHelper
3921 1115:433d4f72a19b Chris
      include Rails.application.routes.url_helpers
3922 0:513646585e45 Chris
      include ApplicationHelper
3923
3924
      # Default to creating links using only the path.  Subclasses can
3925
      # change this default as needed
3926
      def self.default_url_options
3927
        {:only_path => true }
3928
      end
3929 909:cbb26bc654de Chris
3930 0:513646585e45 Chris
      # Helper method to directly render a partial using the context:
3931 909:cbb26bc654de Chris
      #
3932 0:513646585e45 Chris
      #   class MyHook < Redmine::Hook::ViewListener
3933 909:cbb26bc654de Chris
      #     render_on :view_issues_show_details_bottom, :partial => "show_more_data"
3934 0:513646585e45 Chris
      #   end
3935
      #
3936
      def self.render_on(hook, options={})
3937
        define_method hook do |context|
3938 1115:433d4f72a19b Chris
          if context[:hook_caller].respond_to?(:render)
3939
            context[:hook_caller].send(:render, {:locals => context}.merge(options))
3940
          elsif context[:controller].is_a?(ActionController::Base)
3941
            context[:controller].send(:render_to_string, {:locals => context}.merge(options))
3942
          else
3943
            raise "Cannot render #{self.name} hook from #{context[:hook_caller].class.name}"
3944
          end
3945 0:513646585e45 Chris
        end
3946
      end
3947 1115:433d4f72a19b Chris
3948
      def controller
3949
        nil
3950
      end
3951
3952
      def config
3953
        ActionController::Base.config
3954
      end
3955 0:513646585e45 Chris
    end
3956
3957 909:cbb26bc654de Chris
    # Helper module included in ApplicationHelper and ActionController so that
3958 0:513646585e45 Chris
    # hooks can be called in views like this:
3959 909:cbb26bc654de Chris
    #
3960 0:513646585e45 Chris
    #   <%= call_hook(:some_hook) %>
3961 909:cbb26bc654de Chris
    #   <%= call_hook(:another_hook, :foo => 'bar') %>
3962
    #
3963 0:513646585e45 Chris
    # Or in controllers like:
3964
    #   call_hook(:some_hook)
3965 909:cbb26bc654de Chris
    #   call_hook(:another_hook, :foo => 'bar')
3966
    #
3967
    # Hooks added to views will be concatenated into a string. Hooks added to
3968 0:513646585e45 Chris
    # controllers will return an array of results.
3969
    #
3970
    # Several objects are automatically added to the call context:
3971 909:cbb26bc654de Chris
    #
3972 0:513646585e45 Chris
    # * project => current project
3973
    # * request => Request instance
3974
    # * controller => current Controller instance
3975 1115:433d4f72a19b Chris
    # * hook_caller => object that called the hook
3976 909:cbb26bc654de Chris
    #
3977 0:513646585e45 Chris
    module Helper
3978
      def call_hook(hook, context={})
3979
        if is_a?(ActionController::Base)
3980 1115:433d4f72a19b Chris
          default_context = {:controller => self, :project => @project, :request => request, :hook_caller => self}
3981 0:513646585e45 Chris
          Redmine::Hook.call_hook(hook, default_context.merge(context))
3982
        else
3983 1115:433d4f72a19b Chris
          default_context = { :project => @project, :hook_caller => self }
3984
          default_context[:controller] = controller if respond_to?(:controller)
3985
          default_context[:request] = request if respond_to?(:request)
3986
          Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
3987 909:cbb26bc654de Chris
        end
3988 0:513646585e45 Chris
      end
3989
    end
3990
  end
3991
end
3992
3993
ApplicationHelper.send(:include, Redmine::Hook::Helper)
3994
ActionController::Base.send(:include, Redmine::Hook::Helper)
3995 1115:433d4f72a19b Chris
# Redmine - project management software
3996 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
3997 1115:433d4f72a19b Chris
#
3998
# This program is free software; you can redistribute it and/or
3999
# modify it under the terms of the GNU General Public License
4000
# as published by the Free Software Foundation; either version 2
4001
# of the License, or (at your option) any later version.
4002
#
4003
# This program is distributed in the hope that it will be useful,
4004
# but WITHOUT ANY WARRANTY; without even the implied warranty of
4005
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4006
# GNU General Public License for more details.
4007
#
4008
# You should have received a copy of the GNU General Public License
4009
# along with this program; if not, write to the Free Software
4010
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
4011
4012 0:513646585e45 Chris
module Redmine
4013
  module I18n
4014
    def self.included(base)
4015
      base.extend Redmine::I18n
4016
    end
4017 909:cbb26bc654de Chris
4018 0:513646585e45 Chris
    def l(*args)
4019
      case args.size
4020
      when 1
4021
        ::I18n.t(*args)
4022
      when 2
4023
        if args.last.is_a?(Hash)
4024
          ::I18n.t(*args)
4025
        elsif args.last.is_a?(String)
4026
          ::I18n.t(args.first, :value => args.last)
4027
        else
4028
          ::I18n.t(args.first, :count => args.last)
4029
        end
4030
      else
4031
        raise "Translation string with multiple values: #{args.first}"
4032
      end
4033
    end
4034
4035
    def l_or_humanize(s, options={})
4036
      k = "#{options[:prefix]}#{s}".to_sym
4037
      ::I18n.t(k, :default => s.to_s.humanize)
4038
    end
4039 909:cbb26bc654de Chris
4040 0:513646585e45 Chris
    def l_hours(hours)
4041
      hours = hours.to_f
4042
      l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f))
4043
    end
4044 909:cbb26bc654de Chris
4045 0:513646585e45 Chris
    def ll(lang, str, value=nil)
4046
      ::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" })
4047
    end
4048
4049
    def format_date(date)
4050
      return nil unless date
4051 1115:433d4f72a19b Chris
      options = {}
4052
      options[:format] = Setting.date_format unless Setting.date_format.blank?
4053
      options[:locale] = User.current.language unless User.current.language.blank?
4054
      ::I18n.l(date.to_date, options)
4055 0:513646585e45 Chris
    end
4056 909:cbb26bc654de Chris
4057 0:513646585e45 Chris
    def format_time(time, include_date = true)
4058
      return nil unless time
4059 1115:433d4f72a19b Chris
      options = {}
4060
      options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format)
4061
      options[:locale] = User.current.language unless User.current.language.blank?
4062 0:513646585e45 Chris
      time = time.to_time if time.is_a?(String)
4063
      zone = User.current.time_zone
4064
      local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
4065 1115:433d4f72a19b Chris
      (include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options)
4066 0:513646585e45 Chris
    end
4067
4068
    def day_name(day)
4069
      ::I18n.t('date.day_names')[day % 7]
4070
    end
4071 909:cbb26bc654de Chris
4072 1115:433d4f72a19b Chris
    def day_letter(day)
4073
      ::I18n.t('date.abbr_day_names')[day % 7].first
4074
    end
4075
4076 0:513646585e45 Chris
    def month_name(month)
4077
      ::I18n.t('date.month_names')[month]
4078
    end
4079 909:cbb26bc654de Chris
4080 0:513646585e45 Chris
    def valid_languages
4081 1115:433d4f72a19b Chris
      ::I18n.available_locales
4082
    end
4083
4084
    # Returns an array of languages names and code sorted by names, example:
4085
    # [["Deutsch", "de"], ["English", "en"] ...]
4086
    #
4087
    # The result is cached to prevent from loading all translations files.
4088
    def languages_options
4089
      ActionController::Base.cache_store.fetch "i18n/languages_options" do
4090
        valid_languages.map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.sort {|x,y| x.first <=> y.first }
4091
      end
4092 0:513646585e45 Chris
    end
4093 909:cbb26bc654de Chris
4094 0:513646585e45 Chris
    def find_language(lang)
4095
      @@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k }
4096
      @@languages_lookup[lang.to_s.downcase]
4097
    end
4098 909:cbb26bc654de Chris
4099 0:513646585e45 Chris
    def set_language_if_valid(lang)
4100
      if l = find_language(lang)
4101
        ::I18n.locale = l
4102
      end
4103
    end
4104 909:cbb26bc654de Chris
4105 0:513646585e45 Chris
    def current_language
4106
      ::I18n.locale
4107
    end
4108 1115:433d4f72a19b Chris
4109
    # Custom backend based on I18n::Backend::Simple with the following changes:
4110
    # * lazy loading of translation files
4111
    # * available_locales are determined by looking at translation file names
4112
    class Backend
4113
      (class << self; self; end).class_eval { public :include }
4114
4115
      module Implementation
4116
        include ::I18n::Backend::Base
4117
4118
        # Stores translations for the given locale in memory.
4119
        # This uses a deep merge for the translations hash, so existing
4120
        # translations will be overwritten by new ones only at the deepest
4121
        # level of the hash.
4122
        def store_translations(locale, data, options = {})
4123
          locale = locale.to_sym
4124
          translations[locale] ||= {}
4125
          data = data.deep_symbolize_keys
4126
          translations[locale].deep_merge!(data)
4127
        end
4128
4129
        # Get available locales from the translations filenames
4130
        def available_locales
4131
          @available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym)
4132
        end
4133
4134
        # Clean up translations
4135
        def reload!
4136
          @translations = nil
4137
          @available_locales = nil
4138
          super
4139
        end
4140
4141
        protected
4142
4143
        def init_translations(locale)
4144
          locale = locale.to_s
4145
          paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
4146
          load_translations(paths)
4147
          translations[locale] ||= {}
4148
        end
4149
4150
        def translations
4151
          @translations ||= {}
4152
        end
4153
4154
        # Looks up a translation from the translations hash. Returns nil if
4155
        # eiher key is nil, or locale, scope or key do not exist as a key in the
4156
        # nested translations hash. Splits keys or scopes containing dots
4157
        # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
4158
        # <tt>%w(currency format)</tt>.
4159
        def lookup(locale, key, scope = [], options = {})
4160
          init_translations(locale) unless translations.key?(locale)
4161
          keys = ::I18n.normalize_keys(locale, key, scope, options[:separator])
4162
4163
          keys.inject(translations) do |result, _key|
4164
            _key = _key.to_sym
4165
            return nil unless result.is_a?(Hash) && result.has_key?(_key)
4166
            result = result[_key]
4167
            result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
4168
            result
4169
          end
4170
        end
4171
      end
4172
4173
      include Implementation
4174
      # Adds fallback to default locale for untranslated strings
4175
      include ::I18n::Backend::Fallbacks
4176
    end
4177 0:513646585e45 Chris
  end
4178
end
4179
# Redmine - project management software
4180 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
4181 0:513646585e45 Chris
#
4182
# This program is free software; you can redistribute it and/or
4183
# modify it under the terms of the GNU General Public License
4184
# as published by the Free Software Foundation; either version 2
4185
# of the License, or (at your option) any later version.
4186 909:cbb26bc654de Chris
#
4187 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
4188
# but WITHOUT ANY WARRANTY; without even the implied warranty of
4189
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4190
# GNU General Public License for more details.
4191 909:cbb26bc654de Chris
#
4192 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
4193
# along with this program; if not, write to the Free Software
4194
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
4195
4196
require 'net/imap'
4197
4198
module Redmine
4199
  module IMAP
4200
    class << self
4201
      def check(imap_options={}, options={})
4202
        host = imap_options[:host] || '127.0.0.1'
4203
        port = imap_options[:port] || '143'
4204
        ssl = !imap_options[:ssl].nil?
4205
        folder = imap_options[:folder] || 'INBOX'
4206 909:cbb26bc654de Chris
4207
        imap = Net::IMAP.new(host, port, ssl)
4208 0:513646585e45 Chris
        imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil?
4209
        imap.select(folder)
4210 1464:261b3d9a4903 Chris
        imap.uid_search(['NOT', 'SEEN']).each do |uid|
4211
          msg = imap.uid_fetch(uid,'RFC822')[0].attr['RFC822']
4212
          logger.debug "Receiving message #{uid}" if logger && logger.debug?
4213 0:513646585e45 Chris
          if MailHandler.receive(msg, options)
4214 1464:261b3d9a4903 Chris
            logger.debug "Message #{uid} successfully received" if logger && logger.debug?
4215 0:513646585e45 Chris
            if imap_options[:move_on_success]
4216 1464:261b3d9a4903 Chris
              imap.uid_copy(uid, imap_options[:move_on_success])
4217 0:513646585e45 Chris
            end
4218 1464:261b3d9a4903 Chris
            imap.uid_store(uid, "+FLAGS", [:Seen, :Deleted])
4219 0:513646585e45 Chris
          else
4220 1464:261b3d9a4903 Chris
            logger.debug "Message #{uid} can not be processed" if logger && logger.debug?
4221
            imap.uid_store(uid, "+FLAGS", [:Seen])
4222 0:513646585e45 Chris
            if imap_options[:move_on_failure]
4223 1464:261b3d9a4903 Chris
              imap.uid_copy(uid, imap_options[:move_on_failure])
4224
              imap.uid_store(uid, "+FLAGS", [:Deleted])
4225 0:513646585e45 Chris
            end
4226
          end
4227
        end
4228
        imap.expunge
4229 1464:261b3d9a4903 Chris
        imap.logout
4230
        imap.disconnect
4231 0:513646585e45 Chris
      end
4232 909:cbb26bc654de Chris
4233 0:513646585e45 Chris
      private
4234 909:cbb26bc654de Chris
4235 0:513646585e45 Chris
      def logger
4236 1115:433d4f72a19b Chris
        ::Rails.logger
4237 0:513646585e45 Chris
      end
4238
    end
4239
  end
4240
end
4241
module Redmine
4242
  module Info
4243
    class << self
4244
      def app_name; 'Redmine' end
4245
      def url; 'http://www.redmine.org/' end
4246 33:9f4ebcdd78a6 Chris
      def help_url; '/projects/soundsoftware-site/wiki/Help' end
4247 0:513646585e45 Chris
      def versioned_name; "#{app_name} #{Redmine::VERSION}" end
4248
4249 1115:433d4f72a19b Chris
      def environment
4250
        s = "Environment:\n"
4251
        s << [
4252
          ["Redmine version", Redmine::VERSION],
4253 1464:261b3d9a4903 Chris
          ["Ruby version", "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"],
4254 1115:433d4f72a19b Chris
          ["Rails version", Rails::VERSION::STRING],
4255
          ["Environment", Rails.env],
4256
          ["Database adapter", ActiveRecord::Base.connection.adapter_name]
4257 1464:261b3d9a4903 Chris
        ].map {|info| "  %-30s %s" % info}.join("\n") + "\n"
4258 1115:433d4f72a19b Chris
4259 1464:261b3d9a4903 Chris
        s << "SCM:\n"
4260
        Redmine::Scm::Base.all.each do |scm|
4261
          scm_class = "Repository::#{scm}".constantize
4262
          if scm_class.scm_available
4263
            s << "  %-30s %s\n" % [scm, scm_class.scm_version_string]
4264
          end
4265
        end
4266
4267
        s << "Redmine plugins:\n"
4268 1115:433d4f72a19b Chris
        plugins = Redmine::Plugin.all
4269
        if plugins.any?
4270 1464:261b3d9a4903 Chris
          s << plugins.map {|plugin| "  %-30s %s" % [plugin.id.to_s, plugin.version.to_s]}.join("\n")
4271 1115:433d4f72a19b Chris
        else
4272
          s << "  no plugin installed"
4273
        end
4274 0:513646585e45 Chris
      end
4275
    end
4276
  end
4277
end
4278 909:cbb26bc654de Chris
# Redmine - project management software
4279 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
4280 0:513646585e45 Chris
#
4281
# This program is free software; you can redistribute it and/or
4282
# modify it under the terms of the GNU General Public License
4283
# as published by the Free Software Foundation; either version 2
4284
# of the License, or (at your option) any later version.
4285 909:cbb26bc654de Chris
#
4286 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
4287
# but WITHOUT ANY WARRANTY; without even the implied warranty of
4288
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4289
# GNU General Public License for more details.
4290 909:cbb26bc654de Chris
#
4291 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
4292
# along with this program; if not, write to the Free Software
4293
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
4294
4295
module Redmine
4296
  module MenuManager
4297
    class MenuError < StandardError #:nodoc:
4298
    end
4299 909:cbb26bc654de Chris
4300 0:513646585e45 Chris
    module MenuController
4301
      def self.included(base)
4302
        base.extend(ClassMethods)
4303
      end
4304
4305
      module ClassMethods
4306
        @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
4307
        mattr_accessor :menu_items
4308 909:cbb26bc654de Chris
4309 0:513646585e45 Chris
        # Set the menu item name for a controller or specific actions
4310
        # Examples:
4311
        #   * menu_item :tickets # => sets the menu name to :tickets for the whole controller
4312
        #   * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
4313
        #   * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
4314 909:cbb26bc654de Chris
        #
4315 0:513646585e45 Chris
        # The default menu item name for a controller is controller_name by default
4316
        # Eg. the default menu item name for ProjectsController is :projects
4317
        def menu_item(id, options = {})
4318
          if actions = options[:only]
4319
            actions = [] << actions unless actions.is_a?(Array)
4320
            actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
4321
          else
4322
            menu_items[controller_name.to_sym][:default] = id
4323
          end
4324
        end
4325
      end
4326 909:cbb26bc654de Chris
4327 0:513646585e45 Chris
      def menu_items
4328
        self.class.menu_items
4329
      end
4330 909:cbb26bc654de Chris
4331 0:513646585e45 Chris
      # Returns the menu item name according to the current action
4332
      def current_menu_item
4333
        @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
4334
                                 menu_items[controller_name.to_sym][:default]
4335
      end
4336 909:cbb26bc654de Chris
4337 0:513646585e45 Chris
      # Redirects user to the menu item of the given project
4338
      # Returns false if user is not authorized
4339
      def redirect_to_project_menu_item(project, name)
4340
        item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
4341
        if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
4342
          redirect_to({item.param => project}.merge(item.url))
4343
          return true
4344
        end
4345
        false
4346
      end
4347
    end
4348 909:cbb26bc654de Chris
4349 0:513646585e45 Chris
    module MenuHelper
4350
      # Returns the current menu item name
4351
      def current_menu_item
4352 909:cbb26bc654de Chris
        controller.current_menu_item
4353 0:513646585e45 Chris
      end
4354 909:cbb26bc654de Chris
4355 0:513646585e45 Chris
      # Renders the application main menu
4356
      def render_main_menu(project)
4357
        render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
4358
      end
4359 909:cbb26bc654de Chris
4360 0:513646585e45 Chris
      def display_main_menu?(project)
4361
        menu_name = project && !project.new_record? ? :project_menu : :application_menu
4362 1115:433d4f72a19b Chris
        Redmine::MenuManager.items(menu_name).children.present?
4363 0:513646585e45 Chris
      end
4364
4365
      def render_menu(menu, project=nil)
4366
        links = []
4367
        menu_items_for(menu, project) do |node|
4368
          links << render_menu_node(node, project)
4369
        end
4370 909:cbb26bc654de Chris
        links.empty? ? nil : content_tag('ul', links.join("\n").html_safe)
4371 0:513646585e45 Chris
      end
4372
4373
      def render_menu_node(node, project=nil)
4374 1115:433d4f72a19b Chris
        if node.children.present? || !node.child_menus.nil?
4375 0:513646585e45 Chris
          return render_menu_node_with_children(node, project)
4376
        else
4377
          caption, url, selected = extract_node_details(node, project)
4378
          return content_tag('li',
4379
                               render_single_menu_node(node, caption, url, selected))
4380
        end
4381
      end
4382
4383
      def render_menu_node_with_children(node, project=nil)
4384
        caption, url, selected = extract_node_details(node, project)
4385
4386 37:94944d00e43c chris
        html = [].tap do |html|
4387 0:513646585e45 Chris
          html << '<li>'
4388
          # Parent
4389
          html << render_single_menu_node(node, caption, url, selected)
4390
4391
          # Standard children
4392 1115:433d4f72a19b Chris
          standard_children_list = "".html_safe.tap do |child_html|
4393 0:513646585e45 Chris
            node.children.each do |child|
4394
              child_html << render_menu_node(child, project)
4395
            end
4396
          end
4397
4398
          html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
4399
4400
          # Unattached children
4401
          unattached_children_list = render_unattached_children_menu(node, project)
4402
          html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
4403
4404
          html << '</li>'
4405
        end
4406 1115:433d4f72a19b Chris
        return html.join("\n").html_safe
4407 0:513646585e45 Chris
      end
4408
4409
      # Returns a list of unattached children menu items
4410
      def render_unattached_children_menu(node, project)
4411
        return nil unless node.child_menus
4412
4413 1115:433d4f72a19b Chris
        "".html_safe.tap do |child_html|
4414 0:513646585e45 Chris
          unattached_children = node.child_menus.call(project)
4415
          # Tree nodes support #each so we need to do object detection
4416
          if unattached_children.is_a? Array
4417
            unattached_children.each do |child|
4418 909:cbb26bc654de Chris
              child_html << content_tag(:li, render_unattached_menu_item(child, project))
4419 0:513646585e45 Chris
            end
4420
          else
4421
            raise MenuError, ":child_menus must be an array of MenuItems"
4422
          end
4423
        end
4424
      end
4425
4426
      def render_single_menu_node(item, caption, url, selected)
4427
        link_to(h(caption), url, item.html_options(:selected => selected))
4428
      end
4429
4430
      def render_unattached_menu_item(menu_item, project)
4431
        raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
4432
4433
        if User.current.allowed_to?(menu_item.url, project)
4434
          link_to(h(menu_item.caption),
4435
                  menu_item.url,
4436
                  menu_item.html_options)
4437
        end
4438
      end
4439 909:cbb26bc654de Chris
4440 0:513646585e45 Chris
      def menu_items_for(menu, project=nil)
4441
        items = []
4442
        Redmine::MenuManager.items(menu).root.children.each do |node|
4443
          if allowed_node?(node, User.current, project)
4444
            if block_given?
4445
              yield node
4446
            else
4447
              items << node  # TODO: not used?
4448
            end
4449
          end
4450
        end
4451
        return block_given? ? nil : items
4452
      end
4453
4454
      def extract_node_details(node, project=nil)
4455
        item = node
4456
        url = case item.url
4457
        when Hash
4458
          project.nil? ? item.url : {item.param => project}.merge(item.url)
4459
        when Symbol
4460
          send(item.url)
4461
        else
4462
          item.url
4463
        end
4464
        caption = item.caption(project)
4465
        return [caption, url, (current_menu_item == item.name)]
4466
      end
4467
4468
      # Checks if a user is allowed to access the menu item by:
4469
      #
4470 1464:261b3d9a4903 Chris
      # * Checking the url target (project only)
4471 0:513646585e45 Chris
      # * Checking the conditions of the item
4472
      def allowed_node?(node, user, project)
4473 1464:261b3d9a4903 Chris
        if project && user && !user.allowed_to?(node.url, project)
4474
          return false
4475
        end
4476 0:513646585e45 Chris
        if node.condition && !node.condition.call(project)
4477
          # Condition that doesn't pass
4478
          return false
4479
        end
4480 1464:261b3d9a4903 Chris
        return true
4481 0:513646585e45 Chris
      end
4482
    end
4483 909:cbb26bc654de Chris
4484 0:513646585e45 Chris
    class << self
4485
      def map(menu_name)
4486
        @items ||= {}
4487
        mapper = Mapper.new(menu_name.to_sym, @items)
4488
        if block_given?
4489
          yield mapper
4490
        else
4491
          mapper
4492
        end
4493
      end
4494 909:cbb26bc654de Chris
4495 0:513646585e45 Chris
      def items(menu_name)
4496 1115:433d4f72a19b Chris
        @items[menu_name.to_sym] || MenuNode.new(:root, {})
4497 0:513646585e45 Chris
      end
4498
    end
4499 909:cbb26bc654de Chris
4500 0:513646585e45 Chris
    class Mapper
4501 1464:261b3d9a4903 Chris
      attr_reader :menu, :menu_items
4502
4503 0:513646585e45 Chris
      def initialize(menu, items)
4504 1115:433d4f72a19b Chris
        items[menu] ||= MenuNode.new(:root, {})
4505 0:513646585e45 Chris
        @menu = menu
4506
        @menu_items = items[menu]
4507
      end
4508 909:cbb26bc654de Chris
4509 0:513646585e45 Chris
      # Adds an item at the end of the menu. Available options:
4510
      # * param: the parameter name that is used for the project id (default is :id)
4511
      # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
4512
      # * caption that can be:
4513
      #   * a localized string Symbol
4514
      #   * a String
4515
      #   * a Proc that can take the project as argument
4516
      # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
4517
      # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
4518
      # * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item.
4519
      #   eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
4520
      # * last: menu item will stay at the end (eg. :last => true)
4521
      # * html_options: a hash of html options that are passed to link_to
4522
      def push(name, url, options={})
4523
        options = options.dup
4524
4525
        if options[:parent]
4526
          subtree = self.find(options[:parent])
4527
          if subtree
4528
            target_root = subtree
4529
          else
4530
            target_root = @menu_items.root
4531
          end
4532
4533
        else
4534
          target_root = @menu_items.root
4535
        end
4536
4537
        # menu item position
4538
        if first = options.delete(:first)
4539
          target_root.prepend(MenuItem.new(name, url, options))
4540
        elsif before = options.delete(:before)
4541
4542
          if exists?(before)
4543
            target_root.add_at(MenuItem.new(name, url, options), position_of(before))
4544
          else
4545
            target_root.add(MenuItem.new(name, url, options))
4546
          end
4547
4548
        elsif after = options.delete(:after)
4549
4550
          if exists?(after)
4551
            target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
4552
          else
4553
            target_root.add(MenuItem.new(name, url, options))
4554
          end
4555 909:cbb26bc654de Chris
4556 0:513646585e45 Chris
        elsif options[:last] # don't delete, needs to be stored
4557
          target_root.add_last(MenuItem.new(name, url, options))
4558
        else
4559
          target_root.add(MenuItem.new(name, url, options))
4560
        end
4561
      end
4562 909:cbb26bc654de Chris
4563 0:513646585e45 Chris
      # Removes a menu item
4564
      def delete(name)
4565
        if found = self.find(name)
4566
          @menu_items.remove!(found)
4567
        end
4568
      end
4569
4570
      # Checks if a menu item exists
4571
      def exists?(name)
4572
        @menu_items.any? {|node| node.name == name}
4573
      end
4574
4575
      def find(name)
4576
        @menu_items.find {|node| node.name == name}
4577
      end
4578
4579
      def position_of(name)
4580
        @menu_items.each do |node|
4581
          if node.name == name
4582
            return node.position
4583
          end
4584
        end
4585
      end
4586
    end
4587 909:cbb26bc654de Chris
4588 1115:433d4f72a19b Chris
    class MenuNode
4589
      include Enumerable
4590
      attr_accessor :parent
4591
      attr_reader :last_items_count, :name
4592
4593
      def initialize(name, content = nil)
4594
        @name = name
4595
        @children = []
4596
        @last_items_count = 0
4597
      end
4598
4599
      def children
4600
        if block_given?
4601
          @children.each {|child| yield child}
4602
        else
4603
          @children
4604
        end
4605
      end
4606
4607
      # Returns the number of descendants + 1
4608
      def size
4609
        @children.inject(1) {|sum, node| sum + node.size}
4610
      end
4611
4612
      def each &block
4613
        yield self
4614
        children { |child| child.each(&block) }
4615
      end
4616
4617
      # Adds a child at first position
4618
      def prepend(child)
4619
        add_at(child, 0)
4620
      end
4621
4622
      # Adds a child at given position
4623
      def add_at(child, position)
4624
        raise "Child already added" if find {|node| node.name == child.name}
4625
4626
        @children = @children.insert(position, child)
4627
        child.parent = self
4628
        child
4629
      end
4630
4631
      # Adds a child as last child
4632
      def add_last(child)
4633
        add_at(child, -1)
4634
        @last_items_count += 1
4635
        child
4636
      end
4637
4638
      # Adds a child
4639
      def add(child)
4640
        position = @children.size - @last_items_count
4641
        add_at(child, position)
4642
      end
4643
      alias :<< :add
4644
4645
      # Removes a child
4646
      def remove!(child)
4647
        @children.delete(child)
4648
        @last_items_count -= +1 if child && child.last
4649
        child.parent = nil
4650
        child
4651
      end
4652
4653
      # Returns the position for this node in it's parent
4654
      def position
4655
        self.parent.children.index(self)
4656
      end
4657
4658
      # Returns the root for this node
4659
      def root
4660
        root = self
4661
        root = root.parent while root.parent
4662
        root
4663
      end
4664
    end
4665
4666
    class MenuItem < MenuNode
4667 0:513646585e45 Chris
      include Redmine::I18n
4668
      attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
4669 909:cbb26bc654de Chris
4670 0:513646585e45 Chris
      def initialize(name, url, options)
4671
        raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
4672
        raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
4673
        raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
4674
        raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
4675
        @name = name
4676
        @url = url
4677
        @condition = options[:if]
4678
        @param = options[:param] || :id
4679
        @caption = options[:caption]
4680
        @html_options = options[:html] || {}
4681
        # Adds a unique class to each menu item based on its name
4682
        @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
4683
        @parent = options[:parent]
4684
        @child_menus = options[:children]
4685
        @last = options[:last] || false
4686
        super @name.to_sym
4687
      end
4688 909:cbb26bc654de Chris
4689 0:513646585e45 Chris
      def caption(project=nil)
4690
        if @caption.is_a?(Proc)
4691
          c = @caption.call(project).to_s
4692
          c = @name.to_s.humanize if c.blank?
4693
          c
4694
        else
4695
          if @caption.nil?
4696
            l_or_humanize(name, :prefix => 'label_')
4697
          else
4698
            @caption.is_a?(Symbol) ? l(@caption) : @caption
4699
          end
4700
        end
4701
      end
4702 909:cbb26bc654de Chris
4703 0:513646585e45 Chris
      def html_options(options={})
4704
        if options[:selected]
4705
          o = @html_options.dup
4706
          o[:class] += ' selected'
4707
          o
4708
        else
4709
          @html_options
4710
        end
4711
      end
4712 909:cbb26bc654de Chris
    end
4713 0:513646585e45 Chris
  end
4714
end
4715
# Redmine - project management software
4716 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
4717 0:513646585e45 Chris
#
4718
# This program is free software; you can redistribute it and/or
4719
# modify it under the terms of the GNU General Public License
4720
# as published by the Free Software Foundation; either version 2
4721
# of the License, or (at your option) any later version.
4722 909:cbb26bc654de Chris
#
4723 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
4724
# but WITHOUT ANY WARRANTY; without even the implied warranty of
4725
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4726
# GNU General Public License for more details.
4727 909:cbb26bc654de Chris
#
4728 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
4729
# along with this program; if not, write to the Free Software
4730
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
4731
4732 1517:dffacf8a6908 Chris
require 'mime/types'
4733
4734 0:513646585e45 Chris
module Redmine
4735
  module MimeType
4736
4737
    MIME_TYPES = {
4738
      'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade',
4739
      'text/css' => 'css',
4740
      'text/html' => 'html,htm,xhtml',
4741
      'text/jsp' => 'jsp',
4742
      'text/x-c' => 'c,cpp,cc,h,hh',
4743
      'text/x-csharp' => 'cs',
4744
      'text/x-java' => 'java',
4745
      'text/x-html-template' => 'rhtml',
4746
      'text/x-perl' => 'pl,pm',
4747
      'text/x-php' => 'php,php3,php4,php5',
4748
      'text/x-python' => 'py',
4749
      'text/x-ruby' => 'rb,rbw,ruby,rake,erb',
4750
      'text/x-csh' => 'csh',
4751
      'text/x-sh' => 'sh',
4752
      'text/xml' => 'xml,xsd,mxml',
4753
      'text/yaml' => 'yml,yaml',
4754
      'text/csv' => 'csv',
4755 441:cbce1fd3b1b7 Chris
      'text/x-po' => 'po',
4756 0:513646585e45 Chris
      'image/gif' => 'gif',
4757
      'image/jpeg' => 'jpg,jpeg,jpe',
4758
      'image/png' => 'png',
4759
      'image/tiff' => 'tiff,tif',
4760
      'image/x-ms-bmp' => 'bmp',
4761 1115:433d4f72a19b Chris
      'application/javascript' => 'js',
4762 0:513646585e45 Chris
      'application/pdf' => 'pdf',
4763
    }.freeze
4764 909:cbb26bc654de Chris
4765 0:513646585e45 Chris
    EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
4766
      exts.split(',').each {|ext| map[ext.strip] = type}
4767
      map
4768
    end
4769 909:cbb26bc654de Chris
4770 0:513646585e45 Chris
    # returns mime type for name or nil if unknown
4771
    def self.of(name)
4772 1517:dffacf8a6908 Chris
      return nil unless name.present?
4773
      if m = name.to_s.match(/(^|\.)([^\.]+)$/)
4774
        extension = m[2].downcase
4775
        @known_types ||= Hash.new do |h, ext|
4776
          type = EXTENSIONS[ext]
4777
          type ||= MIME::Types.type_for(ext).first.to_s.presence
4778
          h[ext] = type
4779
        end
4780
        @known_types[extension]
4781
      end
4782 0:513646585e45 Chris
    end
4783 909:cbb26bc654de Chris
4784 0:513646585e45 Chris
    # Returns the css class associated to
4785
    # the mime type of name
4786
    def self.css_class_of(name)
4787
      mime = of(name)
4788
      mime && mime.gsub('/', '-')
4789
    end
4790 909:cbb26bc654de Chris
4791 0:513646585e45 Chris
    def self.main_mimetype_of(name)
4792
      mimetype = of(name)
4793
      mimetype.split('/').first if mimetype
4794
    end
4795 909:cbb26bc654de Chris
4796 0:513646585e45 Chris
    # return true if mime-type for name is type/*
4797
    # otherwise false
4798
    def self.is_type?(type, name)
4799
      main_mimetype = main_mimetype_of(name)
4800
      type.to_s == main_mimetype
4801 909:cbb26bc654de Chris
    end
4802 0:513646585e45 Chris
  end
4803
end
4804 37:94944d00e43c chris
module Redmine
4805
  class Notifiable < Struct.new(:name, :parent)
4806
4807
    def to_s
4808
      name
4809
    end
4810 909:cbb26bc654de Chris
4811 37:94944d00e43c chris
    # TODO: Plugin API for adding a new notification?
4812
    def self.all
4813
      notifications = []
4814
      notifications << Notifiable.new('issue_added')
4815
      notifications << Notifiable.new('issue_updated')
4816
      notifications << Notifiable.new('issue_note_added', 'issue_updated')
4817
      notifications << Notifiable.new('issue_status_updated', 'issue_updated')
4818
      notifications << Notifiable.new('issue_priority_updated', 'issue_updated')
4819
      notifications << Notifiable.new('news_added')
4820 441:cbce1fd3b1b7 Chris
      notifications << Notifiable.new('news_comment_added')
4821 37:94944d00e43c chris
      notifications << Notifiable.new('document_added')
4822
      notifications << Notifiable.new('file_added')
4823
      notifications << Notifiable.new('message_posted')
4824
      notifications << Notifiable.new('wiki_content_added')
4825
      notifications << Notifiable.new('wiki_content_updated')
4826
      notifications
4827
    end
4828
  end
4829
end
4830 1464:261b3d9a4903 Chris
# encoding: utf-8
4831
#
4832
# Redmine - project management software
4833 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
4834 1464:261b3d9a4903 Chris
#
4835
# This program is free software; you can redistribute it and/or
4836
# modify it under the terms of the GNU General Public License
4837
# as published by the Free Software Foundation; either version 2
4838
# of the License, or (at your option) any later version.
4839
#
4840
# This program is distributed in the hope that it will be useful,
4841
# but WITHOUT ANY WARRANTY; without even the implied warranty of
4842
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
4843
# GNU General Public License for more details.
4844
#
4845
# You should have received a copy of the GNU General Public License
4846
# along with this program; if not, write to the Free Software
4847
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
4848
4849
module Redmine
4850
  module Pagination
4851
    class Paginator
4852
      attr_reader :item_count, :per_page, :page, :page_param
4853
4854
      def initialize(*args)
4855
        if args.first.is_a?(ActionController::Base)
4856
          args.shift
4857
          ActiveSupport::Deprecation.warn "Paginator no longer takes a controller instance as the first argument. Remove it from #new arguments."
4858
        end
4859
        item_count, per_page, page, page_param = *args
4860
4861
        @item_count = item_count
4862
        @per_page = per_page
4863
        page = (page || 1).to_i
4864
        if page < 1
4865
          page = 1
4866
        end
4867
        @page = page
4868
        @page_param = page_param || :page
4869
      end
4870
4871
      def offset
4872
        (page - 1) * per_page
4873
      end
4874
4875
      def first_page
4876
        if item_count > 0
4877
          1
4878
        end
4879
      end
4880
4881
      def previous_page
4882
        if page > 1
4883
          page - 1
4884
        end
4885
      end
4886
4887
      def next_page
4888
        if last_item < item_count
4889
          page + 1
4890
        end
4891
      end
4892
4893
      def last_page
4894
        if item_count > 0
4895
          (item_count - 1) / per_page + 1
4896
        end
4897
      end
4898
4899
      def first_item
4900
        item_count == 0 ? 0 : (offset + 1)
4901
      end
4902
4903
      def last_item
4904
        l = first_item + per_page - 1
4905
        l > item_count ? item_count : l
4906
      end
4907
4908
      def linked_pages
4909
        pages = []
4910
        if item_count > 0
4911
          pages += [first_page, page, last_page]
4912
          pages += ((page-2)..(page+2)).to_a.select {|p| p > first_page && p < last_page}
4913
        end
4914
        pages = pages.compact.uniq.sort
4915
        if pages.size > 1
4916
          pages
4917
        else
4918
          []
4919
        end
4920
      end
4921
4922
      def items_per_page
4923
        ActiveSupport::Deprecation.warn "Paginator#items_per_page will be removed. Use #per_page instead."
4924
        per_page
4925
      end
4926
4927
      def current
4928
        ActiveSupport::Deprecation.warn "Paginator#current will be removed. Use .offset instead of .current.offset."
4929
        self
4930
      end
4931
    end
4932
4933
    # Paginates the given scope or model. Returns a Paginator instance and
4934
    # the collection of objects for the current page.
4935
    #
4936
    # Options:
4937
    #   :parameter     name of the page parameter
4938
    #
4939
    # Examples:
4940
    #   @user_pages, @users = paginate User.where(:status => 1)
4941
    #
4942
    def paginate(scope, options={})
4943
      options = options.dup
4944
      finder_options = options.extract!(
4945
        :conditions,
4946
        :order,
4947
        :joins,
4948
        :include,
4949
        :select
4950
      )
4951
      if scope.is_a?(Symbol) || finder_options.values.compact.any?
4952
        return deprecated_paginate(scope, finder_options, options)
4953
      end
4954
4955
      paginator = paginator(scope.count, options)
4956
      collection = scope.limit(paginator.per_page).offset(paginator.offset).to_a
4957
4958
      return paginator, collection
4959
    end
4960
4961
    def deprecated_paginate(arg, finder_options, options={})
4962
      ActiveSupport::Deprecation.warn "#paginate with a Symbol and/or find options is depreceted and will be removed. Use a scope instead."
4963
      klass = arg.is_a?(Symbol) ? arg.to_s.classify.constantize : arg
4964
      scope = klass.scoped(finder_options)
4965
      paginate(scope, options)
4966
    end
4967
4968
    def paginator(item_count, options={})
4969
      options.assert_valid_keys :parameter, :per_page
4970
4971
      page_param = options[:parameter] || :page
4972
      page = (params[page_param] || 1).to_i
4973
      per_page = options[:per_page] || per_page_option
4974
      Paginator.new(item_count, per_page, page, page_param)
4975
    end
4976
4977
    module Helper
4978
      include Redmine::I18n
4979
4980
      # Renders the pagination links for the given paginator.
4981
      #
4982
      # Options:
4983
      #   :per_page_links    if set to false, the "Per page" links are not rendered
4984
      #
4985
      def pagination_links_full(*args)
4986
        pagination_links_each(*args) do |text, parameters, options|
4987
          if block_given?
4988
            yield text, parameters, options
4989
          else
4990
            link_to text, params.merge(parameters), options
4991
          end
4992
        end
4993
      end
4994
4995
      # Yields the given block with the text and parameters
4996
      # for each pagination link and returns a string that represents the links
4997
      def pagination_links_each(paginator, count=nil, options={}, &block)
4998
        options.assert_valid_keys :per_page_links
4999
5000
        per_page_links = options.delete(:per_page_links)
5001
        per_page_links = false if count.nil?
5002
        page_param = paginator.page_param
5003
5004
        html = ''
5005
        if paginator.previous_page
5006
          # \xc2\xab(utf-8) = &#171;
5007
          text = "\xc2\xab " + l(:label_previous)
5008
          html << yield(text, {page_param => paginator.previous_page}, :class => 'previous') + ' '
5009
        end
5010
5011
        previous = nil
5012
        paginator.linked_pages.each do |page|
5013
          if previous && previous != page - 1
5014
            html << content_tag('span', '...', :class => 'spacer') + ' '
5015
          end
5016
          if page == paginator.page
5017
            html << content_tag('span', page.to_s, :class => 'current page')
5018
          else
5019
            html << yield(page.to_s, {page_param => page}, :class => 'page')
5020
          end
5021
          html << ' '
5022
          previous = page
5023
        end
5024
5025
        if paginator.next_page
5026
          # \xc2\xbb(utf-8) = &#187;
5027
          text = l(:label_next) + " \xc2\xbb"
5028
          html << yield(text, {page_param => paginator.next_page}, :class => 'next') + ' '
5029
        end
5030
5031
        html << content_tag('span', "(#{paginator.first_item}-#{paginator.last_item}/#{paginator.item_count})", :class => 'items') + ' '
5032
5033
        if per_page_links != false && links = per_page_links(paginator, &block)
5034
          html << content_tag('span', links.to_s, :class => 'per-page')
5035
        end
5036
5037
        html.html_safe
5038
      end
5039
5040
      # Renders the "Per page" links.
5041
      def per_page_links(paginator, &block)
5042
        values = per_page_options(paginator.per_page, paginator.item_count)
5043
        if values.any?
5044
          links = values.collect do |n|
5045
            if n == paginator.per_page
5046
              content_tag('span', n.to_s)
5047
            else
5048
              yield(n, :per_page => n, paginator.page_param => nil)
5049
            end
5050
          end
5051
          l(:label_display_per_page, links.join(', ')).html_safe
5052
        end
5053
      end
5054
5055
      def per_page_options(selected=nil, item_count=nil)
5056
        options = Setting.per_page_options_array
5057
        if item_count && options.any?
5058
          if item_count > options.first
5059
            max = options.detect {|value| value >= item_count} || item_count
5060
          else
5061
            max = item_count
5062
          end
5063
          options = options.select {|value| value <= max || value == selected}
5064
        end
5065
        if options.empty? || (options.size == 1 && options.first == selected)
5066
          []
5067
        else
5068
          options
5069
        end
5070
      end
5071
    end
5072
  end
5073
end
5074 0:513646585e45 Chris
# Redmine - project management software
5075 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
5076 0:513646585e45 Chris
#
5077
# This program is free software; you can redistribute it and/or
5078
# modify it under the terms of the GNU General Public License
5079
# as published by the Free Software Foundation; either version 2
5080
# of the License, or (at your option) any later version.
5081 909:cbb26bc654de Chris
#
5082 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
5083
# but WITHOUT ANY WARRANTY; without even the implied warranty of
5084
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5085
# GNU General Public License for more details.
5086 909:cbb26bc654de Chris
#
5087 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
5088
# along with this program; if not, write to the Free Software
5089
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
5090
5091
module Redmine
5092
  module Platform
5093
    class << self
5094
      def mswin?
5095 909:cbb26bc654de Chris
        (RUBY_PLATFORM =~ /(:?mswin|mingw)/) ||
5096
           (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
5097 0:513646585e45 Chris
      end
5098
    end
5099
  end
5100
end
5101 909:cbb26bc654de Chris
# Redmine - project management software
5102 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
5103 0:513646585e45 Chris
#
5104
# This program is free software; you can redistribute it and/or
5105
# modify it under the terms of the GNU General Public License
5106
# as published by the Free Software Foundation; either version 2
5107
# of the License, or (at your option) any later version.
5108 909:cbb26bc654de Chris
#
5109 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
5110
# but WITHOUT ANY WARRANTY; without even the implied warranty of
5111
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5112
# GNU General Public License for more details.
5113 909:cbb26bc654de Chris
#
5114 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
5115
# along with this program; if not, write to the Free Software
5116
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
5117
5118
module Redmine #:nodoc:
5119
5120
  class PluginNotFound < StandardError; end
5121
  class PluginRequirementError < StandardError; end
5122 909:cbb26bc654de Chris
5123 0:513646585e45 Chris
  # Base class for Redmine plugins.
5124
  # Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
5125 909:cbb26bc654de Chris
  #
5126 0:513646585e45 Chris
  #   Redmine::Plugin.register :example do
5127
  #     name 'Example plugin'
5128
  #     author 'John Smith'
5129
  #     description 'This is an example plugin for Redmine'
5130
  #     version '0.0.1'
5131
  #     settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
5132
  #   end
5133 909:cbb26bc654de Chris
  #
5134 0:513646585e45 Chris
  # === Plugin attributes
5135 909:cbb26bc654de Chris
  #
5136 0:513646585e45 Chris
  # +settings+ is an optional attribute that let the plugin be configurable.
5137
  # It must be a hash with the following keys:
5138
  # * <tt>:default</tt>: default value for the plugin settings
5139
  # * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
5140
  # Example:
5141
  #   settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
5142
  # In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
5143 909:cbb26bc654de Chris
  #
5144 0:513646585e45 Chris
  # When rendered, the plugin settings value is available as the local variable +settings+
5145
  class Plugin
5146 1115:433d4f72a19b Chris
    cattr_accessor :directory
5147
    self.directory = File.join(Rails.root, 'plugins')
5148
5149
    cattr_accessor :public_directory
5150
    self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
5151
5152 0:513646585e45 Chris
    @registered_plugins = {}
5153
    class << self
5154
      attr_reader :registered_plugins
5155
      private :new
5156
5157
      def def_field(*names)
5158 909:cbb26bc654de Chris
        class_eval do
5159 0:513646585e45 Chris
          names.each do |name|
5160 909:cbb26bc654de Chris
            define_method(name) do |*args|
5161 0:513646585e45 Chris
              args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
5162
            end
5163
          end
5164
        end
5165
      end
5166
    end
5167 1464:261b3d9a4903 Chris
    def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
5168 0:513646585e45 Chris
    attr_reader :id
5169 909:cbb26bc654de Chris
5170 0:513646585e45 Chris
    # Plugin constructor
5171
    def self.register(id, &block)
5172
      p = new(id)
5173
      p.instance_eval(&block)
5174 1464:261b3d9a4903 Chris
5175 0:513646585e45 Chris
      # Set a default name if it was not provided during registration
5176
      p.name(id.to_s.humanize) if p.name.nil?
5177 1464:261b3d9a4903 Chris
      # Set a default directory if it was not provided during registration
5178
      p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
5179 1115:433d4f72a19b Chris
5180 0:513646585e45 Chris
      # Adds plugin locales if any
5181
      # YAML translation files should be found under <plugin>/config/locales/
5182 1517:dffacf8a6908 Chris
      Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
5183 1115:433d4f72a19b Chris
5184
      # Prepends the app/views directory of the plugin to the view path
5185
      view_path = File.join(p.directory, 'app', 'views')
5186
      if File.directory?(view_path)
5187
        ActionController::Base.prepend_view_path(view_path)
5188
        ActionMailer::Base.prepend_view_path(view_path)
5189
      end
5190
5191
      # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
5192
      Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
5193
        ActiveSupport::Dependencies.autoload_paths += [dir]
5194
      end
5195
5196 0:513646585e45 Chris
      registered_plugins[id] = p
5197
    end
5198 909:cbb26bc654de Chris
5199
    # Returns an array of all registered plugins
5200 0:513646585e45 Chris
    def self.all
5201
      registered_plugins.values.sort
5202
    end
5203 909:cbb26bc654de Chris
5204 0:513646585e45 Chris
    # Finds a plugin by its id
5205
    # Returns a PluginNotFound exception if the plugin doesn't exist
5206
    def self.find(id)
5207
      registered_plugins[id.to_sym] || raise(PluginNotFound)
5208
    end
5209 909:cbb26bc654de Chris
5210 0:513646585e45 Chris
    # Clears the registered plugins hash
5211
    # It doesn't unload installed plugins
5212
    def self.clear
5213
      @registered_plugins = {}
5214
    end
5215 37:94944d00e43c chris
5216
    # Checks if a plugin is installed
5217
    #
5218
    # @param [String] id name of the plugin
5219
    def self.installed?(id)
5220
      registered_plugins[id.to_sym].present?
5221
    end
5222 909:cbb26bc654de Chris
5223 1115:433d4f72a19b Chris
    def self.load
5224
      Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
5225
        if File.directory?(directory)
5226
          lib = File.join(directory, "lib")
5227
          if File.directory?(lib)
5228
            $:.unshift lib
5229
            ActiveSupport::Dependencies.autoload_paths += [lib]
5230
          end
5231
          initializer = File.join(directory, "init.rb")
5232
          if File.file?(initializer)
5233
            require initializer
5234
          end
5235
        end
5236
      end
5237
    end
5238
5239 0:513646585e45 Chris
    def initialize(id)
5240
      @id = id.to_sym
5241
    end
5242 909:cbb26bc654de Chris
5243 1464:261b3d9a4903 Chris
    def public_directory
5244
      File.join(self.class.public_directory, id.to_s)
5245 1115:433d4f72a19b Chris
    end
5246
5247 1464:261b3d9a4903 Chris
    def to_param
5248
      id
5249 1115:433d4f72a19b Chris
    end
5250
5251
    def assets_directory
5252
      File.join(directory, 'assets')
5253
    end
5254
5255 0:513646585e45 Chris
    def <=>(plugin)
5256
      self.id.to_s <=> plugin.id.to_s
5257
    end
5258 909:cbb26bc654de Chris
5259 0:513646585e45 Chris
    # Sets a requirement on Redmine version
5260
    # Raises a PluginRequirementError exception if the requirement is not met
5261
    #
5262
    # Examples
5263
    #   # Requires Redmine 0.7.3 or higher
5264
    #   requires_redmine :version_or_higher => '0.7.3'
5265
    #   requires_redmine '0.7.3'
5266
    #
5267 1115:433d4f72a19b Chris
    #   # Requires Redmine 0.7.x or higher
5268
    #   requires_redmine '0.7'
5269
    #
5270 0:513646585e45 Chris
    #   # Requires a specific Redmine version
5271
    #   requires_redmine :version => '0.7.3'              # 0.7.3 only
5272 1115:433d4f72a19b Chris
    #   requires_redmine :version => '0.7'                # 0.7.x
5273 0:513646585e45 Chris
    #   requires_redmine :version => ['0.7.3', '0.8.0']   # 0.7.3 or 0.8.0
5274 1115:433d4f72a19b Chris
    #
5275
    #   # Requires a Redmine version within a range
5276
    #   requires_redmine :version => '0.7.3'..'0.9.1'     # >= 0.7.3 and <= 0.9.1
5277
    #   requires_redmine :version => '0.7'..'0.9'         # >= 0.7.x and <= 0.9.x
5278 0:513646585e45 Chris
    def requires_redmine(arg)
5279
      arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
5280
      arg.assert_valid_keys(:version, :version_or_higher)
5281 909:cbb26bc654de Chris
5282 0:513646585e45 Chris
      current = Redmine::VERSION.to_a
5283 1115:433d4f72a19b Chris
      arg.each do |k, req|
5284 0:513646585e45 Chris
        case k
5285
        when :version_or_higher
5286 1115:433d4f72a19b Chris
          raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
5287
          unless compare_versions(req, current) <= 0
5288
            raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
5289 0:513646585e45 Chris
          end
5290
        when :version
5291 1115:433d4f72a19b Chris
          req = [req] if req.is_a?(String)
5292
          if req.is_a?(Array)
5293
            unless req.detect {|ver| compare_versions(ver, current) == 0}
5294
              raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
5295
            end
5296
          elsif req.is_a?(Range)
5297
            unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
5298
              raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
5299
            end
5300
          else
5301
            raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
5302 0:513646585e45 Chris
          end
5303
        end
5304
      end
5305
      true
5306
    end
5307
5308 1115:433d4f72a19b Chris
    def compare_versions(requirement, current)
5309
      requirement = requirement.split('.').collect(&:to_i)
5310
      requirement <=> current.slice(0, requirement.size)
5311
    end
5312
    private :compare_versions
5313
5314 0:513646585e45 Chris
    # Sets a requirement on a Redmine plugin version
5315
    # Raises a PluginRequirementError exception if the requirement is not met
5316
    #
5317
    # Examples
5318
    #   # Requires a plugin named :foo version 0.7.3 or higher
5319
    #   requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
5320
    #   requires_redmine_plugin :foo, '0.7.3'
5321
    #
5322
    #   # Requires a specific version of a Redmine plugin
5323
    #   requires_redmine_plugin :foo, :version => '0.7.3'              # 0.7.3 only
5324
    #   requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0']   # 0.7.3 or 0.8.0
5325
    def requires_redmine_plugin(plugin_name, arg)
5326
      arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
5327
      arg.assert_valid_keys(:version, :version_or_higher)
5328
5329
      plugin = Plugin.find(plugin_name)
5330
      current = plugin.version.split('.').collect(&:to_i)
5331
5332
      arg.each do |k, v|
5333
        v = [] << v unless v.is_a?(Array)
5334
        versions = v.collect {|s| s.split('.').collect(&:to_i)}
5335
        case k
5336
        when :version_or_higher
5337
          raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
5338
          unless (current <=> versions.first) >= 0
5339
            raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
5340
          end
5341
        when :version
5342
          unless versions.include?(current.slice(0,3))
5343
            raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
5344
          end
5345
        end
5346
      end
5347
      true
5348
    end
5349
5350
    # Adds an item to the given +menu+.
5351
    # The +id+ parameter (equals to the project id) is automatically added to the url.
5352
    #   menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
5353 909:cbb26bc654de Chris
    #
5354 0:513646585e45 Chris
    # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
5355 909:cbb26bc654de Chris
    #
5356 0:513646585e45 Chris
    def menu(menu, item, url, options={})
5357
      Redmine::MenuManager.map(menu).push(item, url, options)
5358
    end
5359
    alias :add_menu_item :menu
5360 909:cbb26bc654de Chris
5361 0:513646585e45 Chris
    # Removes +item+ from the given +menu+.
5362
    def delete_menu_item(menu, item)
5363
      Redmine::MenuManager.map(menu).delete(item)
5364
    end
5365
5366
    # Defines a permission called +name+ for the given +actions+.
5367 909:cbb26bc654de Chris
    #
5368 0:513646585e45 Chris
    # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
5369
    #   permission :destroy_contacts, { :contacts => :destroy }
5370
    #   permission :view_contacts, { :contacts => [:index, :show] }
5371 909:cbb26bc654de Chris
    #
5372 1115:433d4f72a19b Chris
    # The +options+ argument is a hash that accept the following keys:
5373
    # * :public => the permission is public if set to true (implicitly given to any user)
5374
    # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
5375
    # * :read => set it to true so that the permission is still granted on closed projects
5376 909:cbb26bc654de Chris
    #
5377 0:513646585e45 Chris
    # Examples
5378
    #   # A permission that is implicitly given to any user
5379
    #   # This permission won't appear on the Roles & Permissions setup screen
5380 1115:433d4f72a19b Chris
    #   permission :say_hello, { :example => :say_hello }, :public => true, :read => true
5381 909:cbb26bc654de Chris
    #
5382 0:513646585e45 Chris
    #   # A permission that can be given to any user
5383
    #   permission :say_hello, { :example => :say_hello }
5384 909:cbb26bc654de Chris
    #
5385 0:513646585e45 Chris
    #   # A permission that can be given to registered users only
5386
    #   permission :say_hello, { :example => :say_hello }, :require => :loggedin
5387 909:cbb26bc654de Chris
    #
5388 0:513646585e45 Chris
    #   # A permission that can be given to project members only
5389
    #   permission :say_hello, { :example => :say_hello }, :require => :member
5390
    def permission(name, actions, options = {})
5391
      if @project_module
5392
        Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
5393
      else
5394
        Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
5395
      end
5396
    end
5397 909:cbb26bc654de Chris
5398 0:513646585e45 Chris
    # Defines a project module, that can be enabled/disabled for each project.
5399
    # Permissions defined inside +block+ will be bind to the module.
5400 909:cbb26bc654de Chris
    #
5401 0:513646585e45 Chris
    #   project_module :things do
5402
    #     permission :view_contacts, { :contacts => [:list, :show] }, :public => true
5403
    #     permission :destroy_contacts, { :contacts => :destroy }
5404
    #   end
5405
    def project_module(name, &block)
5406
      @project_module = name
5407
      self.instance_eval(&block)
5408
      @project_module = nil
5409
    end
5410 909:cbb26bc654de Chris
5411 0:513646585e45 Chris
    # Registers an activity provider.
5412
    #
5413
    # Options:
5414
    # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
5415
    # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
5416 909:cbb26bc654de Chris
    #
5417 0:513646585e45 Chris
    # A model can provide several activity event types.
5418 909:cbb26bc654de Chris
    #
5419 0:513646585e45 Chris
    # Examples:
5420
    #   register :news
5421
    #   register :scrums, :class_name => 'Meeting'
5422
    #   register :issues, :class_name => ['Issue', 'Journal']
5423 909:cbb26bc654de Chris
    #
5424 0:513646585e45 Chris
    # Retrieving events:
5425
    # Associated model(s) must implement the find_events class method.
5426
    # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
5427 909:cbb26bc654de Chris
    #
5428
    # The following call should return all the scrum events visible by current user that occured in the 5 last days:
5429 0:513646585e45 Chris
    #   Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
5430
    #   Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
5431 909:cbb26bc654de Chris
    #
5432 0:513646585e45 Chris
    # Note that :view_scrums permission is required to view these events in the activity view.
5433
    def activity_provider(*args)
5434
      Redmine::Activity.register(*args)
5435
    end
5436 909:cbb26bc654de Chris
5437 0:513646585e45 Chris
    # Registers a wiki formatter.
5438
    #
5439
    # Parameters:
5440
    # * +name+ - human-readable name
5441
    # * +formatter+ - formatter class, which should have an instance method +to_html+
5442
    # * +helper+ - helper module, which will be included by wiki pages
5443
    def wiki_format_provider(name, formatter, helper)
5444
      Redmine::WikiFormatting.register(name, formatter, helper)
5445
    end
5446
5447
    # Returns +true+ if the plugin can be configured.
5448
    def configurable?
5449
      settings && settings.is_a?(Hash) && !settings[:partial].blank?
5450
    end
5451 1115:433d4f72a19b Chris
5452
    def mirror_assets
5453
      source = assets_directory
5454
      destination = public_directory
5455
      return unless File.directory?(source)
5456
5457
      source_files = Dir[source + "/**/*"]
5458
      source_dirs = source_files.select { |d| File.directory?(d) }
5459
      source_files -= source_dirs
5460
5461
      unless source_files.empty?
5462
        base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
5463
        begin
5464
          FileUtils.mkdir_p(base_target_dir)
5465
        rescue Exception => e
5466
          raise "Could not create directory #{base_target_dir}: " + e.message
5467
        end
5468
      end
5469
5470
      source_dirs.each do |dir|
5471
        # strip down these paths so we have simple, relative paths we can
5472
        # add to the destination
5473
        target_dir = File.join(destination, dir.gsub(source, ''))
5474
        begin
5475
          FileUtils.mkdir_p(target_dir)
5476
        rescue Exception => e
5477
          raise "Could not create directory #{target_dir}: " + e.message
5478
        end
5479
      end
5480
5481
      source_files.each do |file|
5482
        begin
5483
          target = File.join(destination, file.gsub(source, ''))
5484
          unless File.exist?(target) && FileUtils.identical?(file, target)
5485
            FileUtils.cp(file, target)
5486
          end
5487
        rescue Exception => e
5488
          raise "Could not copy #{file} to #{target}: " + e.message
5489
        end
5490
      end
5491
    end
5492
5493
    # Mirrors assets from one or all plugins to public/plugin_assets
5494
    def self.mirror_assets(name=nil)
5495
      if name.present?
5496
        find(name).mirror_assets
5497
      else
5498
        all.each do |plugin|
5499
          plugin.mirror_assets
5500
        end
5501
      end
5502
    end
5503
5504
    # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
5505
    def migration_directory
5506
      File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
5507
    end
5508
5509
    # Returns the version number of the latest migration for this plugin. Returns
5510
    # nil if this plugin has no migrations.
5511
    def latest_migration
5512
      migrations.last
5513
    end
5514
5515
    # Returns the version numbers of all migrations for this plugin.
5516
    def migrations
5517
      migrations = Dir[migration_directory+"/*.rb"]
5518
      migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
5519
    end
5520
5521
    # Migrate this plugin to the given version
5522
    def migrate(version = nil)
5523
      puts "Migrating #{id} (#{name})..."
5524
      Redmine::Plugin::Migrator.migrate_plugin(self, version)
5525
    end
5526
5527
    # Migrates all plugins or a single plugin to a given version
5528
    # Exemples:
5529
    #   Plugin.migrate
5530
    #   Plugin.migrate('sample_plugin')
5531
    #   Plugin.migrate('sample_plugin', 1)
5532
    #
5533
    def self.migrate(name=nil, version=nil)
5534
      if name.present?
5535
        find(name).migrate(version)
5536
      else
5537
        all.each do |plugin|
5538
          plugin.migrate
5539
        end
5540
      end
5541
    end
5542
5543
    class Migrator < ActiveRecord::Migrator
5544
      # We need to be able to set the 'current' plugin being migrated.
5545
      cattr_accessor :current_plugin
5546 1464:261b3d9a4903 Chris
5547 1115:433d4f72a19b Chris
      class << self
5548
        # Runs the migrations from a plugin, up (or down) to the version given
5549
        def migrate_plugin(plugin, version)
5550
          self.current_plugin = plugin
5551
          return if current_version(plugin) == version
5552
          migrate(plugin.migration_directory, version)
5553
        end
5554 1464:261b3d9a4903 Chris
5555 1115:433d4f72a19b Chris
        def current_version(plugin=current_plugin)
5556
          # Delete migrations that don't match .. to_i will work because the number comes first
5557
          ::ActiveRecord::Base.connection.select_values(
5558
            "SELECT version FROM #{schema_migrations_table_name}"
5559
          ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
5560
        end
5561
      end
5562 1464:261b3d9a4903 Chris
5563 1115:433d4f72a19b Chris
      def migrated
5564
        sm_table = self.class.schema_migrations_table_name
5565
        ::ActiveRecord::Base.connection.select_values(
5566
          "SELECT version FROM #{sm_table}"
5567
        ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
5568
      end
5569 1464:261b3d9a4903 Chris
5570 1115:433d4f72a19b Chris
      def record_version_state_after_migrating(version)
5571
        super(version.to_s + "-" + current_plugin.id.to_s)
5572
      end
5573
    end
5574 0:513646585e45 Chris
  end
5575
end
5576
# Redmine - project management software
5577 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
5578 0:513646585e45 Chris
#
5579
# This program is free software; you can redistribute it and/or
5580
# modify it under the terms of the GNU General Public License
5581
# as published by the Free Software Foundation; either version 2
5582
# of the License, or (at your option) any later version.
5583 909:cbb26bc654de Chris
#
5584 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
5585
# but WITHOUT ANY WARRANTY; without even the implied warranty of
5586
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5587
# GNU General Public License for more details.
5588 909:cbb26bc654de Chris
#
5589 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
5590
# along with this program; if not, write to the Free Software
5591
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
5592
5593
require 'net/pop'
5594
5595
module Redmine
5596
  module POP3
5597
    class << self
5598
      def check(pop_options={}, options={})
5599
        host = pop_options[:host] || '127.0.0.1'
5600
        port = pop_options[:port] || '110'
5601
        apop = (pop_options[:apop].to_s == '1')
5602
        delete_unprocessed = (pop_options[:delete_unprocessed].to_s == '1')
5603
5604
        pop = Net::POP3.APOP(apop).new(host,port)
5605 210:0579821a129a Chris
        logger.debug "Connecting to #{host}..." if logger && logger.debug?
5606 0:513646585e45 Chris
        pop.start(pop_options[:username], pop_options[:password]) do |pop_session|
5607
          if pop_session.mails.empty?
5608 210:0579821a129a Chris
            logger.debug "No email to process" if logger && logger.debug?
5609 0:513646585e45 Chris
          else
5610 210:0579821a129a Chris
            logger.debug "#{pop_session.mails.size} email(s) to process..." if logger && logger.debug?
5611 0:513646585e45 Chris
            pop_session.each_mail do |msg|
5612
              message = msg.pop
5613 1115:433d4f72a19b Chris
              message_id = (message =~ /^Message-I[dD]: (.*)/ ? $1 : '').strip
5614 0:513646585e45 Chris
              if MailHandler.receive(message, options)
5615
                msg.delete
5616 210:0579821a129a Chris
                logger.debug "--> Message #{message_id} processed and deleted from the server" if logger && logger.debug?
5617 0:513646585e45 Chris
              else
5618
                if delete_unprocessed
5619
                  msg.delete
5620 210:0579821a129a Chris
                  logger.debug "--> Message #{message_id} NOT processed and deleted from the server" if logger && logger.debug?
5621 0:513646585e45 Chris
                else
5622 210:0579821a129a Chris
                  logger.debug "--> Message #{message_id} NOT processed and left on the server" if logger && logger.debug?
5623 0:513646585e45 Chris
                end
5624
              end
5625
            end
5626
          end
5627
        end
5628
      end
5629 909:cbb26bc654de Chris
5630 210:0579821a129a Chris
      private
5631
5632
      def logger
5633 1115:433d4f72a19b Chris
        ::Rails.logger
5634 210:0579821a129a Chris
      end
5635 0:513646585e45 Chris
    end
5636
  end
5637
end
5638 119:8661b858af72 Chris
# Redmine - project management software
5639 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
5640 119:8661b858af72 Chris
#
5641
# This program is free software; you can redistribute it and/or
5642
# modify it under the terms of the GNU General Public License
5643
# as published by the Free Software Foundation; either version 2
5644
# of the License, or (at your option) any later version.
5645 909:cbb26bc654de Chris
#
5646 119:8661b858af72 Chris
# This program is distributed in the hope that it will be useful,
5647
# but WITHOUT ANY WARRANTY; without even the implied warranty of
5648
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5649
# GNU General Public License for more details.
5650 909:cbb26bc654de Chris
#
5651 119:8661b858af72 Chris
# You should have received a copy of the GNU General Public License
5652
# along with this program; if not, write to the Free Software
5653
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
5654
5655
module Redmine
5656
  module SafeAttributes
5657
    def self.included(base)
5658
      base.extend(ClassMethods)
5659
    end
5660 909:cbb26bc654de Chris
5661 119:8661b858af72 Chris
    module ClassMethods
5662
      # Declares safe attributes
5663
      # An optional Proc can be given for conditional inclusion
5664
      #
5665
      # Example:
5666
      #   safe_attributes 'title', 'pages'
5667
      #   safe_attributes 'isbn', :if => {|book, user| book.author == user}
5668
      def safe_attributes(*args)
5669
        @safe_attributes ||= []
5670
        if args.empty?
5671 1115:433d4f72a19b Chris
          if superclass.include?(Redmine::SafeAttributes)
5672
            @safe_attributes + superclass.safe_attributes
5673
          else
5674
            @safe_attributes
5675
          end
5676 119:8661b858af72 Chris
        else
5677
          options = args.last.is_a?(Hash) ? args.pop : {}
5678
          @safe_attributes << [args, options]
5679
        end
5680
      end
5681
    end
5682 909:cbb26bc654de Chris
5683 119:8661b858af72 Chris
    # Returns an array that can be safely set by user or current user
5684
    #
5685
    # Example:
5686
    #   book.safe_attributes # => ['title', 'pages']
5687
    #   book.safe_attributes(book.author) # => ['title', 'pages', 'isbn']
5688 1115:433d4f72a19b Chris
    def safe_attribute_names(user=nil)
5689
      return @safe_attribute_names if @safe_attribute_names && user.nil?
5690 119:8661b858af72 Chris
      names = []
5691
      self.class.safe_attributes.collect do |attrs, options|
5692 1115:433d4f72a19b Chris
        if options[:if].nil? || options[:if].call(self, user || User.current)
5693 119:8661b858af72 Chris
          names += attrs.collect(&:to_s)
5694
        end
5695
      end
5696 1115:433d4f72a19b Chris
      names.uniq!
5697
      @safe_attribute_names = names if user.nil?
5698
      names
5699
    end
5700
5701
    # Returns true if attr can be set by user or the current user
5702
    def safe_attribute?(attr, user=nil)
5703
      safe_attribute_names(user).include?(attr.to_s)
5704 119:8661b858af72 Chris
    end
5705 909:cbb26bc654de Chris
5706 119:8661b858af72 Chris
    # Returns a hash with unsafe attributes removed
5707
    # from the given attrs hash
5708 909:cbb26bc654de Chris
    #
5709 119:8661b858af72 Chris
    # Example:
5710
    #   book.delete_unsafe_attributes({'title' => 'My book', 'foo' => 'bar'})
5711
    #   # => {'title' => 'My book'}
5712
    def delete_unsafe_attributes(attrs, user=User.current)
5713
      safe = safe_attribute_names(user)
5714
      attrs.dup.delete_if {|k,v| !safe.include?(k)}
5715
    end
5716 909:cbb26bc654de Chris
5717 119:8661b858af72 Chris
    # Sets attributes from attrs that are safe
5718
    # attrs is a Hash with string keys
5719
    def safe_attributes=(attrs, user=User.current)
5720
      return unless attrs.is_a?(Hash)
5721
      self.attributes = delete_unsafe_attributes(attrs, user)
5722
    end
5723
  end
5724
end
5725 1464:261b3d9a4903 Chris
# Redmine - project management software
5726 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
5727 1464:261b3d9a4903 Chris
#
5728
# This program is free software; you can redistribute it and/or
5729
# modify it under the terms of the GNU General Public License
5730
# as published by the Free Software Foundation; either version 2
5731
# of the License, or (at your option) any later version.
5732
#
5733
# This program is distributed in the hope that it will be useful,
5734
# but WITHOUT ANY WARRANTY; without even the implied warranty of
5735
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5736
# GNU General Public License for more details.
5737
#
5738
# You should have received a copy of the GNU General Public License
5739
# along with this program; if not, write to the Free Software
5740
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
5741
5742
module Redmine
5743
  module Scm
5744
    module Adapters
5745
      class CommandFailed < StandardError #:nodoc:
5746
      end
5747
    end
5748
  end
5749
end
5750 441:cbce1fd3b1b7 Chris
# Redmine - project management software
5751 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
5752 0:513646585e45 Chris
#
5753
# This program is free software; you can redistribute it and/or
5754
# modify it under the terms of the GNU General Public License
5755
# as published by the Free Software Foundation; either version 2
5756
# of the License, or (at your option) any later version.
5757 441:cbce1fd3b1b7 Chris
#
5758 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
5759
# but WITHOUT ANY WARRANTY; without even the implied warranty of
5760
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
5761
# GNU General Public License for more details.
5762 441:cbce1fd3b1b7 Chris
#
5763 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
5764
# along with this program; if not, write to the Free Software
5765
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
5766
5767
require 'cgi'
5768 1464:261b3d9a4903 Chris
require 'redmine/scm/adapters'
5769
5770
if RUBY_VERSION < '1.9'
5771
  require 'iconv'
5772
end
5773 0:513646585e45 Chris
5774
module Redmine
5775
  module Scm
5776 245:051f544170fe Chris
    module Adapters
5777 0:513646585e45 Chris
      class AbstractAdapter #:nodoc:
5778 909:cbb26bc654de Chris
5779
        # raised if scm command exited with error, e.g. unknown revision.
5780
        class ScmCommandAborted < CommandFailed; end
5781
5782 0:513646585e45 Chris
        class << self
5783 245:051f544170fe Chris
          def client_command
5784
            ""
5785
          end
5786
5787 909:cbb26bc654de Chris
          def shell_quote_command
5788
            if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
5789
              client_command
5790
            else
5791
              shell_quote(client_command)
5792
            end
5793
          end
5794
5795 0:513646585e45 Chris
          # Returns the version of the scm client
5796
          # Eg: [1, 5, 0] or [] if unknown
5797
          def client_version
5798
            []
5799
          end
5800 245:051f544170fe Chris
5801 0:513646585e45 Chris
          # Returns the version string of the scm client
5802
          # Eg: '1.5.0' or 'Unknown version' if unknown
5803
          def client_version_string
5804
            v = client_version || 'Unknown version'
5805
            v.is_a?(Array) ? v.join('.') : v.to_s
5806
          end
5807 245:051f544170fe Chris
5808 0:513646585e45 Chris
          # Returns true if the current client version is above
5809
          # or equals the given one
5810
          # If option is :unknown is set to true, it will return
5811
          # true if the client version is unknown
5812
          def client_version_above?(v, options={})
5813
            ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
5814
          end
5815 245:051f544170fe Chris
5816
          def client_available
5817
            true
5818
          end
5819
5820
          def shell_quote(str)
5821
            if Redmine::Platform.mswin?
5822
              '"' + str.gsub(/"/, '\\"') + '"'
5823
            else
5824
              "'" + str.gsub(/'/, "'\"'\"'") + "'"
5825
            end
5826
          end
5827 0:513646585e45 Chris
        end
5828 245:051f544170fe Chris
5829
        def initialize(url, root_url=nil, login=nil, password=nil,
5830
                       path_encoding=nil)
5831 0:513646585e45 Chris
          @url = url
5832
          @login = login if login && !login.empty?
5833
          @password = (password || "") if @login
5834
          @root_url = root_url.blank? ? retrieve_root_url : root_url
5835
        end
5836 245:051f544170fe Chris
5837 0:513646585e45 Chris
        def adapter_name
5838
          'Abstract'
5839
        end
5840 245:051f544170fe Chris
5841 0:513646585e45 Chris
        def supports_cat?
5842
          true
5843
        end
5844
5845
        def supports_annotate?
5846
          respond_to?('annotate')
5847
        end
5848 245:051f544170fe Chris
5849 0:513646585e45 Chris
        def root_url
5850
          @root_url
5851
        end
5852 245:051f544170fe Chris
5853 0:513646585e45 Chris
        def url
5854
          @url
5855
        end
5856 441:cbce1fd3b1b7 Chris
5857
        def path_encoding
5858
          nil
5859
        end
5860
5861 0:513646585e45 Chris
        # get info about the svn repository
5862
        def info
5863
          return nil
5864
        end
5865 441:cbce1fd3b1b7 Chris
5866 0:513646585e45 Chris
        # Returns the entry identified by path and revision identifier
5867
        # or nil if entry doesn't exist in the repository
5868
        def entry(path=nil, identifier=nil)
5869
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
5870
          search_path = parts[0..-2].join('/')
5871
          search_name = parts[-1]
5872
          if search_path.blank? && search_name.blank?
5873
            # Root entry
5874
            Entry.new(:path => '', :kind => 'dir')
5875
          else
5876
            # Search for the entry in the parent directory
5877
            es = entries(search_path, identifier)
5878
            es ? es.detect {|e| e.name == search_name} : nil
5879
          end
5880
        end
5881 441:cbce1fd3b1b7 Chris
5882 0:513646585e45 Chris
        # Returns an Entries collection
5883
        # or nil if the given path doesn't exist in the repository
5884 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
5885 0:513646585e45 Chris
          return nil
5886
        end
5887
5888
        def branches
5889
          return nil
5890
        end
5891
5892 441:cbce1fd3b1b7 Chris
        def tags
5893 0:513646585e45 Chris
          return nil
5894
        end
5895
5896
        def default_branch
5897
          return nil
5898
        end
5899 441:cbce1fd3b1b7 Chris
5900 0:513646585e45 Chris
        def properties(path, identifier=nil)
5901
          return nil
5902
        end
5903 441:cbce1fd3b1b7 Chris
5904 0:513646585e45 Chris
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
5905
          return nil
5906
        end
5907 441:cbce1fd3b1b7 Chris
5908 0:513646585e45 Chris
        def diff(path, identifier_from, identifier_to=nil)
5909
          return nil
5910
        end
5911 441:cbce1fd3b1b7 Chris
5912 0:513646585e45 Chris
        def cat(path, identifier=nil)
5913
          return nil
5914
        end
5915 441:cbce1fd3b1b7 Chris
5916 0:513646585e45 Chris
        def with_leading_slash(path)
5917
          path ||= ''
5918
          (path[0,1]!="/") ? "/#{path}" : path
5919
        end
5920
5921
        def with_trailling_slash(path)
5922
          path ||= ''
5923
          (path[-1,1] == "/") ? path : "#{path}/"
5924
        end
5925 245:051f544170fe Chris
5926 0:513646585e45 Chris
        def without_leading_slash(path)
5927
          path ||= ''
5928
          path.gsub(%r{^/+}, '')
5929
        end
5930
5931
        def without_trailling_slash(path)
5932
          path ||= ''
5933
          (path[-1,1] == "/") ? path[0..-2] : path
5934 1517:dffacf8a6908 Chris
        end
5935 245:051f544170fe Chris
5936 0:513646585e45 Chris
        def shell_quote(str)
5937 245:051f544170fe Chris
          self.class.shell_quote(str)
5938 0:513646585e45 Chris
        end
5939
5940
      private
5941
        def retrieve_root_url
5942
          info = self.info
5943
          info ? info.root_url : nil
5944
        end
5945 441:cbce1fd3b1b7 Chris
5946 909:cbb26bc654de Chris
        def target(path, sq=true)
5947 0:513646585e45 Chris
          path ||= ''
5948
          base = path.match(/^\//) ? root_url : url
5949 909:cbb26bc654de Chris
          str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
5950
          if sq
5951
            str = shell_quote(str)
5952
          end
5953
          str
5954 0:513646585e45 Chris
        end
5955 245:051f544170fe Chris
5956 0:513646585e45 Chris
        def logger
5957
          self.class.logger
5958
        end
5959 245:051f544170fe Chris
5960 1115:433d4f72a19b Chris
        def shellout(cmd, options = {}, &block)
5961
          self.class.shellout(cmd, options, &block)
5962 0:513646585e45 Chris
        end
5963 245:051f544170fe Chris
5964 0:513646585e45 Chris
        def self.logger
5965 909:cbb26bc654de Chris
          Rails.logger
5966 0:513646585e45 Chris
        end
5967 245:051f544170fe Chris
5968 1464:261b3d9a4903 Chris
        # Path to the file where scm stderr output is logged
5969
        # Returns nil if the log file is not writable
5970
        def self.stderr_log_file
5971
          if @stderr_log_file.nil?
5972
            writable = false
5973
            path = Redmine::Configuration['scm_stderr_log_file'].presence
5974
            path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
5975
            if File.exists?(path)
5976
              if File.file?(path) && File.writable?(path)
5977
                writable = true
5978
              else
5979
                logger.warn("SCM log file (#{path}) is not writable")
5980
              end
5981
            else
5982
              begin
5983
                File.open(path, "w") {}
5984
                writable = true
5985
              rescue => e
5986
                logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
5987
              end
5988
            end
5989
            @stderr_log_file = writable ? path : false
5990
          end
5991
          @stderr_log_file || nil
5992
        end
5993
5994 1115:433d4f72a19b Chris
        def self.shellout(cmd, options = {}, &block)
5995 507:0c939c159af4 Chris
          if logger && logger.debug?
5996
            logger.debug "Shelling out: #{strip_credential(cmd)}"
5997 1464:261b3d9a4903 Chris
            # Capture stderr in a log file
5998
            if stderr_log_file
5999
              cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
6000
            end
6001 0:513646585e45 Chris
          end
6002
          begin
6003 1115:433d4f72a19b Chris
            mode = "r+"
6004 245:051f544170fe Chris
            IO.popen(cmd, mode) do |io|
6005 1115:433d4f72a19b Chris
              io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
6006
              io.close_write unless options[:write_stdin]
6007 0:513646585e45 Chris
              block.call(io) if block_given?
6008
            end
6009 909:cbb26bc654de Chris
          ## If scm command does not exist,
6010
          ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
6011
          ## in production environment.
6012
          # rescue Errno::ENOENT => e
6013
          rescue Exception => e
6014 0:513646585e45 Chris
            msg = strip_credential(e.message)
6015
            # The command failed, log it and re-raise
6016 507:0c939c159af4 Chris
            logmsg = "SCM command failed, "
6017
            logmsg += "make sure that your SCM command (e.g. svn) is "
6018
            logmsg += "in PATH (#{ENV['PATH']})\n"
6019
            logmsg += "You can configure your scm commands in config/configuration.yml.\n"
6020
            logmsg += "#{strip_credential(cmd)}\n"
6021
            logmsg += "with: #{msg}"
6022
            logger.error(logmsg)
6023 0:513646585e45 Chris
            raise CommandFailed.new(msg)
6024
          end
6025 245:051f544170fe Chris
        end
6026
6027 0:513646585e45 Chris
        # Hides username/password in a given command
6028
        def self.strip_credential(cmd)
6029
          q = (Redmine::Platform.mswin? ? '"' : "'")
6030
          cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
6031
        end
6032 441:cbce1fd3b1b7 Chris
6033 0:513646585e45 Chris
        def strip_credential(cmd)
6034
          self.class.strip_credential(cmd)
6035
        end
6036 245:051f544170fe Chris
6037
        def scm_iconv(to, from, str)
6038
          return nil if str.nil?
6039 922:ad295b270cd4 Chris
          # bug 446: non-utf8 paths in repositories blow up repo viewer and reposman
6040
          # -- Remove this short-circuit: we want the conversion to
6041
          #    happen always, so we can trap the error here if the
6042
          #    source text happens not to be in the advertised
6043
          #    encoding (instead of having the database blow up later)
6044
#          return str if to == from
6045 1115:433d4f72a19b Chris
          if str.respond_to?(:force_encoding)
6046
            str.force_encoding(from)
6047
            begin
6048
              str.encode(to)
6049
            rescue Exception => err
6050
              logger.error("failed to convert from #{from} to #{to}. #{err}")
6051
              nil
6052
            end
6053
          else
6054
            begin
6055
              Iconv.conv(to, from, str)
6056
            rescue Iconv::Failure => err
6057
              logger.error("failed to convert from #{from} to #{to}. #{err}")
6058
              nil
6059
            end
6060 245:051f544170fe Chris
          end
6061
        end
6062 1115:433d4f72a19b Chris
6063
        def parse_xml(xml)
6064
          if RUBY_PLATFORM == 'java'
6065
            xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
6066
          end
6067
          ActiveSupport::XmlMini.parse(xml)
6068
        end
6069 0:513646585e45 Chris
      end
6070 245:051f544170fe Chris
6071 0:513646585e45 Chris
      class Entries < Array
6072
        def sort_by_name
6073 1115:433d4f72a19b Chris
          dup.sort! {|x,y|
6074 0:513646585e45 Chris
            if x.kind == y.kind
6075
              x.name.to_s <=> y.name.to_s
6076
            else
6077
              x.kind <=> y.kind
6078
            end
6079 245:051f544170fe Chris
          }
6080 0:513646585e45 Chris
        end
6081 441:cbce1fd3b1b7 Chris
6082 0:513646585e45 Chris
        def revisions
6083
          revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
6084
        end
6085
      end
6086 441:cbce1fd3b1b7 Chris
6087 0:513646585e45 Chris
      class Info
6088
        attr_accessor :root_url, :lastrev
6089
        def initialize(attributes={})
6090
          self.root_url = attributes[:root_url] if attributes[:root_url]
6091
          self.lastrev = attributes[:lastrev]
6092
        end
6093
      end
6094 441:cbce1fd3b1b7 Chris
6095 0:513646585e45 Chris
      class Entry
6096 1115:433d4f72a19b Chris
        attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
6097
6098 0:513646585e45 Chris
        def initialize(attributes={})
6099
          self.name = attributes[:name] if attributes[:name]
6100
          self.path = attributes[:path] if attributes[:path]
6101
          self.kind = attributes[:kind] if attributes[:kind]
6102
          self.size = attributes[:size].to_i if attributes[:size]
6103
          self.lastrev = attributes[:lastrev]
6104
        end
6105 441:cbce1fd3b1b7 Chris
6106 0:513646585e45 Chris
        def is_file?
6107
          'file' == self.kind
6108
        end
6109 441:cbce1fd3b1b7 Chris
6110 0:513646585e45 Chris
        def is_dir?
6111
          'dir' == self.kind
6112
        end
6113 441:cbce1fd3b1b7 Chris
6114 0:513646585e45 Chris
        def is_text?
6115
          Redmine::MimeType.is_type?('text', name)
6116
        end
6117 1115:433d4f72a19b Chris
6118
        def author
6119
          if changeset
6120
            changeset.author.to_s
6121
          elsif lastrev
6122
            Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
6123
          end
6124
        end
6125 0:513646585e45 Chris
      end
6126 441:cbce1fd3b1b7 Chris
6127 0:513646585e45 Chris
      class Revisions < Array
6128
        def latest
6129
          sort {|x,y|
6130
            unless x.time.nil? or y.time.nil?
6131
              x.time <=> y.time
6132
            else
6133
              0
6134
            end
6135
          }.last
6136 441:cbce1fd3b1b7 Chris
        end
6137 0:513646585e45 Chris
      end
6138 441:cbce1fd3b1b7 Chris
6139 0:513646585e45 Chris
      class Revision
6140 441:cbce1fd3b1b7 Chris
        attr_accessor :scmid, :name, :author, :time, :message,
6141 909:cbb26bc654de Chris
                      :paths, :revision, :branch, :identifier,
6142
                      :parents
6143 0:513646585e45 Chris
6144
        def initialize(attributes={})
6145
          self.identifier = attributes[:identifier]
6146 441:cbce1fd3b1b7 Chris
          self.scmid      = attributes[:scmid]
6147
          self.name       = attributes[:name] || self.identifier
6148
          self.author     = attributes[:author]
6149
          self.time       = attributes[:time]
6150
          self.message    = attributes[:message] || ""
6151
          self.paths      = attributes[:paths]
6152
          self.revision   = attributes[:revision]
6153
          self.branch     = attributes[:branch]
6154 909:cbb26bc654de Chris
          self.parents    = attributes[:parents]
6155 117:af80e5618e9b Chris
        end
6156
6157
        # Returns the readable identifier.
6158
        def format_identifier
6159 441:cbce1fd3b1b7 Chris
          self.identifier.to_s
6160 117:af80e5618e9b Chris
        end
6161 1115:433d4f72a19b Chris
6162
        def ==(other)
6163
          if other.nil?
6164
            false
6165
          elsif scmid.present?
6166
            scmid == other.scmid
6167
          elsif identifier.present?
6168
            identifier == other.identifier
6169
          elsif revision.present?
6170
            revision == other.revision
6171
          end
6172
        end
6173 245:051f544170fe Chris
      end
6174 117:af80e5618e9b Chris
6175 0:513646585e45 Chris
      class Annotate
6176
        attr_reader :lines, :revisions
6177 441:cbce1fd3b1b7 Chris
6178 0:513646585e45 Chris
        def initialize
6179
          @lines = []
6180
          @revisions = []
6181
        end
6182 441:cbce1fd3b1b7 Chris
6183 0:513646585e45 Chris
        def add_line(line, revision)
6184
          @lines << line
6185
          @revisions << revision
6186
        end
6187 441:cbce1fd3b1b7 Chris
6188 0:513646585e45 Chris
        def content
6189
          content = lines.join("\n")
6190
        end
6191 441:cbce1fd3b1b7 Chris
6192 0:513646585e45 Chris
        def empty?
6193
          lines.empty?
6194
        end
6195
      end
6196 909:cbb26bc654de Chris
6197
      class Branch < String
6198
        attr_accessor :revision, :scmid
6199
      end
6200 0:513646585e45 Chris
    end
6201
  end
6202
end
6203 441:cbce1fd3b1b7 Chris
# Redmine - project management software
6204 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
6205 0:513646585e45 Chris
#
6206
# This program is free software; you can redistribute it and/or
6207
# modify it under the terms of the GNU General Public License
6208
# as published by the Free Software Foundation; either version 2
6209
# of the License, or (at your option) any later version.
6210 441:cbce1fd3b1b7 Chris
#
6211 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
6212
# but WITHOUT ANY WARRANTY; without even the implied warranty of
6213
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
6214
# GNU General Public License for more details.
6215 441:cbce1fd3b1b7 Chris
#
6216 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
6217
# along with this program; if not, write to the Free Software
6218
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
6219
6220 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
6221 0:513646585e45 Chris
6222
module Redmine
6223
  module Scm
6224 245:051f544170fe Chris
    module Adapters
6225 0:513646585e45 Chris
      class BazaarAdapter < AbstractAdapter
6226 245:051f544170fe Chris
6227 0:513646585e45 Chris
        # Bazaar executable name
6228 210:0579821a129a Chris
        BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
6229 245:051f544170fe Chris
6230
        class << self
6231
          def client_command
6232
            @@bin    ||= BZR_BIN
6233
          end
6234
6235
          def sq_bin
6236 909:cbb26bc654de Chris
            @@sq_bin ||= shell_quote_command
6237 245:051f544170fe Chris
          end
6238
6239
          def client_version
6240
            @@client_version ||= (scm_command_version || [])
6241
          end
6242
6243
          def client_available
6244
            !client_version.empty?
6245
          end
6246
6247
          def scm_command_version
6248
            scm_version = scm_version_from_command_line.dup
6249
            if scm_version.respond_to?(:force_encoding)
6250
              scm_version.force_encoding('ASCII-8BIT')
6251
            end
6252
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
6253
              m[2].scan(%r{\d+}).collect(&:to_i)
6254
            end
6255
          end
6256
6257
          def scm_version_from_command_line
6258
            shellout("#{sq_bin} --version") { |io| io.read }.to_s
6259
          end
6260
        end
6261
6262 1115:433d4f72a19b Chris
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
6263
          @url = url
6264
          @root_url = url
6265
          @path_encoding = 'UTF-8'
6266
          # do not call *super* for non ASCII repository path
6267
        end
6268
6269
        def bzr_path_encodig=(encoding)
6270
          @path_encoding = encoding
6271
        end
6272
6273 0:513646585e45 Chris
        # Get info about the repository
6274
        def info
6275 909:cbb26bc654de Chris
          cmd_args = %w|revno|
6276
          cmd_args << bzr_target('')
6277 0:513646585e45 Chris
          info = nil
6278 909:cbb26bc654de Chris
          scm_cmd(*cmd_args) do |io|
6279 0:513646585e45 Chris
            if io.read =~ %r{^(\d+)\r?$}
6280
              info = Info.new({:root_url => url,
6281
                               :lastrev => Revision.new({
6282
                                 :identifier => $1
6283
                               })
6284
                             })
6285
            end
6286
          end
6287
          info
6288 909:cbb26bc654de Chris
        rescue ScmCommandAborted
6289 0:513646585e45 Chris
          return nil
6290
        end
6291 245:051f544170fe Chris
6292 0:513646585e45 Chris
        # Returns an Entries collection
6293
        # or nil if the given path doesn't exist in the repository
6294 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
6295 0:513646585e45 Chris
          path ||= ''
6296
          entries = Entries.new
6297 441:cbce1fd3b1b7 Chris
          identifier = -1 unless identifier && identifier.to_i > 0
6298 909:cbb26bc654de Chris
          cmd_args = %w|ls -v --show-ids|
6299
          cmd_args << "-r#{identifier.to_i}"
6300
          cmd_args << bzr_target(path)
6301
          scm_cmd(*cmd_args) do |io|
6302 1115:433d4f72a19b Chris
            prefix_utf8 = "#{url}/#{path}".gsub('\\', '/')
6303
            logger.debug "PREFIX: #{prefix_utf8}"
6304
            prefix = scm_iconv(@path_encoding, 'UTF-8', prefix_utf8)
6305
            prefix.force_encoding('ASCII-8BIT') if prefix.respond_to?(:force_encoding)
6306 0:513646585e45 Chris
            re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
6307
            io.each_line do |line|
6308
              next unless line =~ re
6309 1464:261b3d9a4903 Chris
              name_locale, slash, revision = $3.strip, $4, $5.strip
6310 1115:433d4f72a19b Chris
              name = scm_iconv('UTF-8', @path_encoding, name_locale)
6311
              entries << Entry.new({:name => name,
6312
                                    :path => ((path.empty? ? "" : "#{path}/") + name),
6313 1464:261b3d9a4903 Chris
                                    :kind => (slash.blank? ? 'file' : 'dir'),
6314 0:513646585e45 Chris
                                    :size => nil,
6315 1464:261b3d9a4903 Chris
                                    :lastrev => Revision.new(:revision => revision)
6316 0:513646585e45 Chris
                                  })
6317
            end
6318
          end
6319 909:cbb26bc654de Chris
          if logger && logger.debug?
6320
            logger.debug("Found #{entries.size} entries in the repository for #{target(path)}")
6321
          end
6322 0:513646585e45 Chris
          entries.sort_by_name
6323 909:cbb26bc654de Chris
        rescue ScmCommandAborted
6324
          return nil
6325 0:513646585e45 Chris
        end
6326 245:051f544170fe Chris
6327 0:513646585e45 Chris
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
6328
          path ||= ''
6329 117:af80e5618e9b Chris
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
6330
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
6331 0:513646585e45 Chris
          revisions = Revisions.new
6332 909:cbb26bc654de Chris
          cmd_args = %w|log -v --show-ids|
6333
          cmd_args << "-r#{identifier_to}..#{identifier_from}"
6334
          cmd_args << bzr_target(path)
6335
          scm_cmd(*cmd_args) do |io|
6336 0:513646585e45 Chris
            revision = nil
6337 909:cbb26bc654de Chris
            parsing  = nil
6338 0:513646585e45 Chris
            io.each_line do |line|
6339
              if line =~ /^----/
6340
                revisions << revision if revision
6341
                revision = Revision.new(:paths => [], :message => '')
6342
                parsing = nil
6343
              else
6344
                next unless revision
6345
                if line =~ /^revno: (\d+)($|\s\[merge\]$)/
6346
                  revision.identifier = $1.to_i
6347
                elsif line =~ /^committer: (.+)$/
6348
                  revision.author = $1.strip
6349
                elsif line =~ /^revision-id:(.+)$/
6350
                  revision.scmid = $1.strip
6351
                elsif line =~ /^timestamp: (.+)$/
6352
                  revision.time = Time.parse($1).localtime
6353
                elsif line =~ /^    -----/
6354
                  # partial revisions
6355
                  parsing = nil unless parsing == 'message'
6356
                elsif line =~ /^(message|added|modified|removed|renamed):/
6357
                  parsing = $1
6358
                elsif line =~ /^  (.*)$/
6359
                  if parsing == 'message'
6360
                    revision.message << "#{$1}\n"
6361
                  else
6362
                    if $1 =~ /^(.*)\s+(\S+)$/
6363 1115:433d4f72a19b Chris
                      path_locale = $1.strip
6364
                      path = scm_iconv('UTF-8', @path_encoding, path_locale)
6365 0:513646585e45 Chris
                      revid = $2
6366
                      case parsing
6367
                      when 'added'
6368
                        revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
6369
                      when 'modified'
6370
                        revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
6371
                      when 'removed'
6372
                        revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
6373
                      when 'renamed'
6374
                        new_path = path.split('=>').last
6375 1115:433d4f72a19b Chris
                        if new_path
6376
                          revision.paths << {:action => 'M', :path => "/#{new_path.strip}",
6377
                                             :revision => revid}
6378
                        end
6379 0:513646585e45 Chris
                      end
6380
                    end
6381
                  end
6382
                else
6383
                  parsing = nil
6384
                end
6385
              end
6386
            end
6387
            revisions << revision if revision
6388
          end
6389
          revisions
6390 909:cbb26bc654de Chris
        rescue ScmCommandAborted
6391
          return nil
6392 0:513646585e45 Chris
        end
6393 245:051f544170fe Chris
6394 0:513646585e45 Chris
        def diff(path, identifier_from, identifier_to=nil)
6395
          path ||= ''
6396
          if identifier_to
6397 441:cbce1fd3b1b7 Chris
            identifier_to = identifier_to.to_i
6398 0:513646585e45 Chris
          else
6399
            identifier_to = identifier_from.to_i - 1
6400
          end
6401 117:af80e5618e9b Chris
          if identifier_from
6402
            identifier_from = identifier_from.to_i
6403
          end
6404 0:513646585e45 Chris
          diff = []
6405 909:cbb26bc654de Chris
          cmd_args = %w|diff|
6406
          cmd_args << "-r#{identifier_to}..#{identifier_from}"
6407
          cmd_args << bzr_target(path)
6408
          scm_cmd_no_raise(*cmd_args) do |io|
6409 0:513646585e45 Chris
            io.each_line do |line|
6410
              diff << line
6411
            end
6412
          end
6413
          diff
6414
        end
6415 245:051f544170fe Chris
6416 0:513646585e45 Chris
        def cat(path, identifier=nil)
6417
          cat = nil
6418 909:cbb26bc654de Chris
          cmd_args = %w|cat|
6419
          cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
6420
          cmd_args << bzr_target(path)
6421
          scm_cmd(*cmd_args) do |io|
6422 0:513646585e45 Chris
            io.binmode
6423
            cat = io.read
6424
          end
6425
          cat
6426 909:cbb26bc654de Chris
        rescue ScmCommandAborted
6427
          return nil
6428 0:513646585e45 Chris
        end
6429 245:051f544170fe Chris
6430 0:513646585e45 Chris
        def annotate(path, identifier=nil)
6431
          blame = Annotate.new
6432 909:cbb26bc654de Chris
          cmd_args = %w|annotate -q --all|
6433
          cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
6434
          cmd_args << bzr_target(path)
6435
          scm_cmd(*cmd_args) do |io|
6436
            author     = nil
6437 0:513646585e45 Chris
            identifier = nil
6438
            io.each_line do |line|
6439
              next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
6440 441:cbce1fd3b1b7 Chris
              rev = $1
6441
              blame.add_line($3.rstrip,
6442
                 Revision.new(
6443
                  :identifier => rev,
6444
                  :revision   => rev,
6445
                  :author     => $2.strip
6446
                  ))
6447 0:513646585e45 Chris
            end
6448
          end
6449
          blame
6450 909:cbb26bc654de Chris
        rescue ScmCommandAborted
6451
          return nil
6452 0:513646585e45 Chris
        end
6453 909:cbb26bc654de Chris
6454
        def self.branch_conf_path(path)
6455
          bcp = nil
6456
          m = path.match(%r{^(.*[/\\])\.bzr.*$})
6457
          if m
6458
            bcp = m[1]
6459
          else
6460
            bcp = path
6461
          end
6462
          bcp.gsub!(%r{[\/\\]$}, "")
6463
          if bcp
6464
            bcp = File.join(bcp, ".bzr", "branch", "branch.conf")
6465
          end
6466
          bcp
6467
        end
6468
6469
        def append_revisions_only
6470
          return @aro if ! @aro.nil?
6471
          @aro = false
6472
          bcp = self.class.branch_conf_path(url)
6473
          if bcp && File.exist?(bcp)
6474
            begin
6475
              f = File::open(bcp, "r")
6476
              cnt = 0
6477
              f.each_line do |line|
6478
                l = line.chomp.to_s
6479
                if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/
6480
                  str_aro = $1
6481
                  if str_aro.upcase == "TRUE"
6482
                    @aro = true
6483
                    cnt += 1
6484
                  elsif str_aro.upcase == "FALSE"
6485
                    @aro = false
6486
                    cnt += 1
6487
                  end
6488
                  if cnt > 1
6489
                    @aro = false
6490
                    break
6491
                  end
6492
                end
6493
              end
6494
            ensure
6495
              f.close
6496
            end
6497
          end
6498
          @aro
6499
        end
6500
6501
        def scm_cmd(*args, &block)
6502
          full_args = []
6503
          full_args += args
6504 1115:433d4f72a19b Chris
          full_args_locale = []
6505
          full_args.map do |e|
6506
            full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
6507
          end
6508 909:cbb26bc654de Chris
          ret = shellout(
6509 1115:433d4f72a19b Chris
                   self.class.sq_bin + ' ' +
6510
                     full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
6511 909:cbb26bc654de Chris
                   &block
6512
                   )
6513
          if $? && $?.exitstatus != 0
6514
            raise ScmCommandAborted, "bzr exited with non-zero status: #{$?.exitstatus}"
6515
          end
6516
          ret
6517
        end
6518
        private :scm_cmd
6519
6520
        def scm_cmd_no_raise(*args, &block)
6521
          full_args = []
6522
          full_args += args
6523 1115:433d4f72a19b Chris
          full_args_locale = []
6524
          full_args.map do |e|
6525
            full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
6526
          end
6527 909:cbb26bc654de Chris
          ret = shellout(
6528 1115:433d4f72a19b Chris
                   self.class.sq_bin + ' ' +
6529
                     full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
6530 909:cbb26bc654de Chris
                   &block
6531
                   )
6532
          ret
6533
        end
6534
        private :scm_cmd_no_raise
6535
6536
        def bzr_target(path)
6537
          target(path, false)
6538
        end
6539
        private :bzr_target
6540 0:513646585e45 Chris
      end
6541
    end
6542
  end
6543
end
6544
# redMine - project management software
6545
# Copyright (C) 2006-2007  Jean-Philippe Lang
6546
#
6547
# This program is free software; you can redistribute it and/or
6548
# modify it under the terms of the GNU General Public License
6549
# as published by the Free Software Foundation; either version 2
6550
# of the License, or (at your option) any later version.
6551 441:cbce1fd3b1b7 Chris
#
6552 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
6553
# but WITHOUT ANY WARRANTY; without even the implied warranty of
6554
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
6555
# GNU General Public License for more details.
6556 441:cbce1fd3b1b7 Chris
#
6557 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
6558
# along with this program; if not, write to the Free Software
6559
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
6560
6561 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
6562 0:513646585e45 Chris
6563
module Redmine
6564
  module Scm
6565
    module Adapters
6566
      class CvsAdapter < AbstractAdapter
6567
6568
        # CVS executable name
6569 210:0579821a129a Chris
        CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
6570 245:051f544170fe Chris
6571
        class << self
6572
          def client_command
6573
            @@bin    ||= CVS_BIN
6574
          end
6575
6576
          def sq_bin
6577 909:cbb26bc654de Chris
            @@sq_bin ||= shell_quote_command
6578 245:051f544170fe Chris
          end
6579
6580
          def client_version
6581
            @@client_version ||= (scm_command_version || [])
6582
          end
6583
6584
          def client_available
6585
            client_version_above?([1, 12])
6586
          end
6587
6588
          def scm_command_version
6589
            scm_version = scm_version_from_command_line.dup
6590
            if scm_version.respond_to?(:force_encoding)
6591
              scm_version.force_encoding('ASCII-8BIT')
6592
            end
6593
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
6594
              m[2].scan(%r{\d+}).collect(&:to_i)
6595
            end
6596
          end
6597
6598
          def scm_version_from_command_line
6599
            shellout("#{sq_bin} --version") { |io| io.read }.to_s
6600
          end
6601
        end
6602
6603 0:513646585e45 Chris
        # Guidelines for the input:
6604 441:cbce1fd3b1b7 Chris
        #  url      -> the project-path, relative to the cvsroot (eg. module name)
6605 0:513646585e45 Chris
        #  root_url -> the good old, sometimes damned, CVSROOT
6606 441:cbce1fd3b1b7 Chris
        #  login    -> unnecessary
6607 0:513646585e45 Chris
        #  password -> unnecessary too
6608 245:051f544170fe Chris
        def initialize(url, root_url=nil, login=nil, password=nil,
6609
                       path_encoding=nil)
6610 441:cbce1fd3b1b7 Chris
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
6611
          @url      = url
6612
          # TODO: better Exception here (IllegalArgumentException)
6613
          raise CommandFailed if root_url.blank?
6614
          @root_url  = root_url
6615
6616
          # These are unused.
6617
          @login    = login if login && !login.empty?
6618 0:513646585e45 Chris
          @password = (password || "") if @login
6619
        end
6620 245:051f544170fe Chris
6621 441:cbce1fd3b1b7 Chris
        def path_encoding
6622
          @path_encoding
6623 0:513646585e45 Chris
        end
6624 245:051f544170fe Chris
6625 0:513646585e45 Chris
        def info
6626
          logger.debug "<cvs> info"
6627
          Info.new({:root_url => @root_url, :lastrev => nil})
6628
        end
6629 245:051f544170fe Chris
6630 0:513646585e45 Chris
        def get_previous_revision(revision)
6631
          CvsRevisionHelper.new(revision).prevRev
6632
        end
6633 245:051f544170fe Chris
6634 0:513646585e45 Chris
        # Returns an Entries collection
6635
        # or nil if the given path doesn't exist in the repository
6636
        # this method is used by the repository-browser (aka LIST)
6637 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
6638 0:513646585e45 Chris
          logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
6639 441:cbce1fd3b1b7 Chris
          path_locale = scm_iconv(@path_encoding, 'UTF-8', path)
6640
          path_locale.force_encoding("ASCII-8BIT") if path_locale.respond_to?(:force_encoding)
6641 0:513646585e45 Chris
          entries = Entries.new
6642 441:cbce1fd3b1b7 Chris
          cmd_args = %w|-q rls -e|
6643
          cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
6644
          cmd_args << path_with_proj(path)
6645
          scm_cmd(*cmd_args) do |io|
6646
            io.each_line() do |line|
6647
              fields = line.chop.split('/',-1)
6648 0:513646585e45 Chris
              logger.debug(">>InspectLine #{fields.inspect}")
6649
              if fields[0]!="D"
6650 441:cbce1fd3b1b7 Chris
                time = nil
6651
                # Thu Dec 13 16:27:22 2007
6652
                time_l = fields[-3].split(' ')
6653
                if time_l.size == 5 && time_l[4].length == 4
6654
                  begin
6655
                    time = Time.parse(
6656
                             "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
6657
                  rescue
6658
                  end
6659
                end
6660
                entries << Entry.new(
6661
                 {
6662
                  :name => scm_iconv('UTF-8', @path_encoding, fields[-5]),
6663 0:513646585e45 Chris
                  #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
6664 441:cbce1fd3b1b7 Chris
                  :path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[-5]}"),
6665 0:513646585e45 Chris
                  :kind => 'file',
6666
                  :size => nil,
6667 441:cbce1fd3b1b7 Chris
                  :lastrev => Revision.new(
6668
                      {
6669
                        :revision => fields[-4],
6670
                        :name     => scm_iconv('UTF-8', @path_encoding, fields[-4]),
6671
                        :time     => time,
6672
                        :author   => ''
6673
                      })
6674 0:513646585e45 Chris
                  })
6675
              else
6676 441:cbce1fd3b1b7 Chris
                entries << Entry.new(
6677
                 {
6678
                  :name    => scm_iconv('UTF-8', @path_encoding, fields[1]),
6679
                  :path    => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[1]}"),
6680
                  :kind    => 'dir',
6681
                  :size    => nil,
6682 0:513646585e45 Chris
                  :lastrev => nil
6683 441:cbce1fd3b1b7 Chris
                 })
6684 0:513646585e45 Chris
              end
6685 441:cbce1fd3b1b7 Chris
            end
6686 0:513646585e45 Chris
          end
6687
          entries.sort_by_name
6688 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
6689
          nil
6690 245:051f544170fe Chris
        end
6691 0:513646585e45 Chris
6692
        STARTLOG="----------------------------"
6693
        ENDLOG  ="============================================================================="
6694 245:051f544170fe Chris
6695 0:513646585e45 Chris
        # Returns all revisions found between identifier_from and identifier_to
6696
        # in the repository. both identifier have to be dates or nil.
6697
        # these method returns nothing but yield every result in block
6698
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
6699 441:cbce1fd3b1b7 Chris
          path_with_project_utf8   = path_with_proj(path)
6700
          path_with_project_locale = scm_iconv(@path_encoding, 'UTF-8', path_with_project_utf8)
6701
          logger.debug "<cvs> revisions path:" +
6702
              "'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
6703
          cmd_args = %w|-q rlog|
6704
          cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
6705
          cmd_args << path_with_project_utf8
6706
          scm_cmd(*cmd_args) do |io|
6707
            state      = "entry_start"
6708
            commit_log = String.new
6709
            revision   = nil
6710
            date       = nil
6711
            author     = nil
6712
            entry_path = nil
6713
            entry_name = nil
6714
            file_state = nil
6715
            branch_map = nil
6716 245:051f544170fe Chris
            io.each_line() do |line|
6717 441:cbce1fd3b1b7 Chris
              if state != "revision" && /^#{ENDLOG}/ =~ line
6718
                commit_log = String.new
6719
                revision   = nil
6720
                state      = "entry_start"
6721 0:513646585e45 Chris
              end
6722 441:cbce1fd3b1b7 Chris
              if state == "entry_start"
6723
                branch_map = Hash.new
6724
                if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
6725 0:513646585e45 Chris
                  entry_path = normalize_cvs_path($1)
6726
                  entry_name = normalize_path(File.basename($1))
6727
                  logger.debug("Path #{entry_path} <=> Name #{entry_name}")
6728
                elsif /^head: (.+)$/ =~ line
6729
                  entry_headRev = $1 #unless entry.nil?
6730
                elsif /^symbolic names:/ =~ line
6731 441:cbce1fd3b1b7 Chris
                  state = "symbolic" #unless entry.nil?
6732 0:513646585e45 Chris
                elsif /^#{STARTLOG}/ =~ line
6733 441:cbce1fd3b1b7 Chris
                  commit_log = String.new
6734
                  state      = "revision"
6735
                end
6736 0:513646585e45 Chris
                next
6737 441:cbce1fd3b1b7 Chris
              elsif state == "symbolic"
6738
                if /^(.*):\s(.*)/ =~ (line.strip)
6739
                  branch_map[$1] = $2
6740 0:513646585e45 Chris
                else
6741 441:cbce1fd3b1b7 Chris
                  state = "tags"
6742 0:513646585e45 Chris
                  next
6743 441:cbce1fd3b1b7 Chris
                end
6744
              elsif state == "tags"
6745 0:513646585e45 Chris
                if /^#{STARTLOG}/ =~ line
6746
                  commit_log = ""
6747 441:cbce1fd3b1b7 Chris
                  state = "revision"
6748 0:513646585e45 Chris
                elsif /^#{ENDLOG}/ =~ line
6749 441:cbce1fd3b1b7 Chris
                  state = "head"
6750 0:513646585e45 Chris
                end
6751
                next
6752 441:cbce1fd3b1b7 Chris
              elsif state == "revision"
6753 245:051f544170fe Chris
                if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
6754 0:513646585e45 Chris
                  if revision
6755 441:cbce1fd3b1b7 Chris
                    revHelper = CvsRevisionHelper.new(revision)
6756
                    revBranch = "HEAD"
6757
                    branch_map.each() do |branch_name, branch_point|
6758 0:513646585e45 Chris
                      if revHelper.is_in_branch_with_symbol(branch_point)
6759 441:cbce1fd3b1b7 Chris
                        revBranch = branch_name
6760 0:513646585e45 Chris
                      end
6761
                    end
6762
                    logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
6763 245:051f544170fe Chris
                    yield Revision.new({
6764 441:cbce1fd3b1b7 Chris
                      :time    => date,
6765
                      :author  => author,
6766
                      :message => commit_log.chomp,
6767 0:513646585e45 Chris
                      :paths => [{
6768 1115:433d4f72a19b Chris
                        :revision => revision.dup,
6769
                        :branch   => revBranch.dup,
6770 441:cbce1fd3b1b7 Chris
                        :path     => scm_iconv('UTF-8', @path_encoding, entry_path),
6771
                        :name     => scm_iconv('UTF-8', @path_encoding, entry_name),
6772
                        :kind     => 'file',
6773
                        :action   => file_state
6774
                           }]
6775
                         })
6776 0:513646585e45 Chris
                  end
6777 441:cbce1fd3b1b7 Chris
                  commit_log = String.new
6778
                  revision   = nil
6779 0:513646585e45 Chris
                  if /^#{ENDLOG}/ =~ line
6780 441:cbce1fd3b1b7 Chris
                    state = "entry_start"
6781 0:513646585e45 Chris
                  end
6782
                  next
6783
                end
6784 245:051f544170fe Chris
6785 0:513646585e45 Chris
                if /^branches: (.+)$/ =~ line
6786 441:cbce1fd3b1b7 Chris
                  # TODO: version.branch = $1
6787 0:513646585e45 Chris
                elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
6788 441:cbce1fd3b1b7 Chris
                  revision = $1
6789 0:513646585e45 Chris
                elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
6790 441:cbce1fd3b1b7 Chris
                  date       = Time.parse($1)
6791
                  line_utf8    = scm_iconv('UTF-8', options[:log_encoding], line)
6792
                  author_utf8  = /author: ([^;]+)/.match(line_utf8)[1]
6793
                  author       = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
6794
                  file_state   = /state: ([^;]+)/.match(line)[1]
6795
                  # TODO:
6796
                  #    linechanges only available in CVS....
6797
                  #    maybe a feature our SVN implementation.
6798
                  #    I'm sure, they are useful for stats or something else
6799 0:513646585e45 Chris
                  #                linechanges =/lines: \+(\d+) -(\d+)/.match(line)
6800
                  #                unless linechanges.nil?
6801
                  #                  version.line_plus  = linechanges[1]
6802
                  #                  version.line_minus = linechanges[2]
6803
                  #                else
6804
                  #                  version.line_plus  = 0
6805 245:051f544170fe Chris
                  #                  version.line_minus = 0
6806
                  #                end
6807
                else
6808 0:513646585e45 Chris
                  commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
6809 245:051f544170fe Chris
                end
6810
              end
6811 0:513646585e45 Chris
            end
6812
          end
6813 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
6814
          Revisions.new
6815 245:051f544170fe Chris
        end
6816
6817 0:513646585e45 Chris
        def diff(path, identifier_from, identifier_to=nil)
6818 441:cbce1fd3b1b7 Chris
          logger.debug "<cvs> diff path:'#{path}'" +
6819
              ",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
6820
          cmd_args = %w|rdiff -u|
6821
          cmd_args << "-r#{identifier_to}"
6822
          cmd_args << "-r#{identifier_from}"
6823
          cmd_args << path_with_proj(path)
6824 0:513646585e45 Chris
          diff = []
6825 441:cbce1fd3b1b7 Chris
          scm_cmd(*cmd_args) do |io|
6826 0:513646585e45 Chris
            io.each_line do |line|
6827
              diff << line
6828
            end
6829
          end
6830
          diff
6831 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
6832
          nil
6833 245:051f544170fe Chris
        end
6834
6835 0:513646585e45 Chris
        def cat(path, identifier=nil)
6836
          identifier = (identifier) ? identifier : "HEAD"
6837
          logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
6838 441:cbce1fd3b1b7 Chris
          cmd_args = %w|-q co|
6839
          cmd_args << "-D" << time_to_cvstime(identifier) if identifier
6840
          cmd_args << "-p" << path_with_proj(path)
6841 0:513646585e45 Chris
          cat = nil
6842 441:cbce1fd3b1b7 Chris
          scm_cmd(*cmd_args) do |io|
6843 245:051f544170fe Chris
            io.binmode
6844 0:513646585e45 Chris
            cat = io.read
6845
          end
6846
          cat
6847 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
6848
          nil
6849 245:051f544170fe Chris
        end
6850 0:513646585e45 Chris
6851
        def annotate(path, identifier=nil)
6852 441:cbce1fd3b1b7 Chris
          identifier = (identifier) ? identifier : "HEAD"
6853 0:513646585e45 Chris
          logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
6854 441:cbce1fd3b1b7 Chris
          cmd_args = %w|rannotate|
6855
          cmd_args << "-D" << time_to_cvstime(identifier) if identifier
6856
          cmd_args << path_with_proj(path)
6857 0:513646585e45 Chris
          blame = Annotate.new
6858 441:cbce1fd3b1b7 Chris
          scm_cmd(*cmd_args) do |io|
6859 0:513646585e45 Chris
            io.each_line do |line|
6860
              next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
6861 441:cbce1fd3b1b7 Chris
              blame.add_line(
6862
                  $3.rstrip,
6863
                  Revision.new(
6864
                    :revision   => $1,
6865
                    :identifier => nil,
6866
                    :author     => $2.strip
6867
                    ))
6868 0:513646585e45 Chris
            end
6869
          end
6870
          blame
6871 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
6872
          Annotate.new
6873 0:513646585e45 Chris
        end
6874 245:051f544170fe Chris
6875 0:513646585e45 Chris
        private
6876 245:051f544170fe Chris
6877 0:513646585e45 Chris
        # Returns the root url without the connexion string
6878
        # :pserver:anonymous@foo.bar:/path => /path
6879
        # :ext:cvsservername:/path => /path
6880
        def root_url_path
6881 1464:261b3d9a4903 Chris
          root_url.to_s.gsub(%r{^:.+?(?=/)}, '')
6882 0:513646585e45 Chris
        end
6883
6884
        # convert a date/time into the CVS-format
6885
        def time_to_cvstime(time)
6886
          return nil if time.nil?
6887 441:cbce1fd3b1b7 Chris
          time = Time.now if time == 'HEAD'
6888
6889 0:513646585e45 Chris
          unless time.kind_of? Time
6890
            time = Time.parse(time)
6891
          end
6892 441:cbce1fd3b1b7 Chris
          return time_to_cvstime_rlog(time)
6893 0:513646585e45 Chris
        end
6894 210:0579821a129a Chris
6895
        def time_to_cvstime_rlog(time)
6896
          return nil if time.nil?
6897
          t1 = time.clone.localtime
6898
          return t1.strftime("%Y-%m-%d %H:%M:%S")
6899
        end
6900 441:cbce1fd3b1b7 Chris
6901 0:513646585e45 Chris
        def normalize_cvs_path(path)
6902
          normalize_path(path.gsub(/Attic\//,''))
6903
        end
6904 441:cbce1fd3b1b7 Chris
6905 0:513646585e45 Chris
        def normalize_path(path)
6906
          path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
6907 441:cbce1fd3b1b7 Chris
        end
6908
6909
        def path_with_proj(path)
6910
          "#{url}#{with_leading_slash(path)}"
6911
        end
6912
        private :path_with_proj
6913
6914
        class Revision < Redmine::Scm::Adapters::Revision
6915
          # Returns the readable identifier
6916
          def format_identifier
6917
            revision.to_s
6918
          end
6919
        end
6920
6921
        def scm_cmd(*args, &block)
6922 909:cbb26bc654de Chris
          full_args = ['-d', root_url]
6923 441:cbce1fd3b1b7 Chris
          full_args += args
6924
          full_args_locale = []
6925
          full_args.map do |e|
6926
            full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
6927
          end
6928 909:cbb26bc654de Chris
          ret = shellout(
6929
                   self.class.sq_bin + ' ' + full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
6930
                   &block
6931
                   )
6932 441:cbce1fd3b1b7 Chris
          if $? && $?.exitstatus != 0
6933
            raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
6934
          end
6935
          ret
6936
        end
6937
        private :scm_cmd
6938
      end
6939
6940 0:513646585e45 Chris
      class CvsRevisionHelper
6941
        attr_accessor :complete_rev, :revision, :base, :branchid
6942 441:cbce1fd3b1b7 Chris
6943 0:513646585e45 Chris
        def initialize(complete_rev)
6944
          @complete_rev = complete_rev
6945
          parseRevision()
6946
        end
6947 441:cbce1fd3b1b7 Chris
6948 0:513646585e45 Chris
        def branchPoint
6949
          return @base
6950
        end
6951 441:cbce1fd3b1b7 Chris
6952 0:513646585e45 Chris
        def branchVersion
6953
          if isBranchRevision
6954
            return @base+"."+@branchid
6955
          end
6956
          return @base
6957
        end
6958 441:cbce1fd3b1b7 Chris
6959 0:513646585e45 Chris
        def isBranchRevision
6960
          !@branchid.nil?
6961
        end
6962 441:cbce1fd3b1b7 Chris
6963 0:513646585e45 Chris
        def prevRev
6964 441:cbce1fd3b1b7 Chris
          unless @revision == 0
6965
            return buildRevision( @revision - 1 )
6966 0:513646585e45 Chris
          end
6967 441:cbce1fd3b1b7 Chris
          return buildRevision( @revision )
6968 0:513646585e45 Chris
        end
6969 441:cbce1fd3b1b7 Chris
6970 0:513646585e45 Chris
        def is_in_branch_with_symbol(branch_symbol)
6971 441:cbce1fd3b1b7 Chris
          bpieces = branch_symbol.split(".")
6972
          branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
6973
          return ( branchVersion == branch_start )
6974 0:513646585e45 Chris
        end
6975 441:cbce1fd3b1b7 Chris
6976 0:513646585e45 Chris
        private
6977
        def buildRevision(rev)
6978 441:cbce1fd3b1b7 Chris
          if rev == 0
6979 245:051f544170fe Chris
            if @branchid.nil?
6980 441:cbce1fd3b1b7 Chris
              @base + ".0"
6981 245:051f544170fe Chris
            else
6982
              @base
6983
            end
6984 441:cbce1fd3b1b7 Chris
          elsif @branchid.nil?
6985
            @base + "." + rev.to_s
6986 0:513646585e45 Chris
          else
6987 441:cbce1fd3b1b7 Chris
            @base + "." + @branchid + "." + rev.to_s
6988 0:513646585e45 Chris
          end
6989
        end
6990 441:cbce1fd3b1b7 Chris
6991 0:513646585e45 Chris
        # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
6992
        def parseRevision()
6993 441:cbce1fd3b1b7 Chris
          pieces = @complete_rev.split(".")
6994
          @revision = pieces.last.to_i
6995
          baseSize = 1
6996
          baseSize += (pieces.size / 2)
6997
          @base = pieces[0..-baseSize].join(".")
6998 0:513646585e45 Chris
          if baseSize > 2
6999 441:cbce1fd3b1b7 Chris
            @branchid = pieces[-2]
7000
          end
7001 0:513646585e45 Chris
        end
7002
      end
7003
    end
7004
  end
7005
end
7006 441:cbce1fd3b1b7 Chris
# Redmine - project management software
7007 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
7008 0:513646585e45 Chris
#
7009
# This program is free software; you can redistribute it and/or
7010
# modify it under the terms of the GNU General Public License
7011
# as published by the Free Software Foundation; either version 2
7012
# of the License, or (at your option) any later version.
7013 441:cbce1fd3b1b7 Chris
#
7014 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
7015
# but WITHOUT ANY WARRANTY; without even the implied warranty of
7016
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
7017
# GNU General Public License for more details.
7018 441:cbce1fd3b1b7 Chris
#
7019 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
7020
# along with this program; if not, write to the Free Software
7021
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
7022
7023 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
7024 0:513646585e45 Chris
require 'rexml/document'
7025
7026
module Redmine
7027
  module Scm
7028 245:051f544170fe Chris
    module Adapters
7029
      class DarcsAdapter < AbstractAdapter
7030 0:513646585e45 Chris
        # Darcs executable name
7031 210:0579821a129a Chris
        DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
7032 245:051f544170fe Chris
7033 0:513646585e45 Chris
        class << self
7034 245:051f544170fe Chris
          def client_command
7035
            @@bin    ||= DARCS_BIN
7036
          end
7037
7038
          def sq_bin
7039 909:cbb26bc654de Chris
            @@sq_bin ||= shell_quote_command
7040 245:051f544170fe Chris
          end
7041
7042 0:513646585e45 Chris
          def client_version
7043
            @@client_version ||= (darcs_binary_version || [])
7044
          end
7045 245:051f544170fe Chris
7046
          def client_available
7047
            !client_version.empty?
7048
          end
7049
7050 0:513646585e45 Chris
          def darcs_binary_version
7051 245:051f544170fe Chris
            darcsversion = darcs_binary_version_from_command_line.dup
7052
            if darcsversion.respond_to?(:force_encoding)
7053
              darcsversion.force_encoding('ASCII-8BIT')
7054
            end
7055 210:0579821a129a Chris
            if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
7056
              m[2].scan(%r{\d+}).collect(&:to_i)
7057 0:513646585e45 Chris
            end
7058 210:0579821a129a Chris
          end
7059
7060
          def darcs_binary_version_from_command_line
7061 245:051f544170fe Chris
            shellout("#{sq_bin} --version") { |io| io.read }.to_s
7062 0:513646585e45 Chris
          end
7063
        end
7064
7065 245:051f544170fe Chris
        def initialize(url, root_url=nil, login=nil, password=nil,
7066
                       path_encoding=nil)
7067 0:513646585e45 Chris
          @url = url
7068
          @root_url = url
7069
        end
7070
7071
        def supports_cat?
7072
          # cat supported in darcs 2.0.0 and higher
7073
          self.class.client_version_above?([2, 0, 0])
7074
        end
7075
7076
        # Get info about the darcs repository
7077
        def info
7078
          rev = revisions(nil,nil,nil,{:limit => 1})
7079
          rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
7080
        end
7081 245:051f544170fe Chris
7082 0:513646585e45 Chris
        # Returns an Entries collection
7083
        # or nil if the given path doesn't exist in the repository
7084 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
7085 0:513646585e45 Chris
          path_prefix = (path.blank? ? '' : "#{path}/")
7086 210:0579821a129a Chris
          if path.blank?
7087
            path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
7088
          end
7089 245:051f544170fe Chris
          entries = Entries.new
7090
          cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
7091 0:513646585e45 Chris
          cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
7092
          cmd << " #{shell_quote path}"
7093
          shellout(cmd) do |io|
7094
            begin
7095
              doc = REXML::Document.new(io)
7096
              if doc.root.name == 'directory'
7097
                doc.elements.each('directory/*') do |element|
7098
                  next unless ['file', 'directory'].include? element.name
7099
                  entries << entry_from_xml(element, path_prefix)
7100
                end
7101
              elsif doc.root.name == 'file'
7102
                entries << entry_from_xml(doc.root, path_prefix)
7103
              end
7104
            rescue
7105
            end
7106
          end
7107
          return nil if $? && $?.exitstatus != 0
7108 1115:433d4f72a19b Chris
          entries.compact!
7109
          entries.sort_by_name
7110 0:513646585e45 Chris
        end
7111 245:051f544170fe Chris
7112 0:513646585e45 Chris
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
7113
          path = '.' if path.blank?
7114
          revisions = Revisions.new
7115 245:051f544170fe Chris
          cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
7116 0:513646585e45 Chris
          cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
7117
          cmd << " --last #{options[:limit].to_i}" if options[:limit]
7118
          shellout(cmd) do |io|
7119
            begin
7120
              doc = REXML::Document.new(io)
7121
              doc.elements.each("changelog/patch") do |patch|
7122
                message = patch.elements['name'].text
7123
                message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
7124
                revisions << Revision.new({:identifier => nil,
7125
                              :author => patch.attributes['author'],
7126
                              :scmid => patch.attributes['hash'],
7127
                              :time => Time.parse(patch.attributes['local_date']),
7128
                              :message => message,
7129
                              :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
7130
                            })
7131
              end
7132
            rescue
7133
            end
7134
          end
7135
          return nil if $? && $?.exitstatus != 0
7136
          revisions
7137
        end
7138 245:051f544170fe Chris
7139 0:513646585e45 Chris
        def diff(path, identifier_from, identifier_to=nil)
7140
          path = '*' if path.blank?
7141 245:051f544170fe Chris
          cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
7142 0:513646585e45 Chris
          if identifier_to.nil?
7143
            cmd << " --match #{shell_quote("hash #{identifier_from}")}"
7144
          else
7145
            cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
7146
            cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
7147
          end
7148
          cmd << " -u #{shell_quote path}"
7149
          diff = []
7150
          shellout(cmd) do |io|
7151
            io.each_line do |line|
7152
              diff << line
7153
            end
7154
          end
7155
          return nil if $? && $?.exitstatus != 0
7156
          diff
7157
        end
7158 245:051f544170fe Chris
7159 0:513646585e45 Chris
        def cat(path, identifier=nil)
7160 245:051f544170fe Chris
          cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
7161 0:513646585e45 Chris
          cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
7162
          cmd << " #{shell_quote path}"
7163
          cat = nil
7164
          shellout(cmd) do |io|
7165
            io.binmode
7166
            cat = io.read
7167
          end
7168
          return nil if $? && $?.exitstatus != 0
7169
          cat
7170
        end
7171
7172
        private
7173 245:051f544170fe Chris
7174 0:513646585e45 Chris
        # Returns an Entry from the given XML element
7175
        # or nil if the entry was deleted
7176
        def entry_from_xml(element, path_prefix)
7177
          modified_element = element.elements['modified']
7178
          if modified_element.elements['modified_how'].text.match(/removed/)
7179
            return nil
7180
          end
7181 245:051f544170fe Chris
7182 0:513646585e45 Chris
          Entry.new({:name => element.attributes['name'],
7183
                     :path => path_prefix + element.attributes['name'],
7184
                     :kind => element.name == 'file' ? 'file' : 'dir',
7185
                     :size => nil,
7186
                     :lastrev => Revision.new({
7187
                       :identifier => nil,
7188
                       :scmid => modified_element.elements['patch'].attributes['hash']
7189
                       })
7190 245:051f544170fe Chris
                     })
7191 0:513646585e45 Chris
        end
7192 210:0579821a129a Chris
7193
        def get_paths_for_patch(hash)
7194
          paths = get_paths_for_patch_raw(hash)
7195
          if self.class.client_version_above?([2, 4])
7196
            orig_paths = paths
7197
            paths = []
7198
            add_paths = []
7199
            add_paths_name = []
7200
            mod_paths = []
7201
            other_paths = []
7202
            orig_paths.each do |path|
7203
              if path[:action] == 'A'
7204
                add_paths << path
7205
                add_paths_name << path[:path]
7206
              elsif path[:action] == 'M'
7207
                mod_paths << path
7208
              else
7209
                other_paths << path
7210
              end
7211
            end
7212
            add_paths_name.each do |add_path|
7213
              mod_paths.delete_if { |m| m[:path] == add_path }
7214
            end
7215
            paths.concat add_paths
7216
            paths.concat mod_paths
7217
            paths.concat other_paths
7218
          end
7219
          paths
7220
        end
7221 245:051f544170fe Chris
7222 0:513646585e45 Chris
        # Retrieve changed paths for a single patch
7223 210:0579821a129a Chris
        def get_paths_for_patch_raw(hash)
7224 245:051f544170fe Chris
          cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
7225 0:513646585e45 Chris
          cmd << " --match #{shell_quote("hash #{hash}")} "
7226
          paths = []
7227
          shellout(cmd) do |io|
7228
            begin
7229
              # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
7230
              # A root element is added so that REXML doesn't raise an error
7231
              doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
7232
              doc.elements.each('fake_root/summary/*') do |modif|
7233
                paths << {:action => modif.name[0,1].upcase,
7234
                          :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
7235
                         }
7236
              end
7237
            rescue
7238
            end
7239
          end
7240
          paths
7241
        rescue CommandFailed
7242
          paths
7243
        end
7244
      end
7245
    end
7246
  end
7247
end
7248 1115:433d4f72a19b Chris
# Redmine - project management software
7249 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
7250 0:513646585e45 Chris
#
7251
# FileSystem adapter
7252
# File written by Paul Rivier, at Demotera.
7253
#
7254
# This program is free software; you can redistribute it and/or
7255
# modify it under the terms of the GNU General Public License
7256
# as published by the Free Software Foundation; either version 2
7257
# of the License, or (at your option) any later version.
7258 441:cbce1fd3b1b7 Chris
#
7259 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
7260
# but WITHOUT ANY WARRANTY; without even the implied warranty of
7261
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
7262
# GNU General Public License for more details.
7263 441:cbce1fd3b1b7 Chris
#
7264 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
7265
# along with this program; if not, write to the Free Software
7266
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
7267
7268 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
7269 0:513646585e45 Chris
require 'find'
7270
7271
module Redmine
7272
  module Scm
7273 245:051f544170fe Chris
    module Adapters
7274 0:513646585e45 Chris
      class FilesystemAdapter < AbstractAdapter
7275
7276 245:051f544170fe Chris
        class << self
7277
          def client_available
7278
            true
7279
          end
7280
        end
7281
7282
        def initialize(url, root_url=nil, login=nil, password=nil,
7283
                       path_encoding=nil)
7284 0:513646585e45 Chris
          @url = with_trailling_slash(url)
7285 441:cbce1fd3b1b7 Chris
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
7286
        end
7287
7288
        def path_encoding
7289
          @path_encoding
7290 0:513646585e45 Chris
        end
7291
7292
        def format_path_ends(path, leading=true, trailling=true)
7293 441:cbce1fd3b1b7 Chris
          path = leading ? with_leading_slash(path) :
7294 0:513646585e45 Chris
            without_leading_slash(path)
7295 441:cbce1fd3b1b7 Chris
          trailling ? with_trailling_slash(path) :
7296
            without_trailling_slash(path)
7297 0:513646585e45 Chris
        end
7298
7299
        def info
7300
          info = Info.new({:root_url => target(),
7301
                            :lastrev => nil
7302
                          })
7303
          info
7304
        rescue CommandFailed
7305
          return nil
7306
        end
7307 245:051f544170fe Chris
7308 441:cbce1fd3b1b7 Chris
        def entries(path="", identifier=nil, options={})
7309 0:513646585e45 Chris
          entries = Entries.new
7310 245:051f544170fe Chris
          trgt_utf8 = target(path)
7311
          trgt = scm_iconv(@path_encoding, 'UTF-8', trgt_utf8)
7312
          Dir.new(trgt).each do |e1|
7313
            e_utf8 = scm_iconv('UTF-8', @path_encoding, e1)
7314 441:cbce1fd3b1b7 Chris
            next if e_utf8.blank?
7315
            relative_path_utf8 = format_path_ends(
7316
                (format_path_ends(path,false,true) + e_utf8),false,false)
7317 245:051f544170fe Chris
            t1_utf8 = target(relative_path_utf8)
7318
            t1 = scm_iconv(@path_encoding, 'UTF-8', t1_utf8)
7319
            relative_path = scm_iconv(@path_encoding, 'UTF-8', relative_path_utf8)
7320
            e1 = scm_iconv(@path_encoding, 'UTF-8', e_utf8)
7321
            if File.exist?(t1) and # paranoid test
7322
                  %w{file directory}.include?(File.ftype(t1)) and # avoid special types
7323
                  not File.basename(e1).match(/^\.+$/) # avoid . and ..
7324
              p1         = File.readable?(t1) ? relative_path : ""
7325
              utf_8_path = scm_iconv('UTF-8', @path_encoding, p1)
7326
              entries <<
7327
                Entry.new({ :name => scm_iconv('UTF-8', @path_encoding, File.basename(e1)),
7328 0:513646585e45 Chris
                          # below : list unreadable files, but dont link them.
7329 245:051f544170fe Chris
                          :path => utf_8_path,
7330
                          :kind => (File.directory?(t1) ? 'dir' : 'file'),
7331
                          :size => (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first),
7332 441:cbce1fd3b1b7 Chris
                          :lastrev =>
7333 245:051f544170fe Chris
                              Revision.new({:time => (File.mtime(t1)) })
7334
                        })
7335
            end
7336 0:513646585e45 Chris
          end
7337
          entries.sort_by_name
7338 245:051f544170fe Chris
        rescue  => err
7339
          logger.error "scm: filesystem: error: #{err.message}"
7340
          raise CommandFailed.new(err.message)
7341 0:513646585e45 Chris
        end
7342 245:051f544170fe Chris
7343 0:513646585e45 Chris
        def cat(path, identifier=nil)
7344 245:051f544170fe Chris
          p = scm_iconv(@path_encoding, 'UTF-8', target(path))
7345
          File.new(p, "rb").read
7346
        rescue  => err
7347
          logger.error "scm: filesystem: error: #{err.message}"
7348
          raise CommandFailed.new(err.message)
7349 0:513646585e45 Chris
        end
7350
7351
        private
7352 245:051f544170fe Chris
7353 0:513646585e45 Chris
        # AbstractAdapter::target is implicitly made to quote paths.
7354
        # Here we do not shell-out, so we do not want quotes.
7355
        def target(path=nil)
7356 245:051f544170fe Chris
          # Prevent the use of ..
7357 0:513646585e45 Chris
          if path and !path.match(/(^|\/)\.\.(\/|$)/)
7358
            return "#{self.url}#{without_leading_slash(path)}"
7359
          end
7360
          return self.url
7361
        end
7362
      end
7363
    end
7364
  end
7365
end
7366 441:cbce1fd3b1b7 Chris
# Redmine - project management software
7367 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
7368 0:513646585e45 Chris
#
7369
# This program is free software; you can redistribute it and/or
7370
# modify it under the terms of the GNU General Public License
7371
# as published by the Free Software Foundation; either version 2
7372
# of the License, or (at your option) any later version.
7373 441:cbce1fd3b1b7 Chris
#
7374 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
7375
# but WITHOUT ANY WARRANTY; without even the implied warranty of
7376
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
7377
# GNU General Public License for more details.
7378 441:cbce1fd3b1b7 Chris
#
7379 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
7380
# along with this program; if not, write to the Free Software
7381
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
7382
7383 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
7384 0:513646585e45 Chris
7385
module Redmine
7386
  module Scm
7387 245:051f544170fe Chris
    module Adapters
7388 0:513646585e45 Chris
      class GitAdapter < AbstractAdapter
7389 245:051f544170fe Chris
7390 0:513646585e45 Chris
        # Git executable name
7391 210:0579821a129a Chris
        GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
7392 0:513646585e45 Chris
7393 1115:433d4f72a19b Chris
        class GitBranch < Branch
7394
          attr_accessor :is_default
7395
        end
7396
7397 245:051f544170fe Chris
        class << self
7398
          def client_command
7399
            @@bin    ||= GIT_BIN
7400
          end
7401
7402
          def sq_bin
7403 909:cbb26bc654de Chris
            @@sq_bin ||= shell_quote_command
7404 245:051f544170fe Chris
          end
7405
7406
          def client_version
7407
            @@client_version ||= (scm_command_version || [])
7408
          end
7409
7410
          def client_available
7411
            !client_version.empty?
7412
          end
7413
7414
          def scm_command_version
7415
            scm_version = scm_version_from_command_line.dup
7416
            if scm_version.respond_to?(:force_encoding)
7417
              scm_version.force_encoding('ASCII-8BIT')
7418
            end
7419
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
7420
              m[2].scan(%r{\d+}).collect(&:to_i)
7421
            end
7422
          end
7423
7424
          def scm_version_from_command_line
7425
            shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
7426
          end
7427
        end
7428
7429
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
7430
          super
7431 441:cbce1fd3b1b7 Chris
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
7432
        end
7433
7434
        def path_encoding
7435
          @path_encoding
7436 245:051f544170fe Chris
        end
7437
7438 0:513646585e45 Chris
        def info
7439
          begin
7440
            Info.new(:root_url => url, :lastrev => lastrev('',nil))
7441
          rescue
7442
            nil
7443
          end
7444
        end
7445
7446
        def branches
7447
          return @branches if @branches
7448
          @branches = []
7449 909:cbb26bc654de Chris
          cmd_args = %w|branch --no-color --verbose --no-abbrev|
7450 1115:433d4f72a19b Chris
          git_cmd(cmd_args) do |io|
7451 0:513646585e45 Chris
            io.each_line do |line|
7452 1115:433d4f72a19b Chris
              branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
7453
              bran = GitBranch.new(branch_rev[2])
7454
              bran.revision =  branch_rev[3]
7455
              bran.scmid    =  branch_rev[3]
7456
              bran.is_default = ( branch_rev[1] == '*' )
7457 909:cbb26bc654de Chris
              @branches << bran
7458 0:513646585e45 Chris
            end
7459
          end
7460
          @branches.sort!
7461 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
7462
          nil
7463 0:513646585e45 Chris
        end
7464
7465
        def tags
7466
          return @tags if @tags
7467 441:cbce1fd3b1b7 Chris
          cmd_args = %w|tag|
7468 1115:433d4f72a19b Chris
          git_cmd(cmd_args) do |io|
7469 0:513646585e45 Chris
            @tags = io.readlines.sort!.map{|t| t.strip}
7470
          end
7471 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
7472
          nil
7473
        end
7474
7475
        def default_branch
7476
          bras = self.branches
7477
          return nil if bras.nil?
7478 1115:433d4f72a19b Chris
          default_bras = bras.select{|x| x.is_default == true}
7479
          return default_bras.first.to_s if ! default_bras.empty?
7480
          master_bras = bras.select{|x| x.to_s == 'master'}
7481
          master_bras.empty? ? bras.first.to_s : 'master'
7482 441:cbce1fd3b1b7 Chris
        end
7483
7484
        def entry(path=nil, identifier=nil)
7485
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
7486
          search_path = parts[0..-2].join('/')
7487
          search_name = parts[-1]
7488
          if search_path.blank? && search_name.blank?
7489
            # Root entry
7490
            Entry.new(:path => '', :kind => 'dir')
7491
          else
7492
            # Search for the entry in the parent directory
7493
            es = entries(search_path, identifier,
7494
                         options = {:report_last_commit => false})
7495
            es ? es.detect {|e| e.name == search_name} : nil
7496
          end
7497 0:513646585e45 Chris
        end
7498
7499 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
7500 0:513646585e45 Chris
          path ||= ''
7501 441:cbce1fd3b1b7 Chris
          p = scm_iconv(@path_encoding, 'UTF-8', path)
7502 0:513646585e45 Chris
          entries = Entries.new
7503 441:cbce1fd3b1b7 Chris
          cmd_args = %w|ls-tree -l|
7504
          cmd_args << "HEAD:#{p}"          if identifier.nil?
7505
          cmd_args << "#{identifier}:#{p}" if identifier
7506 1115:433d4f72a19b Chris
          git_cmd(cmd_args) do |io|
7507 0:513646585e45 Chris
            io.each_line do |line|
7508
              e = line.chomp.to_s
7509 37:94944d00e43c chris
              if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
7510 0:513646585e45 Chris
                type = $1
7511 441:cbce1fd3b1b7 Chris
                sha  = $2
7512 0:513646585e45 Chris
                size = $3
7513
                name = $4
7514 441:cbce1fd3b1b7 Chris
                if name.respond_to?(:force_encoding)
7515
                  name.force_encoding(@path_encoding)
7516
                end
7517
                full_path = p.empty? ? name : "#{p}/#{name}"
7518
                n      = scm_iconv('UTF-8', @path_encoding, name)
7519
                full_p = scm_iconv('UTF-8', @path_encoding, full_path)
7520
                entries << Entry.new({:name => n,
7521
                 :path => full_p,
7522 0:513646585e45 Chris
                 :kind => (type == "tree") ? 'dir' : 'file',
7523
                 :size => (type == "tree") ? nil : size,
7524 441:cbce1fd3b1b7 Chris
                 :lastrev => options[:report_last_commit] ?
7525
                                 lastrev(full_path, identifier) : Revision.new
7526 0:513646585e45 Chris
                }) unless entries.detect{|entry| entry.name == name}
7527
              end
7528
            end
7529
          end
7530
          entries.sort_by_name
7531 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
7532
          nil
7533 0:513646585e45 Chris
        end
7534
7535 245:051f544170fe Chris
        def lastrev(path, rev)
7536 0:513646585e45 Chris
          return nil if path.nil?
7537 245:051f544170fe Chris
          cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
7538 441:cbce1fd3b1b7 Chris
          cmd_args << rev if rev
7539 245:051f544170fe Chris
          cmd_args << "--" << path unless path.empty?
7540 117:af80e5618e9b Chris
          lines = []
7541 1115:433d4f72a19b Chris
          git_cmd(cmd_args) { |io| lines = io.readlines }
7542 117:af80e5618e9b Chris
          begin
7543
              id = lines[0].split[1]
7544
              author = lines[1].match('Author:\s+(.*)$')[1]
7545 245:051f544170fe Chris
              time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
7546 0:513646585e45 Chris
7547
              Revision.new({
7548
                :identifier => id,
7549 441:cbce1fd3b1b7 Chris
                :scmid      => id,
7550
                :author     => author,
7551
                :time       => time,
7552
                :message    => nil,
7553
                :paths      => nil
7554 245:051f544170fe Chris
                })
7555 117:af80e5618e9b Chris
          rescue NoMethodError => e
7556 0:513646585e45 Chris
              logger.error("The revision '#{path}' has a wrong format")
7557
              return nil
7558
          end
7559 245:051f544170fe Chris
        rescue ScmCommandAborted
7560
          nil
7561 0:513646585e45 Chris
        end
7562
7563
        def revisions(path, identifier_from, identifier_to, options={})
7564 441:cbce1fd3b1b7 Chris
          revs = Revisions.new
7565 1115:433d4f72a19b Chris
          cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
7566 245:051f544170fe Chris
          cmd_args << "--reverse" if options[:reverse]
7567
          cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
7568 441:cbce1fd3b1b7 Chris
          cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
7569 1115:433d4f72a19b Chris
          revisions = []
7570
          if identifier_from || identifier_to
7571
            revisions << ""
7572
            revisions[0] << "#{identifier_from}.." if identifier_from
7573
            revisions[0] << "#{identifier_to}" if identifier_to
7574
          else
7575
            unless options[:includes].blank?
7576
              revisions += options[:includes]
7577
            end
7578
            unless options[:excludes].blank?
7579
              revisions += options[:excludes].map{|r| "^#{r}"}
7580
            end
7581
          end
7582 0:513646585e45 Chris
7583 1115:433d4f72a19b Chris
          git_cmd(cmd_args, {:write_stdin => true}) do |io|
7584
            io.binmode
7585
            io.puts(revisions.join("\n"))
7586
            io.close_write
7587 0:513646585e45 Chris
            files=[]
7588
            changeset = {}
7589
            parsing_descr = 0  #0: not parsing desc or files, 1: parsing desc, 2: parsing files
7590
7591
            io.each_line do |line|
7592 909:cbb26bc654de Chris
              if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
7593 0:513646585e45 Chris
                key = "commit"
7594
                value = $1
7595 909:cbb26bc654de Chris
                parents_str = $2
7596 0:513646585e45 Chris
                if (parsing_descr == 1 || parsing_descr == 2)
7597
                  parsing_descr = 0
7598
                  revision = Revision.new({
7599
                    :identifier => changeset[:commit],
7600 441:cbce1fd3b1b7 Chris
                    :scmid      => changeset[:commit],
7601
                    :author     => changeset[:author],
7602
                    :time       => Time.parse(changeset[:date]),
7603
                    :message    => changeset[:description],
7604 909:cbb26bc654de Chris
                    :paths      => files,
7605
                    :parents    => changeset[:parents]
7606 0:513646585e45 Chris
                  })
7607
                  if block_given?
7608
                    yield revision
7609
                  else
7610 441:cbce1fd3b1b7 Chris
                    revs << revision
7611 0:513646585e45 Chris
                  end
7612
                  changeset = {}
7613
                  files = []
7614
                end
7615
                changeset[:commit] = $1
7616 909:cbb26bc654de Chris
                unless parents_str.nil? or parents_str == ""
7617
                  changeset[:parents] = parents_str.strip.split(' ')
7618
                end
7619 0:513646585e45 Chris
              elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
7620
                key = $1
7621
                value = $2
7622
                if key == "Author"
7623
                  changeset[:author] = value
7624
                elsif key == "CommitDate"
7625
                  changeset[:date] = value
7626
                end
7627
              elsif (parsing_descr == 0) && line.chomp.to_s == ""
7628
                parsing_descr = 1
7629
                changeset[:description] = ""
7630
              elsif (parsing_descr == 1 || parsing_descr == 2) \
7631 441:cbce1fd3b1b7 Chris
                  && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
7632 0:513646585e45 Chris
                parsing_descr = 2
7633 441:cbce1fd3b1b7 Chris
                fileaction    = $1
7634
                filepath      = $2
7635
                p = scm_iconv('UTF-8', @path_encoding, filepath)
7636
                files << {:action => fileaction, :path => p}
7637 0:513646585e45 Chris
              elsif (parsing_descr == 1 || parsing_descr == 2) \
7638 441:cbce1fd3b1b7 Chris
                  && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
7639 0:513646585e45 Chris
                parsing_descr = 2
7640 441:cbce1fd3b1b7 Chris
                fileaction    = $1
7641
                filepath      = $3
7642
                p = scm_iconv('UTF-8', @path_encoding, filepath)
7643
                files << {:action => fileaction, :path => p}
7644 0:513646585e45 Chris
              elsif (parsing_descr == 1) && line.chomp.to_s == ""
7645
                parsing_descr = 2
7646
              elsif (parsing_descr == 1)
7647
                changeset[:description] << line[4..-1]
7648
              end
7649 441:cbce1fd3b1b7 Chris
            end
7650 0:513646585e45 Chris
7651
            if changeset[:commit]
7652
              revision = Revision.new({
7653
                :identifier => changeset[:commit],
7654 441:cbce1fd3b1b7 Chris
                :scmid      => changeset[:commit],
7655
                :author     => changeset[:author],
7656
                :time       => Time.parse(changeset[:date]),
7657
                :message    => changeset[:description],
7658 909:cbb26bc654de Chris
                :paths      => files,
7659
                :parents    => changeset[:parents]
7660 441:cbce1fd3b1b7 Chris
                 })
7661 0:513646585e45 Chris
              if block_given?
7662
                yield revision
7663
              else
7664 441:cbce1fd3b1b7 Chris
                revs << revision
7665 0:513646585e45 Chris
              end
7666
            end
7667
          end
7668 441:cbce1fd3b1b7 Chris
          revs
7669
        rescue ScmCommandAborted => e
7670 1115:433d4f72a19b Chris
          err_msg = "git log error: #{e.message}"
7671
          logger.error(err_msg)
7672
          if block_given?
7673
            raise CommandFailed, err_msg
7674
          else
7675
            revs
7676
          end
7677 0:513646585e45 Chris
        end
7678
7679
        def diff(path, identifier_from, identifier_to=nil)
7680
          path ||= ''
7681 441:cbce1fd3b1b7 Chris
          cmd_args = []
7682 0:513646585e45 Chris
          if identifier_to
7683 441:cbce1fd3b1b7 Chris
            cmd_args << "diff" << "--no-color" <<  identifier_to << identifier_from
7684 0:513646585e45 Chris
          else
7685 441:cbce1fd3b1b7 Chris
            cmd_args << "show" << "--no-color" << identifier_from
7686 0:513646585e45 Chris
          end
7687 441:cbce1fd3b1b7 Chris
          cmd_args << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
7688 0:513646585e45 Chris
          diff = []
7689 1115:433d4f72a19b Chris
          git_cmd(cmd_args) do |io|
7690 0:513646585e45 Chris
            io.each_line do |line|
7691
              diff << line
7692
            end
7693
          end
7694
          diff
7695 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
7696
          nil
7697 0:513646585e45 Chris
        end
7698 441:cbce1fd3b1b7 Chris
7699 0:513646585e45 Chris
        def annotate(path, identifier=nil)
7700
          identifier = 'HEAD' if identifier.blank?
7701 1464:261b3d9a4903 Chris
          cmd_args = %w|blame --encoding=UTF-8|
7702 441:cbce1fd3b1b7 Chris
          cmd_args << "-p" << identifier << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path)
7703 0:513646585e45 Chris
          blame = Annotate.new
7704
          content = nil
7705 1115:433d4f72a19b Chris
          git_cmd(cmd_args) { |io| io.binmode; content = io.read }
7706 0:513646585e45 Chris
          # git annotates binary files
7707
          return nil if content.is_binary_data?
7708
          identifier = ''
7709
          # git shows commit author on the first occurrence only
7710
          authors_by_commit = {}
7711
          content.split("\n").each do |line|
7712
            if line =~ /^([0-9a-f]{39,40})\s.*/
7713
              identifier = $1
7714
            elsif line =~ /^author (.+)/
7715
              authors_by_commit[identifier] = $1.strip
7716
            elsif line =~ /^\t(.*)/
7717 441:cbce1fd3b1b7 Chris
              blame.add_line($1, Revision.new(
7718
                                    :identifier => identifier,
7719
                                    :revision   => identifier,
7720
                                    :scmid      => identifier,
7721
                                    :author     => authors_by_commit[identifier]
7722
                                    ))
7723 0:513646585e45 Chris
              identifier = ''
7724
              author = ''
7725
            end
7726
          end
7727
          blame
7728 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
7729
          nil
7730 0:513646585e45 Chris
        end
7731 245:051f544170fe Chris
7732 0:513646585e45 Chris
        def cat(path, identifier=nil)
7733
          if identifier.nil?
7734
            identifier = 'HEAD'
7735
          end
7736 441:cbce1fd3b1b7 Chris
          cmd_args = %w|show --no-color|
7737
          cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
7738 0:513646585e45 Chris
          cat = nil
7739 1115:433d4f72a19b Chris
          git_cmd(cmd_args) do |io|
7740 0:513646585e45 Chris
            io.binmode
7741
            cat = io.read
7742
          end
7743
          cat
7744 441:cbce1fd3b1b7 Chris
        rescue ScmCommandAborted
7745
          nil
7746 0:513646585e45 Chris
        end
7747 117:af80e5618e9b Chris
7748
        class Revision < Redmine::Scm::Adapters::Revision
7749
          # Returns the readable identifier
7750
          def format_identifier
7751
            identifier[0,8]
7752
          end
7753
        end
7754 245:051f544170fe Chris
7755 1115:433d4f72a19b Chris
        def git_cmd(args, options = {}, &block)
7756 245:051f544170fe Chris
          repo_path = root_url || url
7757 909:cbb26bc654de Chris
          full_args = ['--git-dir', repo_path]
7758 441:cbce1fd3b1b7 Chris
          if self.class.client_version_above?([1, 7, 2])
7759
            full_args << '-c' << 'core.quotepath=false'
7760
            full_args << '-c' << 'log.decorate=no'
7761
          end
7762 245:051f544170fe Chris
          full_args += args
7763 909:cbb26bc654de Chris
          ret = shellout(
7764
                   self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
7765 1115:433d4f72a19b Chris
                   options,
7766 909:cbb26bc654de Chris
                   &block
7767
                   )
7768 245:051f544170fe Chris
          if $? && $?.exitstatus != 0
7769
            raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
7770
          end
7771
          ret
7772
        end
7773 1115:433d4f72a19b Chris
        private :git_cmd
7774 0:513646585e45 Chris
      end
7775
    end
7776
  end
7777
end
7778
changeset = 'This template must be used with --debug option\n'
7779
changeset_quiet =  'This template must be used with --debug option\n'
7780
changeset_verbose = 'This template must be used with --debug option\n'
7781
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
7782
7783 117:af80e5618e9b Chris
file = '<path action="M">{file|urlescape}</path>\n'
7784
file_add = '<path action="A">{file_add|urlescape}</path>\n'
7785
file_del = '<path action="D">{file_del|urlescape}</path>\n'
7786
file_copy = '<path-copied copyfrom-path="{source|urlescape}">{name|urlescape}</path-copied>\n'
7787 0:513646585e45 Chris
tag = '<tag>{tag|escape}</tag>\n'
7788
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
7789 3:7c48bad7d85d Chris
footer='</log>'
7790 0:513646585e45 Chris
changeset = 'This template must be used with --debug option\n'
7791
changeset_quiet =  'This template must be used with --debug option\n'
7792
changeset_verbose = 'This template must be used with --debug option\n'
7793 1517:dffacf8a6908 Chris
changeset_debug = '<logentry revision="{rev}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodatesec}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n<parents>\n{parents}</parents>\n</logentry>\n\n'
7794 0:513646585e45 Chris
7795 117:af80e5618e9b Chris
file_mod = '<path action="M">{file_mod|urlescape}</path>\n'
7796
file_add = '<path action="A">{file_add|urlescape}</path>\n'
7797
file_del = '<path action="D">{file_del|urlescape}</path>\n'
7798
file_copy = '<path-copied copyfrom-path="{source|urlescape}">{name|urlescape}</path-copied>\n'
7799 1517:dffacf8a6908 Chris
parent = '<parent>{node}</parent>\n'
7800 0:513646585e45 Chris
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
7801 3:7c48bad7d85d Chris
footer='</log>'
7802 245:051f544170fe Chris
# redminehelper: Redmine helper extension for Mercurial
7803
#
7804
# Copyright 2010 Alessio Franceschelli (alefranz.net)
7805
# Copyright 2010-2011 Yuya Nishihara <yuya@tcha.org>
7806
#
7807
# This software may be used and distributed according to the terms of the
7808
# GNU General Public License version 2 or any later version.
7809
"""helper commands for Redmine to reduce the number of hg calls
7810
7811
To test this extension, please try::
7812
7813
    $ hg --config extensions.redminehelper=redminehelper.py rhsummary
7814
7815
I/O encoding:
7816
7817
:file path: urlencoded, raw string
7818
:tag name: utf-8
7819
:branch name: utf-8
7820 1517:dffacf8a6908 Chris
:node: hex string
7821 245:051f544170fe Chris
7822
Output example of rhsummary::
7823
7824
    <?xml version="1.0"?>
7825
    <rhsummary>
7826
      <repository root="/foo/bar">
7827
        <tip revision="1234" node="abcdef0123..."/>
7828
        <tag revision="123" node="34567abc..." name="1.1.1"/>
7829
        <branch .../>
7830
        ...
7831
      </repository>
7832
    </rhsummary>
7833
7834
Output example of rhmanifest::
7835
7836
    <?xml version="1.0"?>
7837
    <rhmanifest>
7838
      <repository root="/foo/bar">
7839
        <manifest revision="1234" path="lib">
7840
          <file name="diff.rb" revision="123" node="34567abc..." time="12345"
7841
                 size="100"/>
7842
          ...
7843
          <dir name="redmine"/>
7844
          ...
7845
        </manifest>
7846
      </repository>
7847
    </rhmanifest>
7848
"""
7849
import re, time, cgi, urllib
7850 909:cbb26bc654de Chris
from mercurial import cmdutil, commands, node, error, hg
7851 245:051f544170fe Chris
7852
_x = cgi.escape
7853
_u = lambda s: cgi.escape(urllib.quote(s))
7854
7855
def _tip(ui, repo):
7856
    # see mercurial/commands.py:tip
7857
    def tiprev():
7858
        try:
7859
            return len(repo) - 1
7860
        except TypeError:  # Mercurial < 1.1
7861
            return repo.changelog.count() - 1
7862
    tipctx = repo.changectx(tiprev())
7863
    ui.write('<tip revision="%d" node="%s"/>\n'
7864 1517:dffacf8a6908 Chris
             % (tipctx.rev(), _x(node.hex(tipctx.node()))))
7865 245:051f544170fe Chris
7866
_SPECIAL_TAGS = ('tip',)
7867
7868
def _tags(ui, repo):
7869
    # see mercurial/commands.py:tags
7870
    for t, n in reversed(repo.tagslist()):
7871
        if t in _SPECIAL_TAGS:
7872
            continue
7873
        try:
7874
            r = repo.changelog.rev(n)
7875
        except error.LookupError:
7876
            continue
7877
        ui.write('<tag revision="%d" node="%s" name="%s"/>\n'
7878 1517:dffacf8a6908 Chris
                 % (r, _x(node.hex(n)), _x(t)))
7879 245:051f544170fe Chris
7880
def _branches(ui, repo):
7881
    # see mercurial/commands.py:branches
7882
    def iterbranches():
7883 1494:e248c7af89ec Chris
        if getattr(repo, 'branchtags', None) is not None:
7884
            # Mercurial < 2.9
7885
            for t, n in repo.branchtags().iteritems():
7886
                yield t, n, repo.changelog.rev(n)
7887
        else:
7888
            for tag, heads, tip, isclosed in repo.branchmap().iterbranches():
7889
                yield tag, tip, repo.changelog.rev(tip)
7890 245:051f544170fe Chris
    def branchheads(branch):
7891
        try:
7892
            return repo.branchheads(branch, closed=False)
7893
        except TypeError:  # Mercurial < 1.2
7894
            return repo.branchheads(branch)
7895
    for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True):
7896
        if repo.lookup(r) in branchheads(t):
7897
            ui.write('<branch revision="%d" node="%s" name="%s"/>\n'
7898 1517:dffacf8a6908 Chris
                     % (r, _x(node.hex(n)), _x(t)))
7899 245:051f544170fe Chris
7900
def _manifest(ui, repo, path, rev):
7901
    ctx = repo.changectx(rev)
7902
    ui.write('<manifest revision="%d" path="%s">\n'
7903
             % (ctx.rev(), _u(path)))
7904
7905
    known = set()
7906
    pathprefix = (path.rstrip('/') + '/').lstrip('/')
7907
    for f, n in sorted(ctx.manifest().iteritems(), key=lambda e: e[0]):
7908
        if not f.startswith(pathprefix):
7909
            continue
7910
        name = re.sub(r'/.*', '/', f[len(pathprefix):])
7911
        if name in known:
7912
            continue
7913
        known.add(name)
7914
7915
        if name.endswith('/'):
7916
            ui.write('<dir name="%s"/>\n'
7917
                     % _x(urllib.quote(name[:-1])))
7918
        else:
7919
            fctx = repo.filectx(f, fileid=n)
7920
            tm, tzoffset = fctx.date()
7921
            ui.write('<file name="%s" revision="%d" node="%s" '
7922
                     'time="%d" size="%d"/>\n'
7923 1517:dffacf8a6908 Chris
                     % (_u(name), fctx.rev(), _x(node.hex(fctx.node())),
7924 245:051f544170fe Chris
                        tm, fctx.size(), ))
7925
7926
    ui.write('</manifest>\n')
7927
7928
def rhannotate(ui, repo, *pats, **opts):
7929 441:cbce1fd3b1b7 Chris
    rev = urllib.unquote_plus(opts.pop('rev', None))
7930
    opts['rev'] = rev
7931 245:051f544170fe Chris
    return commands.annotate(ui, repo, *map(urllib.unquote_plus, pats), **opts)
7932
7933
def rhcat(ui, repo, file1, *pats, **opts):
7934 441:cbce1fd3b1b7 Chris
    rev = urllib.unquote_plus(opts.pop('rev', None))
7935
    opts['rev'] = rev
7936 245:051f544170fe Chris
    return commands.cat(ui, repo, urllib.unquote_plus(file1), *map(urllib.unquote_plus, pats), **opts)
7937
7938
def rhdiff(ui, repo, *pats, **opts):
7939
    """diff repository (or selected files)"""
7940
    change = opts.pop('change', None)
7941
    if change:  # add -c option for Mercurial<1.1
7942
        base = repo.changectx(change).parents()[0].rev()
7943
        opts['rev'] = [str(base), change]
7944
    opts['nodates'] = True
7945
    return commands.diff(ui, repo, *map(urllib.unquote_plus, pats), **opts)
7946
7947 441:cbce1fd3b1b7 Chris
def rhlog(ui, repo, *pats, **opts):
7948
    rev      = opts.pop('rev')
7949
    bra0     = opts.pop('branch')
7950
    from_rev = urllib.unquote_plus(opts.pop('from', None))
7951
    to_rev   = urllib.unquote_plus(opts.pop('to'  , None))
7952
    bra      = urllib.unquote_plus(opts.pop('rhbranch', None))
7953
    from_rev = from_rev.replace('"', '\\"')
7954
    to_rev   = to_rev.replace('"', '\\"')
7955 909:cbb26bc654de Chris
    if hg.util.version() >= '1.6':
7956
      opts['rev'] = ['"%s":"%s"' % (from_rev, to_rev)]
7957
    else:
7958
      opts['rev'] = ['%s:%s' % (from_rev, to_rev)]
7959 441:cbce1fd3b1b7 Chris
    opts['branch'] = [bra]
7960
    return commands.log(ui, repo, *map(urllib.unquote_plus, pats), **opts)
7961
7962 245:051f544170fe Chris
def rhmanifest(ui, repo, path='', **opts):
7963
    """output the sub-manifest of the specified directory"""
7964
    ui.write('<?xml version="1.0"?>\n')
7965
    ui.write('<rhmanifest>\n')
7966
    ui.write('<repository root="%s">\n' % _u(repo.root))
7967
    try:
7968
        _manifest(ui, repo, urllib.unquote_plus(path), urllib.unquote_plus(opts.get('rev')))
7969
    finally:
7970
        ui.write('</repository>\n')
7971
        ui.write('</rhmanifest>\n')
7972
7973
def rhsummary(ui, repo, **opts):
7974
    """output the summary of the repository"""
7975
    ui.write('<?xml version="1.0"?>\n')
7976
    ui.write('<rhsummary>\n')
7977
    ui.write('<repository root="%s">\n' % _u(repo.root))
7978
    try:
7979
        _tip(ui, repo)
7980
        _tags(ui, repo)
7981
        _branches(ui, repo)
7982
        # TODO: bookmarks in core (Mercurial>=1.8)
7983
    finally:
7984
        ui.write('</repository>\n')
7985
        ui.write('</rhsummary>\n')
7986
7987
cmdtable = {
7988
    'rhannotate': (rhannotate,
7989
         [('r', 'rev', '', 'revision'),
7990
          ('u', 'user', None, 'list the author (long with -v)'),
7991
          ('n', 'number', None, 'list the revision number (default)'),
7992
          ('c', 'changeset', None, 'list the changeset'),
7993
         ],
7994
         'hg rhannotate [-r REV] [-u] [-n] [-c] FILE...'),
7995
    'rhcat': (rhcat,
7996
               [('r', 'rev', '', 'revision')],
7997
               'hg rhcat ([-r REV] ...) FILE...'),
7998
    'rhdiff': (rhdiff,
7999
               [('r', 'rev', [], 'revision'),
8000
                ('c', 'change', '', 'change made by revision')],
8001
               'hg rhdiff ([-c REV] | [-r REV] ...) [FILE]...'),
8002 441:cbce1fd3b1b7 Chris
    'rhlog': (rhlog,
8003
                   [
8004
                    ('r', 'rev', [], 'show the specified revision'),
8005
                    ('b', 'branch', [],
8006 909:cbb26bc654de Chris
                       'show changesets within the given named branch'),
8007 441:cbce1fd3b1b7 Chris
                    ('l', 'limit', '',
8008 909:cbb26bc654de Chris
                         'limit number of changes displayed'),
8009 441:cbce1fd3b1b7 Chris
                    ('d', 'date', '',
8010 909:cbb26bc654de Chris
                         'show revisions matching date spec'),
8011 441:cbce1fd3b1b7 Chris
                    ('u', 'user', [],
8012 909:cbb26bc654de Chris
                      'revisions committed by user'),
8013 441:cbce1fd3b1b7 Chris
                    ('', 'from', '',
8014 909:cbb26bc654de Chris
                      ''),
8015 441:cbce1fd3b1b7 Chris
                    ('', 'to', '',
8016 909:cbb26bc654de Chris
                      ''),
8017 441:cbce1fd3b1b7 Chris
                    ('', 'rhbranch', '',
8018 909:cbb26bc654de Chris
                      ''),
8019 441:cbce1fd3b1b7 Chris
                    ('', 'template', '',
8020 909:cbb26bc654de Chris
                       'display with template')],
8021 441:cbce1fd3b1b7 Chris
                   'hg rhlog [OPTION]... [FILE]'),
8022 245:051f544170fe Chris
    'rhmanifest': (rhmanifest,
8023
                   [('r', 'rev', '', 'show the specified revision')],
8024
                   'hg rhmanifest [-r REV] [PATH]'),
8025
    'rhsummary': (rhsummary, [], 'hg rhsummary'),
8026
}
8027 441:cbce1fd3b1b7 Chris
# Redmine - project management software
8028 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
8029 0:513646585e45 Chris
#
8030
# This program is free software; you can redistribute it and/or
8031
# modify it under the terms of the GNU General Public License
8032
# as published by the Free Software Foundation; either version 2
8033
# of the License, or (at your option) any later version.
8034 441:cbce1fd3b1b7 Chris
#
8035 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
8036
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8037
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8038
# GNU General Public License for more details.
8039 441:cbce1fd3b1b7 Chris
#
8040 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
8041
# along with this program; if not, write to the Free Software
8042
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
8043
8044 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
8045 119:8661b858af72 Chris
require 'cgi'
8046 0:513646585e45 Chris
8047
module Redmine
8048
  module Scm
8049 245:051f544170fe Chris
    module Adapters
8050 0:513646585e45 Chris
      class MercurialAdapter < AbstractAdapter
8051 119:8661b858af72 Chris
8052 0:513646585e45 Chris
        # Mercurial executable name
8053 210:0579821a129a Chris
        HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
8054 245:051f544170fe Chris
        HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
8055
        HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
8056 0:513646585e45 Chris
        TEMPLATE_NAME = "hg-template"
8057
        TEMPLATE_EXTENSION = "tmpl"
8058 119:8661b858af72 Chris
8059 245:051f544170fe Chris
        # raised if hg command exited with error, e.g. unknown revision.
8060
        class HgCommandAborted < CommandFailed; end
8061
8062 0:513646585e45 Chris
        class << self
8063 245:051f544170fe Chris
          def client_command
8064
            @@bin    ||= HG_BIN
8065
          end
8066
8067
          def sq_bin
8068 909:cbb26bc654de Chris
            @@sq_bin ||= shell_quote_command
8069 245:051f544170fe Chris
          end
8070
8071 0:513646585e45 Chris
          def client_version
8072
            @@client_version ||= (hgversion || [])
8073
          end
8074 119:8661b858af72 Chris
8075 245:051f544170fe Chris
          def client_available
8076 909:cbb26bc654de Chris
            client_version_above?([1, 2])
8077 245:051f544170fe Chris
          end
8078
8079
          def hgversion
8080 0:513646585e45 Chris
            # The hg version is expressed either as a
8081
            # release number (eg 0.9.5 or 1.0) or as a revision
8082
            # id composed of 12 hexa characters.
8083 245:051f544170fe Chris
            theversion = hgversion_from_command_line.dup
8084
            if theversion.respond_to?(:force_encoding)
8085
              theversion.force_encoding('ASCII-8BIT')
8086
            end
8087 119:8661b858af72 Chris
            if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
8088
              m[2].scan(%r{\d+}).collect(&:to_i)
8089 0:513646585e45 Chris
            end
8090
          end
8091 119:8661b858af72 Chris
8092 0:513646585e45 Chris
          def hgversion_from_command_line
8093 245:051f544170fe Chris
            shellout("#{sq_bin} --version") { |io| io.read }.to_s
8094 0:513646585e45 Chris
          end
8095 119:8661b858af72 Chris
8096 0:513646585e45 Chris
          def template_path
8097
            @@template_path ||= template_path_for(client_version)
8098
          end
8099 119:8661b858af72 Chris
8100 0:513646585e45 Chris
          def template_path_for(version)
8101 909:cbb26bc654de Chris
            "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
8102 0:513646585e45 Chris
          end
8103
        end
8104 119:8661b858af72 Chris
8105 245:051f544170fe Chris
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
8106
          super
8107 441:cbce1fd3b1b7 Chris
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
8108
        end
8109
8110
        def path_encoding
8111
          @path_encoding
8112 0:513646585e45 Chris
        end
8113 119:8661b858af72 Chris
8114 245:051f544170fe Chris
        def info
8115
          tip = summary['repository']['tip']
8116
          Info.new(:root_url => CGI.unescape(summary['repository']['root']),
8117
                   :lastrev => Revision.new(:revision => tip['revision'],
8118
                                            :scmid => tip['node']))
8119 507:0c939c159af4 Chris
        # rescue HgCommandAborted
8120
        rescue Exception => e
8121
          logger.error "hg: error during getting info: #{e.message}"
8122
          nil
8123 245:051f544170fe Chris
        end
8124
8125
        def tags
8126
          as_ary(summary['repository']['tag']).map { |e| e['name'] }
8127
        end
8128
8129
        # Returns map of {'tag' => 'nodeid', ...}
8130
        def tagmap
8131
          alist = as_ary(summary['repository']['tag']).map do |e|
8132
            e.values_at('name', 'node')
8133
          end
8134
          Hash[*alist.flatten]
8135
        end
8136
8137
        def branches
8138 909:cbb26bc654de Chris
          brs = []
8139
          as_ary(summary['repository']['branch']).each do |e|
8140
            br = Branch.new(e['name'])
8141
            br.revision =  e['revision']
8142
            br.scmid    =  e['node']
8143
            brs << br
8144
          end
8145
          brs
8146 245:051f544170fe Chris
        end
8147
8148
        # Returns map of {'branch' => 'nodeid', ...}
8149
        def branchmap
8150
          alist = as_ary(summary['repository']['branch']).map do |e|
8151
            e.values_at('name', 'node')
8152
          end
8153
          Hash[*alist.flatten]
8154
        end
8155
8156
        def summary
8157 441:cbce1fd3b1b7 Chris
          return @summary if @summary
8158 245:051f544170fe Chris
          hg 'rhsummary' do |io|
8159
            output = io.read
8160
            if output.respond_to?(:force_encoding)
8161
              output.force_encoding('UTF-8')
8162
            end
8163
            begin
8164 1115:433d4f72a19b Chris
              @summary = parse_xml(output)['rhsummary']
8165 245:051f544170fe Chris
            rescue
8166 0:513646585e45 Chris
            end
8167
          end
8168 245:051f544170fe Chris
        end
8169
        private :summary
8170
8171 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
8172 245:051f544170fe Chris
          p1 = scm_iconv(@path_encoding, 'UTF-8', path)
8173
          manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
8174
                        CGI.escape(without_leading_slash(p1.to_s))) do |io|
8175
            output = io.read
8176
            if output.respond_to?(:force_encoding)
8177
              output.force_encoding('UTF-8')
8178
            end
8179
            begin
8180 1115:433d4f72a19b Chris
              parse_xml(output)['rhmanifest']['repository']['manifest']
8181 245:051f544170fe Chris
            rescue
8182
            end
8183
          end
8184
          path_prefix = path.blank? ? '' : with_trailling_slash(path)
8185
8186
          entries = Entries.new
8187
          as_ary(manifest['dir']).each do |e|
8188
            n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
8189
            p = "#{path_prefix}#{n}"
8190
            entries << Entry.new(:name => n, :path => p, :kind => 'dir')
8191
          end
8192
8193
          as_ary(manifest['file']).each do |e|
8194
            n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
8195
            p = "#{path_prefix}#{n}"
8196
            lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
8197
                              :identifier => e['node'],
8198
                              :time => Time.at(e['time'].to_i))
8199
            entries << Entry.new(:name => n, :path => p, :kind => 'file',
8200
                                 :size => e['size'].to_i, :lastrev => lr)
8201
          end
8202
8203
          entries
8204
        rescue HgCommandAborted
8205
          nil  # means not found
8206 0:513646585e45 Chris
        end
8207 119:8661b858af72 Chris
8208 245:051f544170fe Chris
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
8209
          revs = Revisions.new
8210
          each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
8211
          revs
8212
        end
8213
8214
        # Iterates the revisions by using a template file that
8215 0:513646585e45 Chris
        # makes Mercurial produce a xml output.
8216 245:051f544170fe Chris
        def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
8217
          hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
8218
          hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
8219
          hg_args << '--limit' << options[:limit] if options[:limit]
8220
          hg_args << hgtarget(path) unless path.blank?
8221
          log = hg(*hg_args) do |io|
8222
            output = io.read
8223
            if output.respond_to?(:force_encoding)
8224
              output.force_encoding('UTF-8')
8225
            end
8226 0:513646585e45 Chris
            begin
8227 1116:bb32da3bea34 Chris
              parse_xml("#{output}")['log']
8228 0:513646585e45 Chris
            rescue
8229
            end
8230
          end
8231 245:051f544170fe Chris
          as_ary(log['logentry']).each do |le|
8232
            cpalist = as_ary(le['paths']['path-copied']).map do |e|
8233 441:cbce1fd3b1b7 Chris
              [e['__content__'], e['copyfrom-path']].map do |s|
8234
                scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
8235
              end
8236 245:051f544170fe Chris
            end
8237
            cpmap = Hash[*cpalist.flatten]
8238
            paths = as_ary(le['paths']['path']).map do |e|
8239
              p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
8240 441:cbce1fd3b1b7 Chris
              {:action        => e['action'],
8241
               :path          => with_leading_slash(p),
8242
               :from_path     => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
8243
               :from_revision => (cpmap.member?(p) ? le['node'] : nil)}
8244 245:051f544170fe Chris
            end.sort { |a, b| a[:path] <=> b[:path] }
8245 909:cbb26bc654de Chris
            parents_ary = []
8246
            as_ary(le['parents']['parent']).map do |par|
8247 1517:dffacf8a6908 Chris
              parents_ary << par['__content__'] if par['__content__'] != "0000000000000000000000000000000000000000"
8248 909:cbb26bc654de Chris
            end
8249 245:051f544170fe Chris
            yield Revision.new(:revision => le['revision'],
8250 441:cbce1fd3b1b7 Chris
                               :scmid    => le['node'],
8251
                               :author   => (le['author']['__content__'] rescue ''),
8252
                               :time     => Time.parse(le['date']['__content__']),
8253
                               :message  => le['msg']['__content__'],
8254 909:cbb26bc654de Chris
                               :paths    => paths,
8255
                               :parents  => parents_ary)
8256 245:051f544170fe Chris
          end
8257
          self
8258 0:513646585e45 Chris
        end
8259 119:8661b858af72 Chris
8260 441:cbce1fd3b1b7 Chris
        # Returns list of nodes in the specified branch
8261
        def nodes_in_branch(branch, options={})
8262 1517:dffacf8a6908 Chris
          hg_args = ['rhlog', '--template', '{node}\n', '--rhbranch', CGI.escape(branch)]
8263 441:cbce1fd3b1b7 Chris
          hg_args << '--from' << CGI.escape(branch)
8264
          hg_args << '--to'   << '0'
8265
          hg_args << '--limit' << options[:limit] if options[:limit]
8266
          hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
8267
        end
8268
8269 0:513646585e45 Chris
        def diff(path, identifier_from, identifier_to=nil)
8270 245:051f544170fe Chris
          hg_args = %w|rhdiff|
8271
          if identifier_to
8272
            hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
8273
          else
8274
            hg_args << '-c' << hgrev(identifier_from)
8275
          end
8276
          unless path.blank?
8277
            p = scm_iconv(@path_encoding, 'UTF-8', path)
8278
            hg_args << CGI.escape(hgtarget(p))
8279
          end
8280 119:8661b858af72 Chris
          diff = []
8281 245:051f544170fe Chris
          hg *hg_args do |io|
8282 0:513646585e45 Chris
            io.each_line do |line|
8283
              diff << line
8284
            end
8285
          end
8286
          diff
8287 245:051f544170fe Chris
        rescue HgCommandAborted
8288
          nil  # means not found
8289 0:513646585e45 Chris
        end
8290 119:8661b858af72 Chris
8291 0:513646585e45 Chris
        def cat(path, identifier=nil)
8292 245:051f544170fe Chris
          p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
8293 441:cbce1fd3b1b7 Chris
          hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
8294 0:513646585e45 Chris
            io.binmode
8295 245:051f544170fe Chris
            io.read
8296 0:513646585e45 Chris
          end
8297 245:051f544170fe Chris
        rescue HgCommandAborted
8298
          nil  # means not found
8299 0:513646585e45 Chris
        end
8300 119:8661b858af72 Chris
8301 0:513646585e45 Chris
        def annotate(path, identifier=nil)
8302 245:051f544170fe Chris
          p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
8303 0:513646585e45 Chris
          blame = Annotate.new
8304 441:cbce1fd3b1b7 Chris
          hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
8305 0:513646585e45 Chris
            io.each_line do |line|
8306 245:051f544170fe Chris
              line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
8307 119:8661b858af72 Chris
              next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
8308
              r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
8309
                               :identifier => $3)
8310
              blame.add_line($4.rstrip, r)
8311 0:513646585e45 Chris
            end
8312
          end
8313
          blame
8314 245:051f544170fe Chris
        rescue HgCommandAborted
8315 507:0c939c159af4 Chris
          # means not found or cannot be annotated
8316
          Annotate.new
8317 0:513646585e45 Chris
        end
8318 119:8661b858af72 Chris
8319
        class Revision < Redmine::Scm::Adapters::Revision
8320
          # Returns the readable identifier
8321
          def format_identifier
8322
            "#{revision}:#{scmid}"
8323
          end
8324
        end
8325
8326 245:051f544170fe Chris
        # Runs 'hg' command with the given args
8327
        def hg(*args, &block)
8328
          repo_path = root_url || url
8329 909:cbb26bc654de Chris
          full_args = ['-R', repo_path, '--encoding', 'utf-8']
8330 245:051f544170fe Chris
          full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
8331
          full_args << '--config' << 'diff.git=false'
8332
          full_args += args
8333 909:cbb26bc654de Chris
          ret = shellout(
8334
                   self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
8335
                   &block
8336
                   )
8337 245:051f544170fe Chris
          if $? && $?.exitstatus != 0
8338
            raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
8339
          end
8340
          ret
8341
        end
8342
        private :hg
8343
8344 119:8661b858af72 Chris
        # Returns correct revision identifier
8345 245:051f544170fe Chris
        def hgrev(identifier, sq=false)
8346
          rev = identifier.blank? ? 'tip' : identifier.to_s
8347
          rev = shell_quote(rev) if sq
8348
          rev
8349 119:8661b858af72 Chris
        end
8350
        private :hgrev
8351 245:051f544170fe Chris
8352
        def hgtarget(path)
8353
          path ||= ''
8354
          root_url + '/' + without_leading_slash(path)
8355
        end
8356
        private :hgtarget
8357
8358
        def as_ary(o)
8359
          return [] unless o
8360
          o.is_a?(Array) ? o : Array[o]
8361
        end
8362
        private :as_ary
8363 0:513646585e45 Chris
      end
8364
    end
8365
  end
8366
end
8367 441:cbce1fd3b1b7 Chris
# Redmine - project management software
8368 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
8369 441:cbce1fd3b1b7 Chris
#
8370
# This program is free software; you can redistribute it and/or
8371
# modify it under the terms of the GNU General Public License
8372
# as published by the Free Software Foundation; either version 2
8373
# of the License, or (at your option) any later version.
8374
#
8375
# This program is distributed in the hope that it will be useful,
8376
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8377
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8378
# GNU General Public License for more details.
8379
#
8380
# You should have received a copy of the GNU General Public License
8381
# along with this program; if not, write to the Free Software
8382
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
8383
8384 1136:51d7f3e06556 chris
require_dependency 'redmine/scm/adapters/abstract_adapter'
8385 441:cbce1fd3b1b7 Chris
require 'uri'
8386
8387
module Redmine
8388
  module Scm
8389
    module Adapters
8390
      class SubversionAdapter < AbstractAdapter
8391
8392
        # SVN executable name
8393
        SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
8394
8395
        class << self
8396
          def client_command
8397
            @@bin    ||= SVN_BIN
8398
          end
8399
8400
          def sq_bin
8401 909:cbb26bc654de Chris
            @@sq_bin ||= shell_quote_command
8402 441:cbce1fd3b1b7 Chris
          end
8403
8404
          def client_version
8405
            @@client_version ||= (svn_binary_version || [])
8406
          end
8407
8408
          def client_available
8409
            # --xml options are introduced in 1.3.
8410
            # http://subversion.apache.org/docs/release-notes/1.3.html
8411
            client_version_above?([1, 3])
8412
          end
8413
8414
          def svn_binary_version
8415
            scm_version = scm_version_from_command_line.dup
8416
            if scm_version.respond_to?(:force_encoding)
8417
              scm_version.force_encoding('ASCII-8BIT')
8418
            end
8419
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
8420
              m[2].scan(%r{\d+}).collect(&:to_i)
8421
            end
8422
          end
8423
8424
          def scm_version_from_command_line
8425
            shellout("#{sq_bin} --version") { |io| io.read }.to_s
8426
          end
8427
        end
8428
8429
        # Get info about the svn repository
8430
        def info
8431
          cmd = "#{self.class.sq_bin} info --xml #{target}"
8432
          cmd << credentials_string
8433
          info = nil
8434
          shellout(cmd) do |io|
8435
            output = io.read
8436
            if output.respond_to?(:force_encoding)
8437
              output.force_encoding('UTF-8')
8438
            end
8439
            begin
8440 1115:433d4f72a19b Chris
              doc = parse_xml(output)
8441 441:cbce1fd3b1b7 Chris
              # root_url = doc.elements["info/entry/repository/root"].text
8442
              info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
8443
                               :lastrev => Revision.new({
8444
                                 :identifier => doc['info']['entry']['commit']['revision'],
8445
                                 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
8446
                                 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
8447
                               })
8448
                             })
8449
            rescue
8450
            end
8451
          end
8452
          return nil if $? && $?.exitstatus != 0
8453
          info
8454
        rescue CommandFailed
8455
          return nil
8456
        end
8457
8458
        # Returns an Entries collection
8459
        # or nil if the given path doesn't exist in the repository
8460
        def entries(path=nil, identifier=nil, options={})
8461
          path ||= ''
8462
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
8463
          entries = Entries.new
8464
          cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
8465
          cmd << credentials_string
8466
          shellout(cmd) do |io|
8467
            output = io.read
8468
            if output.respond_to?(:force_encoding)
8469
              output.force_encoding('UTF-8')
8470
            end
8471
            begin
8472 1115:433d4f72a19b Chris
              doc = parse_xml(output)
8473 441:cbce1fd3b1b7 Chris
              each_xml_element(doc['lists']['list'], 'entry') do |entry|
8474
                commit = entry['commit']
8475
                commit_date = commit['date']
8476
                # Skip directory if there is no commit date (usually that
8477
                # means that we don't have read access to it)
8478
                next if entry['kind'] == 'dir' && commit_date.nil?
8479
                name = entry['name']['__content__']
8480
                entries << Entry.new({:name => URI.unescape(name),
8481
                            :path => ((path.empty? ? "" : "#{path}/") + name),
8482
                            :kind => entry['kind'],
8483
                            :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
8484
                            :lastrev => Revision.new({
8485
                              :identifier => commit['revision'],
8486
                              :time => Time.parse(commit_date['__content__'].to_s).localtime,
8487
                              :author => ((a = commit['author']) ? a['__content__'] : nil)
8488
                              })
8489
                            })
8490
              end
8491
            rescue Exception => e
8492
              logger.error("Error parsing svn output: #{e.message}")
8493
              logger.error("Output was:\n #{output}")
8494
            end
8495
          end
8496
          return nil if $? && $?.exitstatus != 0
8497
          logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
8498
          entries.sort_by_name
8499
        end
8500
8501
        def properties(path, identifier=nil)
8502
          # proplist xml output supported in svn 1.5.0 and higher
8503
          return nil unless self.class.client_version_above?([1, 5, 0])
8504
8505
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
8506
          cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
8507
          cmd << credentials_string
8508
          properties = {}
8509
          shellout(cmd) do |io|
8510
            output = io.read
8511
            if output.respond_to?(:force_encoding)
8512
              output.force_encoding('UTF-8')
8513
            end
8514
            begin
8515 1115:433d4f72a19b Chris
              doc = parse_xml(output)
8516 441:cbce1fd3b1b7 Chris
              each_xml_element(doc['properties']['target'], 'property') do |property|
8517
                properties[ property['name'] ] = property['__content__'].to_s
8518
              end
8519
            rescue
8520
            end
8521
          end
8522
          return nil if $? && $?.exitstatus != 0
8523
          properties
8524
        end
8525
8526
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
8527
          path ||= ''
8528
          identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
8529
          identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
8530
          revisions = Revisions.new
8531
          cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
8532
          cmd << credentials_string
8533
          cmd << " --verbose " if  options[:with_paths]
8534
          cmd << " --limit #{options[:limit].to_i}" if options[:limit]
8535
          cmd << ' ' + target(path)
8536
          shellout(cmd) do |io|
8537
            output = io.read
8538
            if output.respond_to?(:force_encoding)
8539
              output.force_encoding('UTF-8')
8540
            end
8541
            begin
8542 1115:433d4f72a19b Chris
              doc = parse_xml(output)
8543 441:cbce1fd3b1b7 Chris
              each_xml_element(doc['log'], 'logentry') do |logentry|
8544
                paths = []
8545
                each_xml_element(logentry['paths'], 'path') do |path|
8546
                  paths << {:action => path['action'],
8547
                            :path => path['__content__'],
8548
                            :from_path => path['copyfrom-path'],
8549
                            :from_revision => path['copyfrom-rev']
8550
                            }
8551
                end if logentry['paths'] && logentry['paths']['path']
8552
                paths.sort! { |x,y| x[:path] <=> y[:path] }
8553
8554
                revisions << Revision.new({:identifier => logentry['revision'],
8555
                              :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
8556
                              :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
8557
                              :message => logentry['msg']['__content__'],
8558
                              :paths => paths
8559
                            })
8560
              end
8561
            rescue
8562
            end
8563
          end
8564
          return nil if $? && $?.exitstatus != 0
8565
          revisions
8566
        end
8567
8568 1115:433d4f72a19b Chris
        def diff(path, identifier_from, identifier_to=nil)
8569 441:cbce1fd3b1b7 Chris
          path ||= ''
8570
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
8571
8572
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
8573
8574
          cmd = "#{self.class.sq_bin} diff -r "
8575
          cmd << "#{identifier_to}:"
8576
          cmd << "#{identifier_from}"
8577
          cmd << " #{target(path)}@#{identifier_from}"
8578
          cmd << credentials_string
8579
          diff = []
8580
          shellout(cmd) do |io|
8581
            io.each_line do |line|
8582
              diff << line
8583
            end
8584
          end
8585
          return nil if $? && $?.exitstatus != 0
8586
          diff
8587
        end
8588
8589
        def cat(path, identifier=nil)
8590
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
8591
          cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
8592
          cmd << credentials_string
8593
          cat = nil
8594
          shellout(cmd) do |io|
8595
            io.binmode
8596
            cat = io.read
8597
          end
8598
          return nil if $? && $?.exitstatus != 0
8599
          cat
8600
        end
8601
8602
        def annotate(path, identifier=nil)
8603
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
8604
          cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
8605
          cmd << credentials_string
8606
          blame = Annotate.new
8607
          shellout(cmd) do |io|
8608
            io.each_line do |line|
8609
              next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
8610
              rev = $1
8611
              blame.add_line($3.rstrip,
8612
                   Revision.new(
8613
                      :identifier => rev,
8614
                      :revision   => rev,
8615
                      :author     => $2.strip
8616
                      ))
8617
            end
8618
          end
8619
          return nil if $? && $?.exitstatus != 0
8620
          blame
8621
        end
8622
8623
        private
8624
8625
        def credentials_string
8626
          str = ''
8627
          str << " --username #{shell_quote(@login)}" unless @login.blank?
8628
          str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
8629
          str << " --no-auth-cache --non-interactive"
8630
          str
8631
        end
8632
8633
        # Helper that iterates over the child elements of a xml node
8634
        # MiniXml returns a hash when a single child is found
8635
        # or an array of hashes for multiple children
8636
        def each_xml_element(node, name)
8637
          if node && node[name]
8638
            if node[name].is_a?(Hash)
8639
              yield node[name]
8640
            else
8641
              node[name].each do |element|
8642
                yield element
8643
              end
8644
            end
8645
          end
8646
        end
8647
8648
        def target(path = '')
8649
          base = path.match(/^\//) ? root_url : url
8650
          uri = "#{base}/#{path}"
8651
          uri = URI.escape(URI.escape(uri), '[]')
8652
          shell_quote(uri.gsub(/[?<>\*]/, ''))
8653
        end
8654
      end
8655
    end
8656
  end
8657
end
8658 0:513646585e45 Chris
module Redmine
8659
  module Scm
8660
    class Base
8661
      class << self
8662
8663
        def all
8664 1464:261b3d9a4903 Chris
          @scms || []
8665 0:513646585e45 Chris
        end
8666
8667
        # Add a new SCM adapter and repository
8668
        def add(scm_name)
8669
          @scms ||= []
8670
          @scms << scm_name
8671
        end
8672
8673
        # Remove a SCM adapter from Redmine's list of supported scms
8674
        def delete(scm_name)
8675
          @scms.delete(scm_name)
8676
        end
8677
      end
8678
    end
8679
  end
8680
end
8681
# Redmine - project management software
8682 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
8683 0:513646585e45 Chris
#
8684
# This program is free software; you can redistribute it and/or
8685
# modify it under the terms of the GNU General Public License
8686
# as published by the Free Software Foundation; either version 2
8687
# of the License, or (at your option) any later version.
8688 909:cbb26bc654de Chris
#
8689 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
8690
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8691
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8692
# GNU General Public License for more details.
8693 909:cbb26bc654de Chris
#
8694 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
8695
# along with this program; if not, write to the Free Software
8696
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
8697
8698
module Redmine
8699
  module Search
8700 909:cbb26bc654de Chris
8701 0:513646585e45 Chris
    mattr_accessor :available_search_types
8702 909:cbb26bc654de Chris
8703 0:513646585e45 Chris
    @@available_search_types = []
8704
8705
    class << self
8706
      def map(&block)
8707
        yield self
8708
      end
8709 909:cbb26bc654de Chris
8710 0:513646585e45 Chris
      # Registers a search provider
8711
      def register(search_type, options={})
8712
        search_type = search_type.to_s
8713
        @@available_search_types << search_type unless @@available_search_types.include?(search_type)
8714
      end
8715
    end
8716 909:cbb26bc654de Chris
8717 0:513646585e45 Chris
    module Controller
8718
      def self.included(base)
8719
        base.extend(ClassMethods)
8720
      end
8721
8722
      module ClassMethods
8723
        @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
8724
        mattr_accessor :default_search_scopes
8725 909:cbb26bc654de Chris
8726 0:513646585e45 Chris
        # Set the default search scope for a controller or specific actions
8727
        # Examples:
8728
        #   * search_scope :issues # => sets the search scope to :issues for the whole controller
8729
        #   * search_scope :issues, :only => :index
8730
        #   * search_scope :issues, :only => [:index, :show]
8731
        def default_search_scope(id, options = {})
8732
          if actions = options[:only]
8733
            actions = [] << actions unless actions.is_a?(Array)
8734
            actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
8735
          else
8736
            default_search_scopes[controller_name.to_sym][:default] = id.to_s
8737
          end
8738
        end
8739
      end
8740
8741
      def default_search_scopes
8742
        self.class.default_search_scopes
8743
      end
8744
8745
      # Returns the default search scope according to the current action
8746
      def default_search_scope
8747
        @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||
8748
                                  default_search_scopes[controller_name.to_sym][:default]
8749
      end
8750
    end
8751
  end
8752
end
8753 1115:433d4f72a19b Chris
# Redmine - project management software
8754 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
8755 1115:433d4f72a19b Chris
#
8756
# This program is free software; you can redistribute it and/or
8757
# modify it under the terms of the GNU General Public License
8758
# as published by the Free Software Foundation; either version 2
8759
# of the License, or (at your option) any later version.
8760
#
8761
# This program is distributed in the hope that it will be useful,
8762
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8763
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8764
# GNU General Public License for more details.
8765
#
8766
# You should have received a copy of the GNU General Public License
8767
# along with this program; if not, write to the Free Software
8768
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
8769
8770
module Redmine
8771
  module SubclassFactory
8772
    def self.included(base)
8773
      base.extend ClassMethods
8774
    end
8775
8776
    module ClassMethods
8777
      def get_subclass(class_name)
8778
        klass = nil
8779
        begin
8780
          klass = class_name.to_s.classify.constantize
8781
        rescue
8782
          # invalid class name
8783
        end
8784
        unless subclasses.include? klass
8785
          klass = nil
8786
        end
8787
        klass
8788
      end
8789
8790
      # Returns an instance of the given subclass name
8791
      def new_subclass_instance(class_name, *args)
8792
        klass = get_subclass(class_name)
8793
        if klass
8794
          klass.new(*args)
8795
        end
8796
      end
8797
    end
8798
  end
8799
end
8800 0:513646585e45 Chris
# Redmine - project management software
8801 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
8802 0:513646585e45 Chris
#
8803
# This program is free software; you can redistribute it and/or
8804
# modify it under the terms of the GNU General Public License
8805
# as published by the Free Software Foundation; either version 2
8806
# of the License, or (at your option) any later version.
8807 909:cbb26bc654de Chris
#
8808 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
8809
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8810
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8811
# GNU General Public License for more details.
8812 909:cbb26bc654de Chris
#
8813 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
8814
# along with this program; if not, write to the Free Software
8815
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
8816
8817
module Redmine
8818
  module SyntaxHighlighting
8819 909:cbb26bc654de Chris
8820 0:513646585e45 Chris
    class << self
8821
      attr_reader :highlighter
8822 909:cbb26bc654de Chris
8823 0:513646585e45 Chris
      def highlighter=(name)
8824
        if name.is_a?(Module)
8825
          @highlighter = name
8826
        else
8827
          @highlighter = const_get(name)
8828
        end
8829
      end
8830 1517:dffacf8a6908 Chris
8831
      def highlight_by_filename(text, filename)
8832
        highlighter.highlight_by_filename(text, filename)
8833
      rescue
8834
        ERB::Util.h(text)
8835
      end
8836
8837
      def highlight_by_language(text, language)
8838
        highlighter.highlight_by_language(text, language)
8839
      rescue
8840
        ERB::Util.h(text)
8841
      end
8842 0:513646585e45 Chris
    end
8843 909:cbb26bc654de Chris
8844 0:513646585e45 Chris
    module CodeRay
8845
      require 'coderay'
8846 909:cbb26bc654de Chris
8847 0:513646585e45 Chris
      class << self
8848
        # Highlights +text+ as the content of +filename+
8849
        # Should not return line numbers nor outer pre tag
8850
        def highlight_by_filename(text, filename)
8851
          language = ::CodeRay::FileType[filename]
8852 1115:433d4f72a19b Chris
          language ? ::CodeRay.scan(text, language).html(:break_lines => true) : ERB::Util.h(text)
8853 0:513646585e45 Chris
        end
8854 909:cbb26bc654de Chris
8855 0:513646585e45 Chris
        # Highlights +text+ using +language+ syntax
8856
        # Should not return outer pre tag
8857
        def highlight_by_language(text, language)
8858 1115:433d4f72a19b Chris
          ::CodeRay.scan(text, language).html(:wrap => :span)
8859 0:513646585e45 Chris
        end
8860
      end
8861
    end
8862
  end
8863 909:cbb26bc654de Chris
8864 0:513646585e45 Chris
  SyntaxHighlighting.highlighter = 'CodeRay'
8865
end
8866 909:cbb26bc654de Chris
# Redmine - project management software
8867 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
8868 0:513646585e45 Chris
#
8869
# This program is free software; you can redistribute it and/or
8870
# modify it under the terms of the GNU General Public License
8871
# as published by the Free Software Foundation; either version 2
8872
# of the License, or (at your option) any later version.
8873 909:cbb26bc654de Chris
#
8874 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
8875
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8876
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8877
# GNU General Public License for more details.
8878 909:cbb26bc654de Chris
#
8879 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
8880
# along with this program; if not, write to the Free Software
8881
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
8882
8883
module Redmine
8884
  module Themes
8885 909:cbb26bc654de Chris
8886 0:513646585e45 Chris
    # Return an array of installed themes
8887
    def self.themes
8888
      @@installed_themes ||= scan_themes
8889
    end
8890 909:cbb26bc654de Chris
8891 0:513646585e45 Chris
    # Rescan themes directory
8892
    def self.rescan
8893
      @@installed_themes = scan_themes
8894
    end
8895 909:cbb26bc654de Chris
8896 0:513646585e45 Chris
    # Return theme for given id, or nil if it's not found
8897 119:8661b858af72 Chris
    def self.theme(id, options={})
8898
      return nil if id.blank?
8899 909:cbb26bc654de Chris
8900 119:8661b858af72 Chris
      found = themes.find {|t| t.id == id}
8901
      if found.nil? && options[:rescan] != false
8902
        rescan
8903
        found = theme(id, :rescan => false)
8904
      end
8905
      found
8906 0:513646585e45 Chris
    end
8907 909:cbb26bc654de Chris
8908 0:513646585e45 Chris
    # Class used to represent a theme
8909
    class Theme
8910 119:8661b858af72 Chris
      attr_reader :path, :name, :dir
8911 909:cbb26bc654de Chris
8912 0:513646585e45 Chris
      def initialize(path)
8913 119:8661b858af72 Chris
        @path = path
8914 0:513646585e45 Chris
        @dir = File.basename(path)
8915
        @name = @dir.humanize
8916 119:8661b858af72 Chris
        @stylesheets = nil
8917
        @javascripts = nil
8918 0:513646585e45 Chris
      end
8919 909:cbb26bc654de Chris
8920 0:513646585e45 Chris
      # Directory name used as the theme id
8921
      def id; dir end
8922 909:cbb26bc654de Chris
8923 119:8661b858af72 Chris
      def ==(theme)
8924
        theme.is_a?(Theme) && theme.dir == dir
8925
      end
8926 909:cbb26bc654de Chris
8927 0:513646585e45 Chris
      def <=>(theme)
8928
        name <=> theme.name
8929
      end
8930 909:cbb26bc654de Chris
8931 119:8661b858af72 Chris
      def stylesheets
8932
        @stylesheets ||= assets("stylesheets", "css")
8933
      end
8934 909:cbb26bc654de Chris
8935 1115:433d4f72a19b Chris
      def images
8936
        @images ||= assets("images")
8937
      end
8938
8939 119:8661b858af72 Chris
      def javascripts
8940
        @javascripts ||= assets("javascripts", "js")
8941
      end
8942 909:cbb26bc654de Chris
8943 1517:dffacf8a6908 Chris
      def favicons
8944
        @favicons ||= assets("favicon")
8945
      end
8946
8947
      def favicon
8948
        favicons.first
8949
      end
8950
8951
      def favicon?
8952
        favicon.present?
8953
      end
8954
8955 119:8661b858af72 Chris
      def stylesheet_path(source)
8956
        "/themes/#{dir}/stylesheets/#{source}"
8957
      end
8958 909:cbb26bc654de Chris
8959 1115:433d4f72a19b Chris
      def image_path(source)
8960
        "/themes/#{dir}/images/#{source}"
8961
      end
8962
8963 119:8661b858af72 Chris
      def javascript_path(source)
8964
        "/themes/#{dir}/javascripts/#{source}"
8965
      end
8966 909:cbb26bc654de Chris
8967 1517:dffacf8a6908 Chris
      def favicon_path
8968
        "/themes/#{dir}/favicon/#{favicon}"
8969
      end
8970
8971 119:8661b858af72 Chris
      private
8972 909:cbb26bc654de Chris
8973 1115:433d4f72a19b Chris
      def assets(dir, ext=nil)
8974
        if ext
8975
          Dir.glob("#{path}/#{dir}/*.#{ext}").collect {|f| File.basename(f).gsub(/\.#{ext}$/, '')}
8976
        else
8977
          Dir.glob("#{path}/#{dir}/*").collect {|f| File.basename(f)}
8978
        end
8979 119:8661b858af72 Chris
      end
8980 0:513646585e45 Chris
    end
8981 909:cbb26bc654de Chris
8982 0:513646585e45 Chris
    private
8983 909:cbb26bc654de Chris
8984 0:513646585e45 Chris
    def self.scan_themes
8985
      dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f|
8986
        # A theme should at least override application.css
8987
        File.directory?(f) && File.exist?("#{f}/stylesheets/application.css")
8988
      end
8989
      dirs.collect {|dir| Theme.new(dir)}.sort
8990
    end
8991
  end
8992
end
8993
8994
module ApplicationHelper
8995 119:8661b858af72 Chris
  def current_theme
8996
    unless instance_variable_defined?(:@current_theme)
8997
      @current_theme = Redmine::Themes.theme(Setting.ui_theme)
8998
    end
8999
    @current_theme
9000
  end
9001 909:cbb26bc654de Chris
9002 119:8661b858af72 Chris
  # Returns the header tags for the current theme
9003
  def heads_for_theme
9004
    if current_theme && current_theme.javascripts.include?('theme')
9005
      javascript_include_tag current_theme.javascript_path('theme')
9006
    end
9007
  end
9008 0:513646585e45 Chris
end
9009 1115:433d4f72a19b Chris
# Redmine - project management software
9010 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9011 1115:433d4f72a19b Chris
#
9012
# This program is free software; you can redistribute it and/or
9013
# modify it under the terms of the GNU General Public License
9014
# as published by the Free Software Foundation; either version 2
9015
# of the License, or (at your option) any later version.
9016
#
9017
# This program is distributed in the hope that it will be useful,
9018
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9019
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9020
# GNU General Public License for more details.
9021
#
9022
# You should have received a copy of the GNU General Public License
9023
# along with this program; if not, write to the Free Software
9024
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9025
9026
require 'fileutils'
9027
9028
module Redmine
9029
  module Thumbnail
9030
    extend Redmine::Utils::Shell
9031
9032
    CONVERT_BIN = (Redmine::Configuration['imagemagick_convert_command'] || 'convert').freeze
9033
9034
    # Generates a thumbnail for the source image to target
9035
    def self.generate(source, target, size)
9036
      return nil unless convert_available?
9037
      unless File.exists?(target)
9038
        directory = File.dirname(target)
9039
        unless File.exists?(directory)
9040
          FileUtils.mkdir_p directory
9041
        end
9042
        size_option = "#{size}x#{size}>"
9043
        cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -thumbnail #{shell_quote size_option} #{shell_quote target}"
9044
        unless system(cmd)
9045
          logger.error("Creating thumbnail failed (#{$?}):\nCommand: #{cmd}")
9046
          return nil
9047
        end
9048
      end
9049
      target
9050
    end
9051
9052
    def self.convert_available?
9053
      return @convert_available if defined?(@convert_available)
9054
      @convert_available = system("#{shell_quote CONVERT_BIN} -version") rescue false
9055
      logger.warn("Imagemagick's convert binary (#{CONVERT_BIN}) not available") unless @convert_available
9056
      @convert_available
9057
    end
9058
9059
    def self.logger
9060
      Rails.logger
9061
    end
9062
  end
9063
end
9064 441:cbce1fd3b1b7 Chris
# Redmine - project management software
9065 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9066 0:513646585e45 Chris
#
9067
# This program is free software; you can redistribute it and/or
9068
# modify it under the terms of the GNU General Public License
9069
# as published by the Free Software Foundation; either version 2
9070
# of the License, or (at your option) any later version.
9071 909:cbb26bc654de Chris
#
9072 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
9073
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9074
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9075
# GNU General Public License for more details.
9076 909:cbb26bc654de Chris
#
9077 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
9078
# along with this program; if not, write to the Free Software
9079
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9080
9081
module Redmine
9082
  # Class used to parse unified diffs
9083 441:cbce1fd3b1b7 Chris
  class UnifiedDiff < Array
9084 1115:433d4f72a19b Chris
    attr_reader :diff_type, :diff_style
9085 909:cbb26bc654de Chris
9086 0:513646585e45 Chris
    def initialize(diff, options={})
9087 1115:433d4f72a19b Chris
      options.assert_valid_keys(:type, :style, :max_lines)
9088 0:513646585e45 Chris
      diff = diff.split("\n") if diff.is_a?(String)
9089 441:cbce1fd3b1b7 Chris
      @diff_type = options[:type] || 'inline'
9090 1115:433d4f72a19b Chris
      @diff_style = options[:style]
9091 0:513646585e45 Chris
      lines = 0
9092
      @truncated = false
9093 1115:433d4f72a19b Chris
      diff_table = DiffTable.new(diff_type, diff_style)
9094 1464:261b3d9a4903 Chris
      diff.each do |line_raw|
9095
        line = Redmine::CodesetUtil.to_utf8_by_setting(line_raw)
9096
        unless diff_table.add_line(line)
9097 245:051f544170fe Chris
          self << diff_table if diff_table.length > 0
9098 1115:433d4f72a19b Chris
          diff_table = DiffTable.new(diff_type, diff_style)
9099 0:513646585e45 Chris
        end
9100
        lines += 1
9101
        if options[:max_lines] && lines > options[:max_lines]
9102
          @truncated = true
9103
          break
9104
        end
9105
      end
9106
      self << diff_table unless diff_table.empty?
9107
      self
9108
    end
9109 245:051f544170fe Chris
9110 0:513646585e45 Chris
    def truncated?; @truncated; end
9111
  end
9112
9113
  # Class that represents a file diff
9114 909:cbb26bc654de Chris
  class DiffTable < Array
9115 441:cbce1fd3b1b7 Chris
    attr_reader :file_name
9116 0:513646585e45 Chris
9117
    # Initialize with a Diff file and the type of Diff View
9118
    # The type view must be inline or sbs (side_by_side)
9119 1115:433d4f72a19b Chris
    def initialize(type="inline", style=nil)
9120 0:513646585e45 Chris
      @parsing = false
9121 441:cbce1fd3b1b7 Chris
      @added = 0
9122
      @removed = 0
9123 0:513646585e45 Chris
      @type = type
9124 1115:433d4f72a19b Chris
      @style = style
9125
      @file_name = nil
9126
      @git_diff = false
9127 0:513646585e45 Chris
    end
9128
9129
    # Function for add a line of this Diff
9130
    # Returns false when the diff ends
9131
    def add_line(line)
9132
      unless @parsing
9133
        if line =~ /^(---|\+\+\+) (.*)$/
9134 1115:433d4f72a19b Chris
          self.file_name = $2
9135 0:513646585e45 Chris
        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
9136
          @line_num_l = $2.to_i
9137
          @line_num_r = $5.to_i
9138
          @parsing = true
9139
        end
9140
      else
9141 1464:261b3d9a4903 Chris
        if line =~ %r{^[^\+\-\s@\\]}
9142 0:513646585e45 Chris
          @parsing = false
9143
          return false
9144
        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
9145
          @line_num_l = $2.to_i
9146
          @line_num_r = $5.to_i
9147
        else
9148 909:cbb26bc654de Chris
          parse_line(line, @type)
9149 0:513646585e45 Chris
        end
9150
      end
9151
      return true
9152
    end
9153 909:cbb26bc654de Chris
9154 441:cbce1fd3b1b7 Chris
    def each_line
9155
      prev_line_left, prev_line_right = nil, nil
9156
      each do |line|
9157
        spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1)
9158
        yield spacing, line
9159
        prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0
9160
        prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0
9161
      end
9162
    end
9163 0:513646585e45 Chris
9164
    def inspect
9165
      puts '### DIFF TABLE ###'
9166
      puts "file : #{file_name}"
9167
      self.each do |d|
9168
        d.inspect
9169
      end
9170
    end
9171
9172 441:cbce1fd3b1b7 Chris
    private
9173 0:513646585e45 Chris
9174 1115:433d4f72a19b Chris
    def file_name=(arg)
9175
      both_git_diff = false
9176
      if file_name.nil?
9177
        @git_diff = true if arg =~ %r{^(a/|/dev/null)}
9178
      else
9179
        both_git_diff = (@git_diff && arg =~ %r{^(b/|/dev/null)})
9180
      end
9181
      if both_git_diff
9182
        if file_name && arg == "/dev/null"
9183
          # keep the original file name
9184
          @file_name = file_name.sub(%r{^a/}, '')
9185
        else
9186
          # remove leading b/
9187
          @file_name = arg.sub(%r{^b/}, '')
9188
        end
9189
      elsif @style == "Subversion"
9190
        # removing trailing "(revision nn)"
9191
        @file_name = arg.sub(%r{\t+\(.*\)$}, '')
9192
      else
9193
        @file_name = arg
9194
      end
9195
    end
9196
9197 441:cbce1fd3b1b7 Chris
    def diff_for_added_line
9198
      if @type == 'sbs' && @removed > 0 && @added < @removed
9199
        self[-(@removed - @added)]
9200
      else
9201
        diff = Diff.new
9202
        self << diff
9203
        diff
9204
      end
9205
    end
9206 0:513646585e45 Chris
9207
    def parse_line(line, type="inline")
9208
      if line[0, 1] == "+"
9209 441:cbce1fd3b1b7 Chris
        diff = diff_for_added_line
9210 929:5f33065ddc4b Chris
        diff.line_right = line[1..-1]
9211 0:513646585e45 Chris
        diff.nb_line_right = @line_num_r
9212
        diff.type_diff_right = 'diff_in'
9213
        @line_num_r += 1
9214 441:cbce1fd3b1b7 Chris
        @added += 1
9215 0:513646585e45 Chris
        true
9216
      elsif line[0, 1] == "-"
9217 441:cbce1fd3b1b7 Chris
        diff = Diff.new
9218 929:5f33065ddc4b Chris
        diff.line_left = line[1..-1]
9219 0:513646585e45 Chris
        diff.nb_line_left = @line_num_l
9220
        diff.type_diff_left = 'diff_out'
9221 441:cbce1fd3b1b7 Chris
        self << diff
9222 0:513646585e45 Chris
        @line_num_l += 1
9223 441:cbce1fd3b1b7 Chris
        @removed += 1
9224 0:513646585e45 Chris
        true
9225 441:cbce1fd3b1b7 Chris
      else
9226
        write_offsets
9227
        if line[0, 1] =~ /\s/
9228
          diff = Diff.new
9229 929:5f33065ddc4b Chris
          diff.line_right = line[1..-1]
9230 441:cbce1fd3b1b7 Chris
          diff.nb_line_right = @line_num_r
9231 929:5f33065ddc4b Chris
          diff.line_left = line[1..-1]
9232 441:cbce1fd3b1b7 Chris
          diff.nb_line_left = @line_num_l
9233
          self << diff
9234
          @line_num_l += 1
9235
          @line_num_r += 1
9236
          true
9237
        elsif line[0, 1] = "\\"
9238 0:513646585e45 Chris
          true
9239
        else
9240
          false
9241
        end
9242
      end
9243
    end
9244 909:cbb26bc654de Chris
9245 441:cbce1fd3b1b7 Chris
    def write_offsets
9246
      if @added > 0 && @added == @removed
9247
        @added.times do |i|
9248
          line = self[-(1 + i)]
9249
          removed = (@type == 'sbs') ? line : self[-(1 + @added + i)]
9250
          offsets = offsets(removed.line_left, line.line_right)
9251
          removed.offsets = line.offsets = offsets
9252
        end
9253
      end
9254
      @added = 0
9255
      @removed = 0
9256
    end
9257 909:cbb26bc654de Chris
9258 441:cbce1fd3b1b7 Chris
    def offsets(line_left, line_right)
9259
      if line_left.present? && line_right.present? && line_left != line_right
9260
        max = [line_left.size, line_right.size].min
9261
        starting = 0
9262
        while starting < max && line_left[starting] == line_right[starting]
9263
          starting += 1
9264
        end
9265 1464:261b3d9a4903 Chris
        if (! "".respond_to?(:force_encoding)) && starting < line_left.size
9266
          while line_left[starting].ord.between?(128, 191) && starting > 0
9267
            starting -= 1
9268
          end
9269
        end
9270 441:cbce1fd3b1b7 Chris
        ending = -1
9271 1464:261b3d9a4903 Chris
        while ending >= -(max - starting) && (line_left[ending] == line_right[ending])
9272 441:cbce1fd3b1b7 Chris
          ending -= 1
9273
        end
9274 1464:261b3d9a4903 Chris
        if (! "".respond_to?(:force_encoding)) && ending > (-1 * line_left.size)
9275
          while line_left[ending].ord.between?(128, 255) && ending < -1
9276
            if line_left[ending].ord.between?(128, 191)
9277
              if line_left[ending + 1].ord.between?(128, 191)
9278
                ending += 1
9279
              else
9280
                break
9281
              end
9282
            else
9283
              ending += 1
9284
            end
9285
          end
9286
        end
9287 441:cbce1fd3b1b7 Chris
        unless starting == 0 && ending == -1
9288
          [starting, ending]
9289
        end
9290
      end
9291
    end
9292
  end
9293 0:513646585e45 Chris
9294
  # A line of diff
9295 909:cbb26bc654de Chris
  class Diff
9296 0:513646585e45 Chris
    attr_accessor :nb_line_left
9297
    attr_accessor :line_left
9298
    attr_accessor :nb_line_right
9299
    attr_accessor :line_right
9300
    attr_accessor :type_diff_right
9301
    attr_accessor :type_diff_left
9302 441:cbce1fd3b1b7 Chris
    attr_accessor :offsets
9303 909:cbb26bc654de Chris
9304 0:513646585e45 Chris
    def initialize()
9305
      self.nb_line_left = ''
9306
      self.nb_line_right = ''
9307
      self.line_left = ''
9308
      self.line_right = ''
9309
      self.type_diff_right = ''
9310
      self.type_diff_left = ''
9311
    end
9312 909:cbb26bc654de Chris
9313 441:cbce1fd3b1b7 Chris
    def type_diff
9314
      type_diff_right == 'diff_in' ? type_diff_right : type_diff_left
9315
    end
9316 909:cbb26bc654de Chris
9317 441:cbce1fd3b1b7 Chris
    def line
9318
      type_diff_right == 'diff_in' ? line_right : line_left
9319
    end
9320 909:cbb26bc654de Chris
9321 441:cbce1fd3b1b7 Chris
    def html_line_left
9322 929:5f33065ddc4b Chris
      line_to_html(line_left, offsets)
9323 441:cbce1fd3b1b7 Chris
    end
9324 909:cbb26bc654de Chris
9325 441:cbce1fd3b1b7 Chris
    def html_line_right
9326 929:5f33065ddc4b Chris
      line_to_html(line_right, offsets)
9327 441:cbce1fd3b1b7 Chris
    end
9328 909:cbb26bc654de Chris
9329 441:cbce1fd3b1b7 Chris
    def html_line
9330 929:5f33065ddc4b Chris
      line_to_html(line, offsets)
9331 441:cbce1fd3b1b7 Chris
    end
9332 0:513646585e45 Chris
9333
    def inspect
9334
      puts '### Start Line Diff ###'
9335
      puts self.nb_line_left
9336
      puts self.line_left
9337
      puts self.nb_line_right
9338
      puts self.line_right
9339
    end
9340 929:5f33065ddc4b Chris
9341
    private
9342
9343
    def line_to_html(line, offsets)
9344 1464:261b3d9a4903 Chris
      html = line_to_html_raw(line, offsets)
9345
      html.force_encoding('UTF-8') if html.respond_to?(:force_encoding)
9346
      html
9347
    end
9348
9349
    def line_to_html_raw(line, offsets)
9350 929:5f33065ddc4b Chris
      if offsets
9351
        s = ''
9352
        unless offsets.first == 0
9353
          s << CGI.escapeHTML(line[0..offsets.first-1])
9354
        end
9355
        s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
9356
        unless offsets.last == -1
9357
          s << CGI.escapeHTML(line[offsets.last+1..-1])
9358
        end
9359
        s
9360
      else
9361
        CGI.escapeHTML(line)
9362
      end
9363
    end
9364 0:513646585e45 Chris
  end
9365
end
9366
# Redmine - project management software
9367 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9368 0:513646585e45 Chris
#
9369
# This program is free software; you can redistribute it and/or
9370
# modify it under the terms of the GNU General Public License
9371
# as published by the Free Software Foundation; either version 2
9372
# of the License, or (at your option) any later version.
9373
#
9374
# This program is distributed in the hope that it will be useful,
9375
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9376
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9377
# GNU General Public License for more details.
9378
#
9379
# You should have received a copy of the GNU General Public License
9380
# along with this program; if not, write to the Free Software
9381
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9382
9383
module Redmine
9384
  module Utils
9385
    class << self
9386
      # Returns the relative root url of the application
9387
      def relative_url_root
9388
        ActionController::Base.respond_to?('relative_url_root') ?
9389
          ActionController::Base.relative_url_root.to_s :
9390 1115:433d4f72a19b Chris
          ActionController::Base.config.relative_url_root.to_s
9391 0:513646585e45 Chris
      end
9392 909:cbb26bc654de Chris
9393 0:513646585e45 Chris
      # Sets the relative root url of the application
9394
      def relative_url_root=(arg)
9395
        if ActionController::Base.respond_to?('relative_url_root=')
9396
          ActionController::Base.relative_url_root=arg
9397
        else
9398 1115:433d4f72a19b Chris
          ActionController::Base.config.relative_url_root = arg
9399
        end
9400
      end
9401
9402
      # Generates a n bytes random hex string
9403
      # Example:
9404
      #   random_hex(4) # => "89b8c729"
9405
      def random_hex(n)
9406
        SecureRandom.hex(n)
9407
      end
9408
    end
9409
9410
    module Shell
9411
      def shell_quote(str)
9412
        if Redmine::Platform.mswin?
9413
          '"' + str.gsub(/"/, '\\"') + '"'
9414
        else
9415
          "'" + str.gsub(/'/, "'\"'\"'") + "'"
9416
        end
9417
      end
9418
    end
9419
9420
    module DateCalculation
9421
      # Returns the number of working days between from and to
9422
      def working_days(from, to)
9423
        days = (to - from).to_i
9424
        if days > 0
9425
          weeks = days / 7
9426
          result = weeks * (7 - non_working_week_days.size)
9427
          days_left = days - weeks * 7
9428
          start_cwday = from.cwday
9429
          days_left.times do |i|
9430
            unless non_working_week_days.include?(((start_cwday + i - 1) % 7) + 1)
9431
              result += 1
9432
            end
9433
          end
9434
          result
9435
        else
9436
          0
9437
        end
9438
      end
9439
9440
      # Adds working days to the given date
9441
      def add_working_days(date, working_days)
9442
        if working_days > 0
9443
          weeks = working_days / (7 - non_working_week_days.size)
9444
          result = weeks * 7
9445
          days_left = working_days - weeks * (7 - non_working_week_days.size)
9446
          cwday = date.cwday
9447
          while days_left > 0
9448
            cwday += 1
9449
            unless non_working_week_days.include?(((cwday - 1) % 7) + 1)
9450
              days_left -= 1
9451
            end
9452
            result += 1
9453
          end
9454
          next_working_date(date + result)
9455
        else
9456
          date
9457
        end
9458
      end
9459
9460
      # Returns the date of the first day on or after the given date that is a working day
9461
      def next_working_date(date)
9462
        cwday = date.cwday
9463
        days = 0
9464
        while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
9465
          days += 1
9466
        end
9467
        date + days
9468
      end
9469
9470
      # Returns the index of non working week days (1=monday, 7=sunday)
9471
      def non_working_week_days
9472
        @non_working_week_days ||= begin
9473
          days = Setting.non_working_week_days
9474
          if days.is_a?(Array) && days.size < 7
9475
            days.map(&:to_i)
9476
          else
9477
            []
9478
          end
9479 0:513646585e45 Chris
        end
9480
      end
9481
    end
9482
  end
9483
end
9484
require 'rexml/document'
9485
9486
module Redmine
9487
  module VERSION #:nodoc:
9488 1115:433d4f72a19b Chris
    MAJOR = 2
9489 1517:dffacf8a6908 Chris
    MINOR = 5
9490
    TINY  = 2
9491 441:cbce1fd3b1b7 Chris
9492 0:513646585e45 Chris
    # Branch values:
9493
    # * official release: nil
9494
    # * stable branch:    stable
9495
    # * trunk:            devel
9496 441:cbce1fd3b1b7 Chris
    BRANCH = 'stable'
9497 0:513646585e45 Chris
9498 1115:433d4f72a19b Chris
    # Retrieves the revision from the working copy
9499 0:513646585e45 Chris
    def self.revision
9500 1568:bc47b68a9487 Chris
      return 0
9501
   #   if File.directory?(File.join(Rails.root, '.svn'))
9502
   #     begin
9503
   #       path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s)
9504
   #       if `svn info --xml #{path}` =~ /revision="(\d+)"/
9505
   #         return $1.to_i
9506
   #       end
9507
   #     rescue
9508 441:cbce1fd3b1b7 Chris
          # Could not find the current revision
9509 1568:bc47b68a9487 Chris
   #     end
9510
   #   end
9511
   #   nil
9512 0:513646585e45 Chris
    end
9513
9514
    REVISION = self.revision
9515 441:cbce1fd3b1b7 Chris
    ARRAY    = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact
9516
    STRING   = ARRAY.join('.')
9517 909:cbb26bc654de Chris
9518 441:cbce1fd3b1b7 Chris
    def self.to_a; ARRAY  end
9519 909:cbb26bc654de Chris
    def self.to_s; STRING end
9520 0:513646585e45 Chris
  end
9521
end
9522 119:8661b858af72 Chris
# Redmine - project management software
9523 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9524 119:8661b858af72 Chris
#
9525
# This program is free software; you can redistribute it and/or
9526
# modify it under the terms of the GNU General Public License
9527
# as published by the Free Software Foundation; either version 2
9528
# of the License, or (at your option) any later version.
9529 909:cbb26bc654de Chris
#
9530 119:8661b858af72 Chris
# This program is distributed in the hope that it will be useful,
9531
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9532
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9533
# GNU General Public License for more details.
9534 909:cbb26bc654de Chris
#
9535 119:8661b858af72 Chris
# You should have received a copy of the GNU General Public License
9536
# along with this program; if not, write to the Free Software
9537
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9538
9539
module Redmine
9540
  module Views
9541 1115:433d4f72a19b Chris
    class ApiTemplateHandler
9542
      def self.call(template)
9543
        "Redmine::Views::Builders.for(params[:format], request, response) do |api|; #{template.source}; self.output_buffer = api.output; end"
9544 119:8661b858af72 Chris
      end
9545
    end
9546
  end
9547
end
9548
# Redmine - project management software
9549 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9550 119:8661b858af72 Chris
#
9551
# This program is free software; you can redistribute it and/or
9552
# modify it under the terms of the GNU General Public License
9553
# as published by the Free Software Foundation; either version 2
9554
# of the License, or (at your option) any later version.
9555 909:cbb26bc654de Chris
#
9556 119:8661b858af72 Chris
# This program is distributed in the hope that it will be useful,
9557
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9558
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9559
# GNU General Public License for more details.
9560 909:cbb26bc654de Chris
#
9561 119:8661b858af72 Chris
# You should have received a copy of the GNU General Public License
9562
# along with this program; if not, write to the Free Software
9563
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9564
9565 1464:261b3d9a4903 Chris
require 'redmine/views/builders/json'
9566
require 'redmine/views/builders/xml'
9567
9568 119:8661b858af72 Chris
module Redmine
9569
  module Views
9570
    module Builders
9571 1115:433d4f72a19b Chris
      def self.for(format, request, response, &block)
9572 119:8661b858af72 Chris
        builder = case format
9573 1115:433d4f72a19b Chris
          when 'xml',  :xml;  Builders::Xml.new(request, response)
9574
          when 'json', :json; Builders::Json.new(request, response)
9575 119:8661b858af72 Chris
          else; raise "No builder for format #{format}"
9576
        end
9577
        if block
9578
          block.call(builder)
9579
        else
9580
          builder
9581
        end
9582
      end
9583
    end
9584
  end
9585
end
9586
# Redmine - project management software
9587 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9588 119:8661b858af72 Chris
#
9589
# This program is free software; you can redistribute it and/or
9590
# modify it under the terms of the GNU General Public License
9591
# as published by the Free Software Foundation; either version 2
9592
# of the License, or (at your option) any later version.
9593 909:cbb26bc654de Chris
#
9594 119:8661b858af72 Chris
# This program is distributed in the hope that it will be useful,
9595
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9596
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9597
# GNU General Public License for more details.
9598 909:cbb26bc654de Chris
#
9599 119:8661b858af72 Chris
# You should have received a copy of the GNU General Public License
9600
# along with this program; if not, write to the Free Software
9601
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9602
9603 1464:261b3d9a4903 Chris
require 'redmine/views/builders/structure'
9604 119:8661b858af72 Chris
9605
module Redmine
9606
  module Views
9607
    module Builders
9608
      class Json < Structure
9609 1115:433d4f72a19b Chris
        attr_accessor :jsonp
9610
9611
        def initialize(request, response)
9612
          super
9613 1464:261b3d9a4903 Chris
          callback = request.params[:callback] || request.params[:jsonp]
9614
          if callback && Setting.jsonp_enabled?
9615
            self.jsonp = callback.to_s.gsub(/[^a-zA-Z0-9_]/, '')
9616
          end
9617 1115:433d4f72a19b Chris
        end
9618
9619 119:8661b858af72 Chris
        def output
9620 1115:433d4f72a19b Chris
          json = @struct.first.to_json
9621
          if jsonp.present?
9622
            json = "#{jsonp}(#{json})"
9623
            response.content_type = 'application/javascript'
9624
          end
9625
          json
9626 119:8661b858af72 Chris
        end
9627
      end
9628
    end
9629
  end
9630
end
9631
# Redmine - project management software
9632 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9633 119:8661b858af72 Chris
#
9634
# This program is free software; you can redistribute it and/or
9635
# modify it under the terms of the GNU General Public License
9636
# as published by the Free Software Foundation; either version 2
9637
# of the License, or (at your option) any later version.
9638 909:cbb26bc654de Chris
#
9639 119:8661b858af72 Chris
# This program is distributed in the hope that it will be useful,
9640
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9641
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9642
# GNU General Public License for more details.
9643 909:cbb26bc654de Chris
#
9644 119:8661b858af72 Chris
# You should have received a copy of the GNU General Public License
9645
# along with this program; if not, write to the Free Software
9646
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9647
9648
require 'blankslate'
9649
9650
module Redmine
9651
  module Views
9652
    module Builders
9653
      class Structure < BlankSlate
9654 1115:433d4f72a19b Chris
        attr_accessor :request, :response
9655
9656
        def initialize(request, response)
9657 119:8661b858af72 Chris
          @struct = [{}]
9658 1115:433d4f72a19b Chris
          self.request = request
9659
          self.response = response
9660 119:8661b858af72 Chris
        end
9661 909:cbb26bc654de Chris
9662 119:8661b858af72 Chris
        def array(tag, options={}, &block)
9663
          @struct << []
9664
          block.call(self)
9665
          ret = @struct.pop
9666
          @struct.last[tag] = ret
9667
          @struct.last.merge!(options) if options
9668
        end
9669 909:cbb26bc654de Chris
9670 119:8661b858af72 Chris
        def method_missing(sym, *args, &block)
9671
          if args.any?
9672
            if args.first.is_a?(Hash)
9673
              if @struct.last.is_a?(Array)
9674
                @struct.last << args.first unless block
9675
              else
9676
                @struct.last[sym] = args.first
9677
              end
9678
            else
9679
              if @struct.last.is_a?(Array)
9680 1115:433d4f72a19b Chris
                if args.size == 1 && !block_given?
9681
                  @struct.last << args.first
9682
                else
9683
                  @struct.last << (args.last || {}).merge(:value => args.first)
9684
                end
9685 119:8661b858af72 Chris
              else
9686
                @struct.last[sym] = args.first
9687
              end
9688
            end
9689
          end
9690 909:cbb26bc654de Chris
9691 119:8661b858af72 Chris
          if block
9692
            @struct << (args.first.is_a?(Hash) ? args.first : {})
9693
            block.call(self)
9694
            ret = @struct.pop
9695
            if @struct.last.is_a?(Array)
9696
              @struct.last << ret
9697
            else
9698
              if @struct.last.has_key?(sym) && @struct.last[sym].is_a?(Hash)
9699
                @struct.last[sym].merge! ret
9700
              else
9701
                @struct.last[sym] = ret
9702
              end
9703
            end
9704
          end
9705
        end
9706 909:cbb26bc654de Chris
9707 119:8661b858af72 Chris
        def output
9708
          raise "Need to implement #{self.class.name}#output"
9709
        end
9710
      end
9711
    end
9712
  end
9713
end
9714
# Redmine - project management software
9715 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9716 119:8661b858af72 Chris
#
9717
# This program is free software; you can redistribute it and/or
9718
# modify it under the terms of the GNU General Public License
9719
# as published by the Free Software Foundation; either version 2
9720
# of the License, or (at your option) any later version.
9721 909:cbb26bc654de Chris
#
9722 119:8661b858af72 Chris
# This program is distributed in the hope that it will be useful,
9723
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9724
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9725
# GNU General Public License for more details.
9726 909:cbb26bc654de Chris
#
9727 119:8661b858af72 Chris
# You should have received a copy of the GNU General Public License
9728
# along with this program; if not, write to the Free Software
9729
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9730
9731 1115:433d4f72a19b Chris
require 'builder'
9732
9733 119:8661b858af72 Chris
module Redmine
9734
  module Views
9735
    module Builders
9736
      class Xml < ::Builder::XmlMarkup
9737 1115:433d4f72a19b Chris
        def initialize(request, response)
9738
          super()
9739 119:8661b858af72 Chris
          instruct!
9740
        end
9741 909:cbb26bc654de Chris
9742 119:8661b858af72 Chris
        def output
9743
          target!
9744
        end
9745 909:cbb26bc654de Chris
9746 119:8661b858af72 Chris
        def method_missing(sym, *args, &block)
9747 1115:433d4f72a19b Chris
          if args.size == 1 && args.first.is_a?(::Time)
9748 119:8661b858af72 Chris
            __send__ sym, args.first.xmlschema, &block
9749
          else
9750
            super
9751
          end
9752
        end
9753 909:cbb26bc654de Chris
9754 119:8661b858af72 Chris
        def array(name, options={}, &block)
9755
          __send__ name, (options || {}).merge(:type => 'array'), &block
9756
        end
9757
      end
9758
    end
9759
  end
9760
end
9761 1115:433d4f72a19b Chris
# Redmine - project management software
9762 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9763 1115:433d4f72a19b Chris
#
9764
# This program is free software; you can redistribute it and/or
9765
# modify it under the terms of the GNU General Public License
9766
# as published by the Free Software Foundation; either version 2
9767
# of the License, or (at your option) any later version.
9768
#
9769
# This program is distributed in the hope that it will be useful,
9770
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9771
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9772
# GNU General Public License for more details.
9773
#
9774
# You should have received a copy of the GNU General Public License
9775
# along with this program; if not, write to the Free Software
9776
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9777
9778
require 'action_view/helpers/form_helper'
9779
9780
class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder
9781
  include Redmine::I18n
9782
9783 1517:dffacf8a6908 Chris
  (field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for check_box) +
9784 1115:433d4f72a19b Chris
        %w(date_select)).each do |selector|
9785
    src = <<-END_SRC
9786
    def #{selector}(field, options = {})
9787
      label_for_field(field, options) + super(field, options.except(:label)).html_safe
9788
    end
9789
    END_SRC
9790
    class_eval src, __FILE__, __LINE__
9791
  end
9792
9793 1517:dffacf8a6908 Chris
  def check_box(field, options={}, checked_value="1", unchecked_value="0")
9794
    label_for_field(field, options) + super(field, options.except(:label), checked_value, unchecked_value).html_safe
9795
  end
9796
9797 1115:433d4f72a19b Chris
  def select(field, choices, options = {}, html_options = {})
9798
    label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe
9799
  end
9800
9801
  def time_zone_select(field, priority_zones = nil, options = {}, html_options = {})
9802
        label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe
9803
  end
9804
9805
  # Returns a label tag for the given field
9806
  def label_for_field(field, options = {})
9807
      return ''.html_safe if options.delete(:no_label)
9808
      text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label]
9809
      text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
9810
      text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
9811
      @template.content_tag("label", text.html_safe,
9812
                                     :class => (@object && @object.errors[field].present? ? "error" : nil),
9813
                                     :for => (@object_name.to_s + "_" + field.to_s))
9814
  end
9815
end
9816 0:513646585e45 Chris
# Redmine - project management software
9817 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9818 0:513646585e45 Chris
#
9819
# This program is free software; you can redistribute it and/or
9820
# modify it under the terms of the GNU General Public License
9821
# as published by the Free Software Foundation; either version 2
9822
# of the License, or (at your option) any later version.
9823 909:cbb26bc654de Chris
#
9824 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
9825
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9826
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9827
# GNU General Public License for more details.
9828 909:cbb26bc654de Chris
#
9829 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
9830
# along with this program; if not, write to the Free Software
9831
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9832
9833
module Redmine
9834
  module Views
9835
    module MyPage
9836
      module Block
9837
        def self.additional_blocks
9838 1115:433d4f72a19b Chris
          @@additional_blocks ||= Dir.glob("#{Redmine::Plugin.directory}/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
9839 0:513646585e45 Chris
            name = File.basename(file).split('.').first.gsub(/^_/, '')
9840
            h[name] = name.to_sym
9841
            h
9842
          end
9843
        end
9844
      end
9845
    end
9846
  end
9847
end
9848
# Redmine - project management software
9849 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9850 0:513646585e45 Chris
#
9851
# This program is free software; you can redistribute it and/or
9852
# modify it under the terms of the GNU General Public License
9853
# as published by the Free Software Foundation; either version 2
9854
# of the License, or (at your option) any later version.
9855 909:cbb26bc654de Chris
#
9856 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
9857
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9858
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9859
# GNU General Public License for more details.
9860 909:cbb26bc654de Chris
#
9861 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
9862
# along with this program; if not, write to the Free Software
9863
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9864
9865
module Redmine
9866
  module Views
9867
    class OtherFormatsBuilder
9868
      def initialize(view)
9869
        @view = view
9870
      end
9871 909:cbb26bc654de Chris
9872 0:513646585e45 Chris
      def link_to(name, options={})
9873 1115:433d4f72a19b Chris
        url = { :format => name.to_s.downcase }.merge(options.delete(:url) || {}).except('page')
9874 0:513646585e45 Chris
        caption = options.delete(:caption) || name
9875
        html_options = { :class => name.to_s.downcase, :rel => 'nofollow' }.merge(options)
9876
        @view.content_tag('span', @view.link_to(caption, url, html_options))
9877
      end
9878
    end
9879
  end
9880
end
9881
# Redmine - project management software
9882 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
9883 0:513646585e45 Chris
#
9884
# This program is free software; you can redistribute it and/or
9885
# modify it under the terms of the GNU General Public License
9886
# as published by the Free Software Foundation; either version 2
9887
# of the License, or (at your option) any later version.
9888 909:cbb26bc654de Chris
#
9889 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
9890
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9891
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9892
# GNU General Public License for more details.
9893 909:cbb26bc654de Chris
#
9894 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
9895
# along with this program; if not, write to the Free Software
9896
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
9897
9898 1115:433d4f72a19b Chris
require 'digest/md5'
9899
9900 0:513646585e45 Chris
module Redmine
9901
  module WikiFormatting
9902 909:cbb26bc654de Chris
    class StaleSectionError < Exception; end
9903
9904 0:513646585e45 Chris
    @@formatters = {}
9905
9906
    class << self
9907
      def map
9908
        yield self
9909
      end
9910 909:cbb26bc654de Chris
9911 1517:dffacf8a6908 Chris
      def register(name, formatter, helper, options={})
9912
        name = name.to_s
9913
        raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
9914
        @@formatters[name] = {
9915
          :formatter => formatter,
9916
          :helper => helper,
9917
          :label => options[:label] || name.humanize
9918
        }
9919 0:513646585e45 Chris
      end
9920 909:cbb26bc654de Chris
9921
      def formatter
9922
        formatter_for(Setting.text_formatting)
9923
      end
9924
9925 0:513646585e45 Chris
      def formatter_for(name)
9926
        entry = @@formatters[name.to_s]
9927
        (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
9928
      end
9929 909:cbb26bc654de Chris
9930 0:513646585e45 Chris
      def helper_for(name)
9931
        entry = @@formatters[name.to_s]
9932
        (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
9933
      end
9934 909:cbb26bc654de Chris
9935 0:513646585e45 Chris
      def format_names
9936
        @@formatters.keys.map
9937
      end
9938 909:cbb26bc654de Chris
9939 1517:dffacf8a6908 Chris
      def formats_for_select
9940
        @@formatters.map {|name, options| [options[:label], name]}
9941
      end
9942
9943 909:cbb26bc654de Chris
      def to_html(format, text, options = {})
9944 1115:433d4f72a19b Chris
        text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute])
9945 0:513646585e45 Chris
          # Text retrieved from the cache store may be frozen
9946
          # We need to dup it so we can do in-place substitutions with gsub!
9947
          cache_store.fetch cache_key do
9948
            formatter_for(format).new(text).to_html
9949
          end.dup
9950
        else
9951
          formatter_for(format).new(text).to_html
9952
        end
9953
        text
9954
      end
9955
9956 909:cbb26bc654de Chris
      # Returns true if the text formatter supports single section edit
9957
      def supports_section_edit?
9958
        (formatter.instance_methods & ['update_section', :update_section]).any?
9959
      end
9960
9961 1115:433d4f72a19b Chris
      # Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done
9962
      def cache_key_for(format, text, object, attribute)
9963
        if object && attribute && !object.new_record? && format.present?
9964
          "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}"
9965 0:513646585e45 Chris
        end
9966
      end
9967 909:cbb26bc654de Chris
9968 0:513646585e45 Chris
      # Returns the cache store used to cache HTML output
9969
      def cache_store
9970
        ActionController::Base.cache_store
9971
      end
9972
    end
9973 909:cbb26bc654de Chris
9974 1115:433d4f72a19b Chris
    module LinksHelper
9975
      AUTO_LINK_RE = %r{
9976
                      (                          # leading text
9977
                        <\w+.*?>|                # leading HTML tag, or
9978 1464:261b3d9a4903 Chris
                        [\s\(\[,;]|              # leading punctuation, or
9979 1115:433d4f72a19b Chris
                        ^                        # beginning of line
9980
                      )
9981
                      (
9982
                        (?:https?://)|           # protocol spec, or
9983
                        (?:s?ftps?://)|
9984
                        (?:www\.)                # www.*
9985
                      )
9986
                      (
9987 1464:261b3d9a4903 Chris
                        ([^<]\S*?)               # url
9988 1115:433d4f72a19b Chris
                        (\/)?                    # slash
9989
                      )
9990
                      ((?:&gt;)?|[^[:alnum:]_\=\/;\(\)]*?)               # post
9991
                      (?=<|\s|$)
9992
                     }x unless const_defined?(:AUTO_LINK_RE)
9993
9994
      # Destructively remplaces urls into clickable links
9995
      def auto_link!(text)
9996
        text.gsub!(AUTO_LINK_RE) do
9997
          all, leading, proto, url, post = $&, $1, $2, $3, $6
9998
          if leading =~ /<a\s/i || leading =~ /![<>=]?/
9999
            # don't replace URL's that are already linked
10000
            # and URL's prefixed with ! !> !< != (textile images)
10001
            all
10002
          else
10003
            # Idea below : an URL with unbalanced parethesis and
10004
            # ending by ')' is put into external parenthesis
10005
            if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
10006
              url=url[0..-2] # discard closing parenth from url
10007
              post = ")"+post # add closing parenth to post
10008
            end
10009
            content = proto + url
10010
            href = "#{proto=="www."?"http://www.":proto}#{url}"
10011
            %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
10012
          end
10013
        end
10014
      end
10015
10016
      # Destructively remplaces email addresses into clickable links
10017
      def auto_mailto!(text)
10018
        text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
10019
          mail = $1
10020
          if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
10021
            mail
10022
          else
10023
            %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
10024
          end
10025
        end
10026
      end
10027
    end
10028
10029 0:513646585e45 Chris
    # Default formatter module
10030
    module NullFormatter
10031
      class Formatter
10032
        include ActionView::Helpers::TagHelper
10033
        include ActionView::Helpers::TextHelper
10034
        include ActionView::Helpers::UrlHelper
10035 1115:433d4f72a19b Chris
        include Redmine::WikiFormatting::LinksHelper
10036 909:cbb26bc654de Chris
10037 0:513646585e45 Chris
        def initialize(text)
10038
          @text = text
10039
        end
10040 909:cbb26bc654de Chris
10041 0:513646585e45 Chris
        def to_html(*args)
10042 1115:433d4f72a19b Chris
          t = CGI::escapeHTML(@text)
10043
          auto_link!(t)
10044
          auto_mailto!(t)
10045
          simple_format(t, {}, :sanitize => false)
10046 0:513646585e45 Chris
        end
10047
      end
10048 909:cbb26bc654de Chris
10049 0:513646585e45 Chris
      module Helper
10050
        def wikitoolbar_for(field_id)
10051
        end
10052 909:cbb26bc654de Chris
10053 0:513646585e45 Chris
        def heads_for_wiki_formatter
10054
        end
10055 909:cbb26bc654de Chris
10056 0:513646585e45 Chris
        def initial_page_content(page)
10057
          page.pretty_title.to_s
10058
        end
10059
      end
10060
    end
10061
  end
10062
end
10063 909:cbb26bc654de Chris
# Redmine - project management software
10064 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
10065 0:513646585e45 Chris
#
10066
# This program is free software; you can redistribute it and/or
10067
# modify it under the terms of the GNU General Public License
10068
# as published by the Free Software Foundation; either version 2
10069
# of the License, or (at your option) any later version.
10070 909:cbb26bc654de Chris
#
10071 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
10072
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10073
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10074
# GNU General Public License for more details.
10075 909:cbb26bc654de Chris
#
10076 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
10077
# along with this program; if not, write to the Free Software
10078
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
10079
10080
module Redmine
10081
  module WikiFormatting
10082
    module Macros
10083
      module Definitions
10084 1115:433d4f72a19b Chris
        # Returns true if +name+ is the name of an existing macro
10085
        def macro_exists?(name)
10086
          Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
10087
        end
10088
10089
        def exec_macro(name, obj, args, text)
10090
          macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
10091
          return unless macro_options
10092
10093 0:513646585e45 Chris
          method_name = "macro_#{name}"
10094 1115:433d4f72a19b Chris
          unless macro_options[:parse_args] == false
10095
            args = args.split(',').map(&:strip)
10096
          end
10097
10098
          begin
10099
            if self.class.instance_method(method_name).arity == 3
10100
              send(method_name, obj, args, text)
10101
            elsif text
10102
              raise "This macro does not accept a block of text"
10103
            else
10104
              send(method_name, obj, args)
10105
            end
10106
          rescue => e
10107
            "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
10108
          end
10109 0:513646585e45 Chris
        end
10110 909:cbb26bc654de Chris
10111 0:513646585e45 Chris
        def extract_macro_options(args, *keys)
10112
          options = {}
10113 1115:433d4f72a19b Chris
          while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym)
10114 0:513646585e45 Chris
            options[$1.downcase.to_sym] = $2
10115
            args.pop
10116
          end
10117
          return [args, options]
10118
        end
10119
      end
10120 909:cbb26bc654de Chris
10121 0:513646585e45 Chris
      @@available_macros = {}
10122 1115:433d4f72a19b Chris
      mattr_accessor :available_macros
10123 909:cbb26bc654de Chris
10124 0:513646585e45 Chris
      class << self
10125
        # Plugins can use this method to define new macros:
10126 909:cbb26bc654de Chris
        #
10127 0:513646585e45 Chris
        #   Redmine::WikiFormatting::Macros.register do
10128
        #     desc "This is my macro"
10129
        #     macro :my_macro do |obj, args|
10130
        #       "My macro output"
10131
        #     end
10132 1115:433d4f72a19b Chris
        #
10133
        #     desc "This is my macro that accepts a block of text"
10134
        #     macro :my_macro do |obj, args, text|
10135
        #       "My macro output"
10136
        #     end
10137 0:513646585e45 Chris
        #   end
10138
        def register(&block)
10139
          class_eval(&block) if block_given?
10140
        end
10141 909:cbb26bc654de Chris
10142 1115:433d4f72a19b Chris
        # Defines a new macro with the given name, options and block.
10143
        #
10144
        # Options:
10145
        # * :desc - A description of the macro
10146
        # * :parse_args => false - Disables arguments parsing (the whole arguments
10147
        #   string is passed to the macro)
10148
        #
10149
        # Macro blocks accept 2 or 3 arguments:
10150
        # * obj: the object that is rendered (eg. an Issue, a WikiContent...)
10151
        # * args: macro arguments
10152
        # * text: the block of text given to the macro (should be present only if the
10153
        #   macro accepts a block of text). text is a String or nil if the macro is
10154
        #   invoked without a block of text.
10155
        #
10156
        # Examples:
10157
        # By default, when the macro is invoked, the coma separated list of arguments
10158
        # is split and passed to the macro block as an array. If no argument is given
10159
        # the macro will be invoked with an empty array:
10160
        #
10161
        #   macro :my_macro do |obj, args|
10162
        #     # args is an array
10163
        #     # and this macro do not accept a block of text
10164
        #   end
10165
        #
10166
        # You can disable arguments spliting with the :parse_args => false option. In
10167
        # this case, the full string of arguments is passed to the macro:
10168
        #
10169
        #   macro :my_macro, :parse_args => false do |obj, args|
10170
        #     # args is a string
10171
        #   end
10172
        #
10173
        # Macro can optionally accept a block of text:
10174
        #
10175
        #   macro :my_macro do |obj, args, text|
10176
        #     # this macro accepts a block of text
10177
        #   end
10178
        #
10179
        # Macros are invoked in formatted text using double curly brackets. Arguments
10180
        # must be enclosed in parenthesis if any. A new line after the macro name or the
10181
        # arguments starts the block of text that will be passe to the macro (invoking
10182
        # a macro that do not accept a block of text with some text will fail).
10183
        # Examples:
10184
        #
10185
        #   No arguments:
10186
        #   {{my_macro}}
10187
        #
10188
        #   With arguments:
10189
        #   {{my_macro(arg1, arg2)}}
10190
        #
10191
        #   With a block of text:
10192
        #   {{my_macro
10193
        #   multiple lines
10194
        #   of text
10195
        #   }}
10196
        #
10197
        #   With arguments and a block of text
10198
        #   {{my_macro(arg1, arg2)
10199
        #   multiple lines
10200
        #   of text
10201
        #   }}
10202
        #
10203
        # If a block of text is given, the closing tag }} must be at the start of a new line.
10204
        def macro(name, options={}, &block)
10205
          options.assert_valid_keys(:desc, :parse_args)
10206
          unless name.to_s.match(/\A\w+\z/)
10207
            raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
10208
          end
10209
          unless block_given?
10210
            raise "Can not create a macro without a block!"
10211
          end
10212 1294:3e4c3460b6ca Chris
          name = name.to_s.downcase.to_sym
10213 1115:433d4f72a19b Chris
          available_macros[name] = {:desc => @@desc || ''}.merge(options)
10214 0:513646585e45 Chris
          @@desc = nil
10215 1294:3e4c3460b6ca Chris
          Definitions.send :define_method, "macro_#{name}", &block
10216 0:513646585e45 Chris
        end
10217 909:cbb26bc654de Chris
10218 0:513646585e45 Chris
        # Sets description for the next macro to be defined
10219
        def desc(txt)
10220
          @@desc = txt
10221
        end
10222
      end
10223 909:cbb26bc654de Chris
10224 0:513646585e45 Chris
      # Builtin macros
10225
      desc "Sample macro."
10226 1115:433d4f72a19b Chris
      macro :hello_world do |obj, args, text|
10227
        h("Hello world! Object: #{obj.class.name}, " +
10228
          (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
10229
          " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
10230
        )
10231 0:513646585e45 Chris
      end
10232 909:cbb26bc654de Chris
10233 0:513646585e45 Chris
      desc "Displays a list of all available macros, including description if available."
10234 909:cbb26bc654de Chris
      macro :macro_list do |obj, args|
10235 1115:433d4f72a19b Chris
        out = ''.html_safe
10236
        @@available_macros.each do |macro, options|
10237
          out << content_tag('dt', content_tag('code', macro.to_s))
10238
          out << content_tag('dd', textilizable(options[:desc]))
10239 0:513646585e45 Chris
        end
10240
        content_tag('dl', out)
10241
      end
10242 909:cbb26bc654de Chris
10243 0:513646585e45 Chris
      desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
10244
             "  !{{child_pages}} -- can be used from a wiki page only\n" +
10245 1115:433d4f72a19b Chris
             "  !{{child_pages(depth=2)}} -- display 2 levels nesting only\n"
10246 0:513646585e45 Chris
             "  !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
10247
             "  !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
10248
      macro :child_pages do |obj, args|
10249 1115:433d4f72a19b Chris
        args, options = extract_macro_options(args, :parent, :depth)
10250
        options[:depth] = options[:depth].to_i if options[:depth].present?
10251
10252 0:513646585e45 Chris
        page = nil
10253
        if args.size > 0
10254
          page = Wiki.find_page(args.first.to_s, :project => @project)
10255
        elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
10256
          page = obj.page
10257
        else
10258
          raise 'With no argument, this macro can be called from wiki pages only.'
10259
        end
10260
        raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
10261 1115:433d4f72a19b Chris
        pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
10262 0:513646585e45 Chris
        render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
10263
      end
10264 909:cbb26bc654de Chris
10265 0:513646585e45 Chris
      desc "Include a wiki page. Example:\n\n  !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n  !{{include(projectname:Foo)}}"
10266
      macro :include do |obj, args|
10267
        page = Wiki.find_page(args.first.to_s, :project => @project)
10268
        raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
10269
        @included_wiki_pages ||= []
10270
        raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
10271
        @included_wiki_pages << page.title
10272 37:94944d00e43c chris
        out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
10273 0:513646585e45 Chris
        @included_wiki_pages.pop
10274
        out
10275
      end
10276 1115:433d4f72a19b Chris
10277
      desc "Inserts of collapsed block of text. Example:\n\n  {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}"
10278
      macro :collapse do |obj, args, text|
10279
        html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
10280
        show_label = args[0] || l(:button_show)
10281
        hide_label = args[1] || args[0] || l(:button_hide)
10282
        js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
10283
        out = ''.html_safe
10284
        out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
10285
        out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
10286 1517:dffacf8a6908 Chris
        out << content_tag('div', textilizable(text, :object => obj, :headings => false), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
10287 1115:433d4f72a19b Chris
        out
10288
      end
10289
10290
      desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
10291
      macro :thumbnail do |obj, args|
10292
        args, options = extract_macro_options(args, :size, :title)
10293
        filename = args.first
10294
        raise 'Filename required' unless filename.present?
10295
        size = options[:size]
10296
        raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
10297
        size = size.to_i
10298
        size = nil unless size > 0
10299
        if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
10300
          title = options[:title] || attachment.title
10301 1517:dffacf8a6908 Chris
          thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => false)
10302
          image_url = url_for(:controller => 'attachments', :action => 'show', :id => attachment, :only_path => false)
10303
10304
          img = image_tag(thumbnail_url, :alt => attachment.filename)
10305
          link_to(img, image_url, :class => 'thumbnail', :title => title)
10306 1115:433d4f72a19b Chris
        else
10307
          raise "Attachment #{filename} not found"
10308
        end
10309
      end
10310 0:513646585e45 Chris
    end
10311
  end
10312
end
10313 1517:dffacf8a6908 Chris
# Redmine - project management software
10314
# Copyright (C) 2006-2014  Jean-Philippe Lang
10315
#
10316
# This program is free software; you can redistribute it and/or
10317
# modify it under the terms of the GNU General Public License
10318
# as published by the Free Software Foundation; either version 2
10319
# of the License, or (at your option) any later version.
10320
#
10321
# This program is distributed in the hope that it will be useful,
10322
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10323
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10324
# GNU General Public License for more details.
10325
#
10326
# You should have received a copy of the GNU General Public License
10327
# along with this program; if not, write to the Free Software
10328
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
10329
10330
require 'cgi'
10331
10332
module Redmine
10333
  module WikiFormatting
10334
    module Markdown
10335
      class HTML < Redcarpet::Render::HTML
10336
        include ActionView::Helpers::TagHelper
10337
10338
        def link(link, title, content)
10339
          css = nil
10340
          unless link && link.starts_with?('/')
10341
            css = 'external'
10342
          end
10343
          content_tag('a', content.to_s.html_safe, :href => link, :title => title, :class => css)
10344
        end
10345
10346
        def block_code(code, language)
10347
          if language.present?
10348
            "<pre><code class=\"#{CGI.escapeHTML language} syntaxhl\">" +
10349
              Redmine::SyntaxHighlighting.highlight_by_language(code, language) +
10350
              "</code></pre>"
10351
          else
10352
            "<pre>" + CGI.escapeHTML(code) + "</pre>"
10353
          end
10354
        end
10355
      end
10356
10357
      class Formatter
10358
        def initialize(text)
10359
          @text = text
10360
        end
10361
10362
        def to_html(*args)
10363
          html = formatter.render(@text)
10364
          # restore wiki links eg. [[Foo]]
10365
          html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
10366
            "[[#{$2}]]"
10367
          end
10368
          # restore Redmine links with double-quotes, eg. version:"1.0"
10369
          html.gsub!(/(\w):&quot;(.+?)&quot;/) do
10370
            "#{$1}:\"#{$2}\""
10371
          end
10372
          html
10373
        end
10374
10375
        def get_section(index)
10376
          section = extract_sections(index)[1]
10377
          hash = Digest::MD5.hexdigest(section)
10378
          return section, hash
10379
        end
10380
10381
        def update_section(index, update, hash=nil)
10382
          t = extract_sections(index)
10383
          if hash.present? && hash != Digest::MD5.hexdigest(t[1])
10384
            raise Redmine::WikiFormatting::StaleSectionError
10385
          end
10386
          t[1] = update unless t[1].blank?
10387
          t.reject(&:blank?).join "\n\n"
10388
        end
10389
10390
        def extract_sections(index)
10391
          sections = ['', '', '']
10392
          offset = 0
10393
          i = 0
10394
          l = 1
10395
          inside_pre = false
10396
          @text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|~~~.*)\s*$)/).each do |part|
10397
            level = nil
10398
            if part =~ /\A~{3,}(\S+)?\s*$/
10399
              if $1
10400
                if !inside_pre
10401
                  inside_pre = true
10402
                end
10403
              else
10404
                inside_pre = !inside_pre
10405
              end
10406
            elsif inside_pre
10407
              # nop
10408
            elsif part =~ /\A(#+).+/
10409
              level = $1.size
10410
            elsif part =~ /\A.+\r?\n\r?(\=+|\-+)\s*$/
10411
              level = $1.include?('=') ? 1 : 2
10412
            end
10413
            if level
10414
              i += 1
10415
              if offset == 0 && i == index
10416
                # entering the requested section
10417
                offset = 1
10418
                l = level
10419
              elsif offset == 1 && i > index && level <= l
10420
                # leaving the requested section
10421
                offset = 2
10422
              end
10423
            end
10424
            sections[offset] << part
10425
          end
10426
          sections.map(&:strip)
10427
        end
10428
10429
        private
10430
10431
        def formatter
10432
          @@formatter ||= Redcarpet::Markdown.new(
10433
            Redmine::WikiFormatting::Markdown::HTML.new(
10434
              :filter_html => true,
10435
              :hard_wrap => true
10436
            ),
10437
            :autolink => true,
10438
            :fenced_code_blocks => true,
10439
            :space_after_headers => true,
10440
            :tables => true,
10441
            :strikethrough => true,
10442
            :superscript => true,
10443
            :no_intra_emphasis => true
10444
          )
10445
        end
10446
      end
10447
    end
10448
  end
10449
end
10450
# Redmine - project management software
10451
# Copyright (C) 2006-2014  Jean-Philippe Lang
10452
#
10453
# This program is free software; you can redistribute it and/or
10454
# modify it under the terms of the GNU General Public License
10455
# as published by the Free Software Foundation; either version 2
10456
# of the License, or (at your option) any later version.
10457
#
10458
# This program is distributed in the hope that it will be useful,
10459
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10460
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10461
# GNU General Public License for more details.
10462
#
10463
# You should have received a copy of the GNU General Public License
10464
# along with this program; if not, write to the Free Software
10465
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
10466
10467
module Redmine
10468
  module WikiFormatting
10469
    module Markdown
10470
      module Helper
10471
        def wikitoolbar_for(field_id)
10472
          heads_for_wiki_formatter
10473
          javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.draw();")
10474
        end
10475
10476
        def initial_page_content(page)
10477
          "# #{@page.pretty_title}"
10478
        end
10479
10480
        def heads_for_wiki_formatter
10481
          unless @heads_for_wiki_formatter_included
10482
            content_for :header_tags do
10483
              javascript_include_tag('jstoolbar/jstoolbar') +
10484
              javascript_include_tag('jstoolbar/markdown') +
10485
              javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
10486
              stylesheet_link_tag('jstoolbar')
10487
            end
10488
            @heads_for_wiki_formatter_included = true
10489
          end
10490
        end
10491
      end
10492
    end
10493
  end
10494
end
10495 0:513646585e45 Chris
# Redmine - project management software
10496 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
10497 0:513646585e45 Chris
#
10498
# This program is free software; you can redistribute it and/or
10499
# modify it under the terms of the GNU General Public License
10500
# as published by the Free Software Foundation; either version 2
10501
# of the License, or (at your option) any later version.
10502 909:cbb26bc654de Chris
#
10503 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
10504
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10505
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10506
# GNU General Public License for more details.
10507 909:cbb26bc654de Chris
#
10508 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
10509
# along with this program; if not, write to the Free Software
10510
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
10511
10512
require 'redcloth3'
10513 909:cbb26bc654de Chris
require 'digest/md5'
10514 0:513646585e45 Chris
10515
module Redmine
10516
  module WikiFormatting
10517
    module Textile
10518
      class Formatter < RedCloth3
10519
        include ActionView::Helpers::TagHelper
10520 1115:433d4f72a19b Chris
        include Redmine::WikiFormatting::LinksHelper
10521
10522
        alias :inline_auto_link :auto_link!
10523
        alias :inline_auto_mailto :auto_mailto!
10524 909:cbb26bc654de Chris
10525 0:513646585e45 Chris
        # auto_link rule after textile rules so that it doesn't break !image_url! tags
10526 37:94944d00e43c chris
        RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
10527 909:cbb26bc654de Chris
10528 0:513646585e45 Chris
        def initialize(*args)
10529
          super
10530
          self.hard_breaks=true
10531
          self.no_span_caps=true
10532 1115:433d4f72a19b Chris
          self.filter_styles=false
10533 0:513646585e45 Chris
        end
10534 909:cbb26bc654de Chris
10535 0:513646585e45 Chris
        def to_html(*rules)
10536
          @toc = []
10537
          super(*RULES).to_s
10538
        end
10539 909:cbb26bc654de Chris
10540
        def get_section(index)
10541
          section = extract_sections(index)[1]
10542
          hash = Digest::MD5.hexdigest(section)
10543
          return section, hash
10544
        end
10545
10546
        def update_section(index, update, hash=nil)
10547
          t = extract_sections(index)
10548
          if hash.present? && hash != Digest::MD5.hexdigest(t[1])
10549
            raise Redmine::WikiFormatting::StaleSectionError
10550
          end
10551
          t[1] = update unless t[1].blank?
10552
          t.reject(&:blank?).join "\n\n"
10553
        end
10554
10555
        def extract_sections(index)
10556
          @pre_list = []
10557
          text = self.dup
10558
          rip_offtags text, false, false
10559
          before = ''
10560
          s = ''
10561
          after = ''
10562
          i = 0
10563
          l = 1
10564
          started = false
10565
          ended = false
10566 1294:3e4c3460b6ca Chris
          text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level|
10567 909:cbb26bc654de Chris
            if heading.nil?
10568
              if ended
10569
                after << all
10570
              elsif started
10571
                s << all
10572
              else
10573
                before << all
10574
              end
10575
              break
10576
            end
10577
            i += 1
10578
            if ended
10579
              after << all
10580
            elsif i == index
10581
              l = level.to_i
10582
              before << content
10583
              s << heading
10584
              started = true
10585
            elsif i > index
10586
              s << content
10587
              if level.to_i > l
10588
                s << heading
10589
              else
10590
                after << heading
10591
                ended = true
10592
              end
10593
            else
10594
              before << all
10595
            end
10596
          end
10597
          sections = [before.strip, s.strip, after.strip]
10598
          sections.each {|section| smooth_offtags_without_code_highlighting section}
10599
          sections
10600
        end
10601
10602 0:513646585e45 Chris
      private
10603 909:cbb26bc654de Chris
10604 0:513646585e45 Chris
        # Patch for RedCloth.  Fixed in RedCloth r128 but _why hasn't released it yet.
10605
        # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
10606 909:cbb26bc654de Chris
        def hard_break( text )
10607 441:cbce1fd3b1b7 Chris
          text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
10608 0:513646585e45 Chris
        end
10609 909:cbb26bc654de Chris
10610
        alias :smooth_offtags_without_code_highlighting :smooth_offtags
10611 0:513646585e45 Chris
        # Patch to add code highlighting support to RedCloth
10612
        def smooth_offtags( text )
10613
          unless @pre_list.empty?
10614
            ## replace <pre> content
10615
            text.gsub!(/<redpre#(\d+)>/) do
10616
              content = @pre_list[$1.to_i]
10617
              if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
10618 909:cbb26bc654de Chris
                content = "<code class=\"#{$1} syntaxhl\">" +
10619 0:513646585e45 Chris
                  Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
10620
              end
10621
              content
10622
            end
10623
          end
10624
        end
10625
      end
10626
    end
10627
  end
10628
end
10629
# Redmine - project management software
10630 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
10631 0:513646585e45 Chris
#
10632
# This program is free software; you can redistribute it and/or
10633
# modify it under the terms of the GNU General Public License
10634
# as published by the Free Software Foundation; either version 2
10635
# of the License, or (at your option) any later version.
10636 909:cbb26bc654de Chris
#
10637 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
10638
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10639
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10640
# GNU General Public License for more details.
10641 909:cbb26bc654de Chris
#
10642 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
10643
# along with this program; if not, write to the Free Software
10644
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
10645
10646
module Redmine
10647
  module WikiFormatting
10648
    module Textile
10649
      module Helper
10650
        def wikitoolbar_for(field_id)
10651 441:cbce1fd3b1b7 Chris
          heads_for_wiki_formatter
10652 0:513646585e45 Chris
          # Is there a simple way to link to a public resource?
10653 1464:261b3d9a4903 Chris
          url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax.html"
10654
          javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();")
10655 0:513646585e45 Chris
        end
10656 909:cbb26bc654de Chris
10657 0:513646585e45 Chris
        def initial_page_content(page)
10658
          "h1. #{@page.pretty_title}"
10659
        end
10660 909:cbb26bc654de Chris
10661 0:513646585e45 Chris
        def heads_for_wiki_formatter
10662 441:cbce1fd3b1b7 Chris
          unless @heads_for_wiki_formatter_included
10663
            content_for :header_tags do
10664 1115:433d4f72a19b Chris
              javascript_include_tag('jstoolbar/jstoolbar-textile.min') +
10665 441:cbce1fd3b1b7 Chris
              javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
10666
              stylesheet_link_tag('jstoolbar')
10667
            end
10668
            @heads_for_wiki_formatter_included = true
10669
          end
10670 0:513646585e45 Chris
        end
10671
      end
10672
    end
10673
  end
10674
end