##// END OF EJS Templates
Check permission of wiki pages before generating a link to it (#23793)....
Jean-Philippe Lang -
r15901:19d2529faa25
parent child
Show More
@@ -1,1446 +1,1446
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31 include Redmine::Helpers::URL
32 32
33 33 extend Forwardable
34 34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
35 35
36 36 # Return true if user is authorized for controller/action, otherwise false
37 37 def authorize_for(controller, action)
38 38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
39 39 end
40 40
41 41 # Display a link if user is authorized
42 42 #
43 43 # @param [String] name Anchor text (passed to link_to)
44 44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
45 45 # @param [optional, Hash] html_options Options passed to link_to
46 46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
47 47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
48 48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
49 49 end
50 50
51 51 # Displays a link to user's account page if active
52 52 def link_to_user(user, options={})
53 53 if user.is_a?(User)
54 54 name = h(user.name(options[:format]))
55 55 if user.active? || (User.current.admin? && user.logged?)
56 56 link_to name, user_path(user), :class => user.css_classes
57 57 else
58 58 name
59 59 end
60 60 else
61 61 h(user.to_s)
62 62 end
63 63 end
64 64
65 65 # Displays a link to +issue+ with its subject.
66 66 # Examples:
67 67 #
68 68 # link_to_issue(issue) # => Defect #6: This is the subject
69 69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 70 # link_to_issue(issue, :subject => false) # => Defect #6
71 71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
78 78 if options[:subject] == false
79 79 title = issue.subject.truncate(60)
80 80 else
81 81 subject = issue.subject
82 82 if truncate_length = options[:truncate]
83 83 subject = subject.truncate(truncate_length)
84 84 end
85 85 end
86 86 only_path = options[:only_path].nil? ? true : options[:only_path]
87 87 s = link_to(text, issue_url(issue, :only_path => only_path),
88 88 :class => issue.css_classes, :title => title)
89 89 s << h(": #{subject}") if subject
90 90 s = h("#{issue.project} - ") + s if options[:project]
91 91 s
92 92 end
93 93
94 94 # Generates a link to an attachment.
95 95 # Options:
96 96 # * :text - Link text (default to attachment filename)
97 97 # * :download - Force download (default: false)
98 98 def link_to_attachment(attachment, options={})
99 99 text = options.delete(:text) || attachment.filename
100 100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101 101 html_options = options.slice!(:only_path)
102 102 options[:only_path] = true unless options.key?(:only_path)
103 103 url = send(route_method, attachment, attachment.filename, options)
104 104 link_to text, url, html_options
105 105 end
106 106
107 107 # Generates a link to a SCM revision
108 108 # Options:
109 109 # * :text - Link text (default to the formatted revision)
110 110 def link_to_revision(revision, repository, options={})
111 111 if repository.is_a?(Project)
112 112 repository = repository.repository
113 113 end
114 114 text = options.delete(:text) || format_revision(revision)
115 115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
116 116 link_to(
117 117 h(text),
118 118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
119 119 :title => l(:label_revision_id, format_revision(revision)),
120 120 :accesskey => options[:accesskey]
121 121 )
122 122 end
123 123
124 124 # Generates a link to a message
125 125 def link_to_message(message, options={}, html_options = nil)
126 126 link_to(
127 127 message.subject.truncate(60),
128 128 board_message_url(message.board_id, message.parent_id || message.id, {
129 129 :r => (message.parent_id && message.id),
130 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
131 131 :only_path => true
132 132 }.merge(options)),
133 133 html_options
134 134 )
135 135 end
136 136
137 137 # Generates a link to a project if active
138 138 # Examples:
139 139 #
140 140 # link_to_project(project) # => link to the specified project overview
141 141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 143 #
144 144 def link_to_project(project, options={}, html_options = nil)
145 145 if project.archived?
146 146 h(project.name)
147 147 else
148 148 link_to project.name,
149 149 project_url(project, {:only_path => true}.merge(options)),
150 150 html_options
151 151 end
152 152 end
153 153
154 154 # Generates a link to a project settings if active
155 155 def link_to_project_settings(project, options={}, html_options=nil)
156 156 if project.active?
157 157 link_to project.name, settings_project_path(project, options), html_options
158 158 elsif project.archived?
159 159 h(project.name)
160 160 else
161 161 link_to project.name, project_path(project, options), html_options
162 162 end
163 163 end
164 164
165 165 # Generates a link to a version
166 166 def link_to_version(version, options = {})
167 167 return '' unless version && version.is_a?(Version)
168 168 options = {:title => format_date(version.effective_date)}.merge(options)
169 169 link_to_if version.visible?, format_version_name(version), version_path(version), options
170 170 end
171 171
172 172 # Helper that formats object for html or text rendering
173 173 def format_object(object, html=true, &block)
174 174 if block_given?
175 175 object = yield object
176 176 end
177 177 case object.class.name
178 178 when 'Array'
179 179 object.map {|o| format_object(o, html)}.join(', ').html_safe
180 180 when 'Time'
181 181 format_time(object)
182 182 when 'Date'
183 183 format_date(object)
184 184 when 'Fixnum'
185 185 object.to_s
186 186 when 'Float'
187 187 sprintf "%.2f", object
188 188 when 'User'
189 189 html ? link_to_user(object) : object.to_s
190 190 when 'Project'
191 191 html ? link_to_project(object) : object.to_s
192 192 when 'Version'
193 193 html ? link_to_version(object) : object.to_s
194 194 when 'TrueClass'
195 195 l(:general_text_Yes)
196 196 when 'FalseClass'
197 197 l(:general_text_No)
198 198 when 'Issue'
199 199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 200 when 'Attachment'
201 201 html ? link_to_attachment(object, :download => true) : object.filename
202 202 when 'CustomValue', 'CustomFieldValue'
203 203 if object.custom_field
204 204 f = object.custom_field.format.formatted_custom_value(self, object, html)
205 205 if f.nil? || f.is_a?(String)
206 206 f
207 207 else
208 208 format_object(f, html, &block)
209 209 end
210 210 else
211 211 object.value.to_s
212 212 end
213 213 else
214 214 html ? h(object) : object.to_s
215 215 end
216 216 end
217 217
218 218 def wiki_page_path(page, options={})
219 219 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
220 220 end
221 221
222 222 def thumbnail_tag(attachment)
223 223 link_to image_tag(thumbnail_path(attachment)),
224 224 named_attachment_path(attachment, attachment.filename),
225 225 :title => attachment.filename
226 226 end
227 227
228 228 def toggle_link(name, id, options={})
229 229 onclick = "$('##{id}').toggle(); "
230 230 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
231 231 onclick << "return false;"
232 232 link_to(name, "#", :onclick => onclick)
233 233 end
234 234
235 235 # Used to format item titles on the activity view
236 236 def format_activity_title(text)
237 237 text
238 238 end
239 239
240 240 def format_activity_day(date)
241 241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 242 end
243 243
244 244 def format_activity_description(text)
245 245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 247 end
248 248
249 249 def format_version_name(version)
250 250 if version.project == @project
251 251 h(version)
252 252 else
253 253 h("#{version.project} - #{version}")
254 254 end
255 255 end
256 256
257 257 def format_changeset_comments(changeset, options={})
258 258 method = options[:short] ? :short_comments : :comments
259 259 textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
260 260 end
261 261
262 262 def due_date_distance_in_words(date)
263 263 if date
264 264 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
265 265 end
266 266 end
267 267
268 268 # Renders a tree of projects as a nested set of unordered lists
269 269 # The given collection may be a subset of the whole project tree
270 270 # (eg. some intermediate nodes are private and can not be seen)
271 271 def render_project_nested_lists(projects, &block)
272 272 s = ''
273 273 if projects.any?
274 274 ancestors = []
275 275 original_project = @project
276 276 projects.sort_by(&:lft).each do |project|
277 277 # set the project environment to please macros.
278 278 @project = project
279 279 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
280 280 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
281 281 else
282 282 ancestors.pop
283 283 s << "</li>"
284 284 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
285 285 ancestors.pop
286 286 s << "</ul></li>\n"
287 287 end
288 288 end
289 289 classes = (ancestors.empty? ? 'root' : 'child')
290 290 s << "<li class='#{classes}'><div class='#{classes}'>"
291 291 s << h(block_given? ? capture(project, &block) : project.name)
292 292 s << "</div>\n"
293 293 ancestors << project
294 294 end
295 295 s << ("</li></ul>\n" * ancestors.size)
296 296 @project = original_project
297 297 end
298 298 s.html_safe
299 299 end
300 300
301 301 def render_page_hierarchy(pages, node=nil, options={})
302 302 content = ''
303 303 if pages[node]
304 304 content << "<ul class=\"pages-hierarchy\">\n"
305 305 pages[node].each do |page|
306 306 content << "<li>"
307 307 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
308 308 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
309 309 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
310 310 content << "</li>\n"
311 311 end
312 312 content << "</ul>\n"
313 313 end
314 314 content.html_safe
315 315 end
316 316
317 317 # Renders flash messages
318 318 def render_flash_messages
319 319 s = ''
320 320 flash.each do |k,v|
321 321 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
322 322 end
323 323 s.html_safe
324 324 end
325 325
326 326 # Renders tabs and their content
327 327 def render_tabs(tabs, selected=params[:tab])
328 328 if tabs.any?
329 329 unless tabs.detect {|tab| tab[:name] == selected}
330 330 selected = nil
331 331 end
332 332 selected ||= tabs.first[:name]
333 333 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
334 334 else
335 335 content_tag 'p', l(:label_no_data), :class => "nodata"
336 336 end
337 337 end
338 338
339 339 # Returns an array of projects that are displayed in the quick-jump box
340 340 def projects_for_jump_box(user=User.current)
341 341 if user.logged?
342 342 user.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
343 343 else
344 344 []
345 345 end
346 346 end
347 347
348 348 def render_projects_for_jump_box(projects, selected=nil)
349 349 s = ''.html_safe
350 350 project_tree(projects) do |project, level|
351 351 padding = level * 16
352 352 text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
353 353 s << link_to(text, project_path(project, :jump => current_menu_item), :title => project.name, :class => (project == selected ? 'selected' : nil))
354 354 end
355 355 s
356 356 end
357 357
358 358 # Renders the project quick-jump box
359 359 def render_project_jump_box
360 360 projects = projects_for_jump_box(User.current)
361 361 text = @project.try(:name) || l(:label_jump_to_a_project)
362 362 trigger = content_tag('span', text, :class => 'drdn-trigger')
363 363 q = text_field_tag('q', '', :id => 'projects-quick-search', :class => 'autocomplete', :data => {:automcomplete_url => projects_path(:format => 'js')})
364 364 all = link_to(l(:label_project_all), projects_path(:jump => current_menu_item), :class => (@project.nil? && controller.class.main_menu ? 'selected' : nil))
365 365 content = content_tag('div',
366 366 content_tag('div', q, :class => 'quick-search') +
367 367 content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items projects selection') +
368 368 content_tag('div', all, :class => 'drdn-items all-projects selection'),
369 369 :class => 'drdn-content'
370 370 )
371 371
372 372 content_tag('span', trigger + content, :id => "project-jump", :class => "drdn")
373 373 end
374 374
375 375 def project_tree_options_for_select(projects, options = {})
376 376 s = ''.html_safe
377 377 if blank_text = options[:include_blank]
378 378 if blank_text == true
379 379 blank_text = '&nbsp;'.html_safe
380 380 end
381 381 s << content_tag('option', blank_text, :value => '')
382 382 end
383 383 project_tree(projects) do |project, level|
384 384 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
385 385 tag_options = {:value => project.id}
386 386 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
387 387 tag_options[:selected] = 'selected'
388 388 else
389 389 tag_options[:selected] = nil
390 390 end
391 391 tag_options.merge!(yield(project)) if block_given?
392 392 s << content_tag('option', name_prefix + h(project), tag_options)
393 393 end
394 394 s.html_safe
395 395 end
396 396
397 397 # Yields the given block for each project with its level in the tree
398 398 #
399 399 # Wrapper for Project#project_tree
400 400 def project_tree(projects, options={}, &block)
401 401 Project.project_tree(projects, options, &block)
402 402 end
403 403
404 404 def principals_check_box_tags(name, principals)
405 405 s = ''
406 406 principals.each do |principal|
407 407 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
408 408 end
409 409 s.html_safe
410 410 end
411 411
412 412 # Returns a string for users/groups option tags
413 413 def principals_options_for_select(collection, selected=nil)
414 414 s = ''
415 415 if collection.include?(User.current)
416 416 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
417 417 end
418 418 groups = ''
419 419 collection.sort.each do |element|
420 420 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
421 421 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
422 422 end
423 423 unless groups.empty?
424 424 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
425 425 end
426 426 s.html_safe
427 427 end
428 428
429 429 def option_tag(name, text, value, selected=nil, options={})
430 430 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
431 431 end
432 432
433 433 def truncate_single_line_raw(string, length)
434 434 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
435 435 end
436 436
437 437 # Truncates at line break after 250 characters or options[:length]
438 438 def truncate_lines(string, options={})
439 439 length = options[:length] || 250
440 440 if string.to_s =~ /\A(.{#{length}}.*?)$/m
441 441 "#{$1}..."
442 442 else
443 443 string
444 444 end
445 445 end
446 446
447 447 def anchor(text)
448 448 text.to_s.gsub(' ', '_')
449 449 end
450 450
451 451 def html_hours(text)
452 452 text.gsub(%r{(\d+)([\.:])(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">\2\3</span>').html_safe
453 453 end
454 454
455 455 def authoring(created, author, options={})
456 456 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
457 457 end
458 458
459 459 def time_tag(time)
460 460 text = distance_of_time_in_words(Time.now, time)
461 461 if @project
462 462 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
463 463 else
464 464 content_tag('abbr', text, :title => format_time(time))
465 465 end
466 466 end
467 467
468 468 def syntax_highlight_lines(name, content)
469 469 syntax_highlight(name, content).each_line.to_a
470 470 end
471 471
472 472 def syntax_highlight(name, content)
473 473 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
474 474 end
475 475
476 476 def to_path_param(path)
477 477 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
478 478 str.blank? ? nil : str
479 479 end
480 480
481 481 def reorder_links(name, url, method = :post)
482 482 # TODO: remove associated styles from application.css too
483 483 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
484 484
485 485 link_to(l(:label_sort_highest),
486 486 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
487 487 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
488 488 link_to(l(:label_sort_higher),
489 489 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
490 490 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
491 491 link_to(l(:label_sort_lower),
492 492 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
493 493 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
494 494 link_to(l(:label_sort_lowest),
495 495 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
496 496 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
497 497 end
498 498
499 499 def reorder_handle(object, options={})
500 500 data = {
501 501 :reorder_url => options[:url] || url_for(object),
502 502 :reorder_param => options[:param] || object.class.name.underscore
503 503 }
504 504 content_tag('span', '',
505 505 :class => "sort-handle",
506 506 :data => data,
507 507 :title => l(:button_sort))
508 508 end
509 509
510 510 def breadcrumb(*args)
511 511 elements = args.flatten
512 512 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
513 513 end
514 514
515 515 def other_formats_links(&block)
516 516 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
517 517 yield Redmine::Views::OtherFormatsBuilder.new(self)
518 518 concat('</p>'.html_safe)
519 519 end
520 520
521 521 def page_header_title
522 522 if @project.nil? || @project.new_record?
523 523 h(Setting.app_title)
524 524 else
525 525 b = []
526 526 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
527 527 if ancestors.any?
528 528 root = ancestors.shift
529 529 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
530 530 if ancestors.size > 2
531 531 b << "\xe2\x80\xa6"
532 532 ancestors = ancestors[-2, 2]
533 533 end
534 534 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
535 535 end
536 536 b << content_tag(:span, h(@project), class: 'current-project')
537 537 if b.size > 1
538 538 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
539 539 path = safe_join(b[0..-2], separator) + separator
540 540 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
541 541 end
542 542 safe_join b
543 543 end
544 544 end
545 545
546 546 # Returns a h2 tag and sets the html title with the given arguments
547 547 def title(*args)
548 548 strings = args.map do |arg|
549 549 if arg.is_a?(Array) && arg.size >= 2
550 550 link_to(*arg)
551 551 else
552 552 h(arg.to_s)
553 553 end
554 554 end
555 555 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
556 556 content_tag('h2', strings.join(' &#187; ').html_safe)
557 557 end
558 558
559 559 # Sets the html title
560 560 # Returns the html title when called without arguments
561 561 # Current project name and app_title and automatically appended
562 562 # Exemples:
563 563 # html_title 'Foo', 'Bar'
564 564 # html_title # => 'Foo - Bar - My Project - Redmine'
565 565 def html_title(*args)
566 566 if args.empty?
567 567 title = @html_title || []
568 568 title << @project.name if @project
569 569 title << Setting.app_title unless Setting.app_title == title.last
570 570 title.reject(&:blank?).join(' - ')
571 571 else
572 572 @html_title ||= []
573 573 @html_title += args
574 574 end
575 575 end
576 576
577 577 # Returns the theme, controller name, and action as css classes for the
578 578 # HTML body.
579 579 def body_css_classes
580 580 css = []
581 581 if theme = Redmine::Themes.theme(Setting.ui_theme)
582 582 css << 'theme-' + theme.name
583 583 end
584 584
585 585 css << 'project-' + @project.identifier if @project && @project.identifier.present?
586 586 css << 'controller-' + controller_name
587 587 css << 'action-' + action_name
588 588 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
589 589 css << "textarea-#{User.current.pref.textarea_font}"
590 590 end
591 591 css.join(' ')
592 592 end
593 593
594 594 def accesskey(s)
595 595 @used_accesskeys ||= []
596 596 key = Redmine::AccessKeys.key_for(s)
597 597 return nil if @used_accesskeys.include?(key)
598 598 @used_accesskeys << key
599 599 key
600 600 end
601 601
602 602 # Formats text according to system settings.
603 603 # 2 ways to call this method:
604 604 # * with a String: textilizable(text, options)
605 605 # * with an object and one of its attribute: textilizable(issue, :description, options)
606 606 def textilizable(*args)
607 607 options = args.last.is_a?(Hash) ? args.pop : {}
608 608 case args.size
609 609 when 1
610 610 obj = options[:object]
611 611 text = args.shift
612 612 when 2
613 613 obj = args.shift
614 614 attr = args.shift
615 615 text = obj.send(attr).to_s
616 616 else
617 617 raise ArgumentError, 'invalid arguments to textilizable'
618 618 end
619 619 return '' if text.blank?
620 620 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
621 621 @only_path = only_path = options.delete(:only_path) == false ? false : true
622 622
623 623 text = text.dup
624 624 macros = catch_macros(text)
625 625
626 626 if options[:formatting] == false
627 627 text = h(text)
628 628 else
629 629 formatting = options[:formatting] || Setting.text_formatting
630 630 text = Redmine::WikiFormatting.to_html(formatting, text, :object => obj, :attribute => attr)
631 631 end
632 632
633 633 @parsed_headings = []
634 634 @heading_anchors = {}
635 635 @current_section = 0 if options[:edit_section_links]
636 636
637 637 parse_sections(text, project, obj, attr, only_path, options)
638 638 text = parse_non_pre_blocks(text, obj, macros) do |text|
639 639 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
640 640 send method_name, text, project, obj, attr, only_path, options
641 641 end
642 642 end
643 643 parse_headings(text, project, obj, attr, only_path, options)
644 644
645 645 if @parsed_headings.any?
646 646 replace_toc(text, @parsed_headings)
647 647 end
648 648
649 649 text.html_safe
650 650 end
651 651
652 652 def parse_non_pre_blocks(text, obj, macros)
653 653 s = StringScanner.new(text)
654 654 tags = []
655 655 parsed = ''
656 656 while !s.eos?
657 657 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
658 658 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
659 659 if tags.empty?
660 660 yield text
661 661 inject_macros(text, obj, macros) if macros.any?
662 662 else
663 663 inject_macros(text, obj, macros, false) if macros.any?
664 664 end
665 665 parsed << text
666 666 if tag
667 667 if closing
668 668 if tags.last && tags.last.casecmp(tag) == 0
669 669 tags.pop
670 670 end
671 671 else
672 672 tags << tag.downcase
673 673 end
674 674 parsed << full_tag
675 675 end
676 676 end
677 677 # Close any non closing tags
678 678 while tag = tags.pop
679 679 parsed << "</#{tag}>"
680 680 end
681 681 parsed
682 682 end
683 683
684 684 def parse_inline_attachments(text, project, obj, attr, only_path, options)
685 685 return if options[:inline_attachments] == false
686 686
687 687 # when using an image link, try to use an attachment, if possible
688 688 attachments = options[:attachments] || []
689 689 attachments += obj.attachments if obj.respond_to?(:attachments)
690 690 if attachments.present?
691 691 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
692 692 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
693 693 # search for the picture in attachments
694 694 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
695 695 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
696 696 desc = found.description.to_s.gsub('"', '')
697 697 if !desc.blank? && alttext.blank?
698 698 alt = " title=\"#{desc}\" alt=\"#{desc}\""
699 699 end
700 700 "src=\"#{image_url}\"#{alt}"
701 701 else
702 702 m
703 703 end
704 704 end
705 705 end
706 706 end
707 707
708 708 # Wiki links
709 709 #
710 710 # Examples:
711 711 # [[mypage]]
712 712 # [[mypage|mytext]]
713 713 # wiki links can refer other project wikis, using project name or identifier:
714 714 # [[project:]] -> wiki starting page
715 715 # [[project:|mytext]]
716 716 # [[project:mypage]]
717 717 # [[project:mypage|mytext]]
718 718 def parse_wiki_links(text, project, obj, attr, only_path, options)
719 719 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
720 720 link_project = project
721 721 esc, all, page, title = $1, $2, $3, $5
722 722 if esc.nil?
723 723 if page =~ /^([^\:]+)\:(.*)$/
724 724 identifier, page = $1, $2
725 725 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
726 726 title ||= identifier if page.blank?
727 727 end
728 728
729 if link_project && link_project.wiki
729 if link_project && link_project.wiki && User.current.allowed_to?(:view_wiki_pages, link_project)
730 730 # extract anchor
731 731 anchor = nil
732 732 if page =~ /^(.+?)\#(.+)$/
733 733 page, anchor = $1, $2
734 734 end
735 735 anchor = sanitize_anchor_name(anchor) if anchor.present?
736 736 # check if page exists
737 737 wiki_page = link_project.wiki.find_page(page)
738 738 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
739 739 "##{anchor}"
740 740 else
741 741 case options[:wiki_links]
742 742 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
743 743 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
744 744 else
745 745 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
746 746 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
747 747 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
748 748 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
749 749 end
750 750 end
751 751 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
752 752 else
753 753 # project or wiki doesn't exist
754 754 all
755 755 end
756 756 else
757 757 all
758 758 end
759 759 end
760 760 end
761 761
762 762 # Redmine links
763 763 #
764 764 # Examples:
765 765 # Issues:
766 766 # #52 -> Link to issue #52
767 767 # Changesets:
768 768 # r52 -> Link to revision 52
769 769 # commit:a85130f -> Link to scmid starting with a85130f
770 770 # Documents:
771 771 # document#17 -> Link to document with id 17
772 772 # document:Greetings -> Link to the document with title "Greetings"
773 773 # document:"Some document" -> Link to the document with title "Some document"
774 774 # Versions:
775 775 # version#3 -> Link to version with id 3
776 776 # version:1.0.0 -> Link to version named "1.0.0"
777 777 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
778 778 # Attachments:
779 779 # attachment:file.zip -> Link to the attachment of the current object named file.zip
780 780 # Source files:
781 781 # source:some/file -> Link to the file located at /some/file in the project's repository
782 782 # source:some/file@52 -> Link to the file's revision 52
783 783 # source:some/file#L120 -> Link to line 120 of the file
784 784 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
785 785 # export:some/file -> Force the download of the file
786 786 # Forum messages:
787 787 # message#1218 -> Link to message with id 1218
788 788 # Projects:
789 789 # project:someproject -> Link to project named "someproject"
790 790 # project#3 -> Link to project with id 3
791 791 #
792 792 # Links can refer other objects from other projects, using project identifier:
793 793 # identifier:r52
794 794 # identifier:document:"Some document"
795 795 # identifier:version:1.0.0
796 796 # identifier:source:some/file
797 797 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
798 798 text.gsub!(LINKS_RE) do |_|
799 799 tag_content = $~[:tag_content]
800 800 leading = $~[:leading]
801 801 esc = $~[:esc]
802 802 project_prefix = $~[:project_prefix]
803 803 project_identifier = $~[:project_identifier]
804 804 prefix = $~[:prefix]
805 805 repo_prefix = $~[:repo_prefix]
806 806 repo_identifier = $~[:repo_identifier]
807 807 sep = $~[:sep1] || $~[:sep2] || $~[:sep3]
808 808 identifier = $~[:identifier1] || $~[:identifier2]
809 809 comment_suffix = $~[:comment_suffix]
810 810 comment_id = $~[:comment_id]
811 811
812 812 if tag_content
813 813 $&
814 814 else
815 815 link = nil
816 816 project = default_project
817 817 if project_identifier
818 818 project = Project.visible.find_by_identifier(project_identifier)
819 819 end
820 820 if esc.nil?
821 821 if prefix.nil? && sep == 'r'
822 822 if project
823 823 repository = nil
824 824 if repo_identifier
825 825 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
826 826 else
827 827 repository = project.repository
828 828 end
829 829 # project.changesets.visible raises an SQL error because of a double join on repositories
830 830 if repository &&
831 831 (changeset = Changeset.visible.
832 832 find_by_repository_id_and_revision(repository.id, identifier))
833 833 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
834 834 {:only_path => only_path, :controller => 'repositories',
835 835 :action => 'revision', :id => project,
836 836 :repository_id => repository.identifier_param,
837 837 :rev => changeset.revision},
838 838 :class => 'changeset',
839 839 :title => truncate_single_line_raw(changeset.comments, 100))
840 840 end
841 841 end
842 842 elsif sep == '#'
843 843 oid = identifier.to_i
844 844 case prefix
845 845 when nil
846 846 if oid.to_s == identifier &&
847 847 issue = Issue.visible.find_by_id(oid)
848 848 anchor = comment_id ? "note-#{comment_id}" : nil
849 849 link = link_to("##{oid}#{comment_suffix}",
850 850 issue_url(issue, :only_path => only_path, :anchor => anchor),
851 851 :class => issue.css_classes,
852 852 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
853 853 end
854 854 when 'document'
855 855 if document = Document.visible.find_by_id(oid)
856 856 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
857 857 end
858 858 when 'version'
859 859 if version = Version.visible.find_by_id(oid)
860 860 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
861 861 end
862 862 when 'message'
863 863 if message = Message.visible.find_by_id(oid)
864 864 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
865 865 end
866 866 when 'forum'
867 867 if board = Board.visible.find_by_id(oid)
868 868 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
869 869 end
870 870 when 'news'
871 871 if news = News.visible.find_by_id(oid)
872 872 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
873 873 end
874 874 when 'project'
875 875 if p = Project.visible.find_by_id(oid)
876 876 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
877 877 end
878 878 end
879 879 elsif sep == ':'
880 880 # removes the double quotes if any
881 881 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
882 882 name = CGI.unescapeHTML(name)
883 883 case prefix
884 884 when 'document'
885 885 if project && document = project.documents.visible.find_by_title(name)
886 886 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
887 887 end
888 888 when 'version'
889 889 if project && version = project.versions.visible.find_by_name(name)
890 890 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
891 891 end
892 892 when 'forum'
893 893 if project && board = project.boards.visible.find_by_name(name)
894 894 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
895 895 end
896 896 when 'news'
897 897 if project && news = project.news.visible.find_by_title(name)
898 898 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
899 899 end
900 900 when 'commit', 'source', 'export'
901 901 if project
902 902 repository = nil
903 903 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
904 904 repo_prefix, repo_identifier, name = $1, $2, $3
905 905 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
906 906 else
907 907 repository = project.repository
908 908 end
909 909 if prefix == 'commit'
910 910 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
911 911 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
912 912 :class => 'changeset',
913 913 :title => truncate_single_line_raw(changeset.comments, 100)
914 914 end
915 915 else
916 916 if repository && User.current.allowed_to?(:browse_repository, project)
917 917 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
918 918 path, rev, anchor = $1, $3, $5
919 919 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
920 920 :path => to_path_param(path),
921 921 :rev => rev,
922 922 :anchor => anchor},
923 923 :class => (prefix == 'export' ? 'source download' : 'source')
924 924 end
925 925 end
926 926 repo_prefix = nil
927 927 end
928 928 when 'attachment'
929 929 attachments = options[:attachments] || []
930 930 attachments += obj.attachments if obj.respond_to?(:attachments)
931 931 if attachments && attachment = Attachment.latest_attach(attachments, name)
932 932 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
933 933 end
934 934 when 'project'
935 935 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
936 936 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
937 937 end
938 938 end
939 939 end
940 940 end
941 941 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
942 942 end
943 943 end
944 944 end
945 945
946 946 LINKS_RE =
947 947 %r{
948 948 <a( [^>]+?)?>(?<tag_content>.*?)</a>|
949 949 (?<leading>[\s\(,\-\[\>]|^)
950 950 (?<esc>!)?
951 951 (?<project_prefix>(?<project_identifier>[a-z0-9\-_]+):)?
952 952 (?<prefix>attachment|document|version|forum|news|message|project|commit|source|export)?
953 953 (
954 954 (
955 955 (?<sep1>\#)|
956 956 (
957 957 (?<repo_prefix>(?<repo_identifier>[a-z0-9\-_]+)\|)?
958 958 (?<sep2>r)
959 959 )
960 960 )
961 961 (
962 962 (?<identifier1>\d+)
963 963 (?<comment_suffix>
964 964 (\#note)?
965 965 -(?<comment_id>\d+)
966 966 )?
967 967 )|
968 968 (?<sep3>:)
969 969 (?<identifier2>[^"\s<>][^\s<>]*?|"[^"]+?")
970 970 )
971 971 (?=
972 972 (?=[[:punct:]][^A-Za-z0-9_/])|
973 973 ,|
974 974 \s|
975 975 \]|
976 976 <|
977 977 $)
978 978 }x
979 979 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
980 980
981 981 def parse_sections(text, project, obj, attr, only_path, options)
982 982 return unless options[:edit_section_links]
983 983 text.gsub!(HEADING_RE) do
984 984 heading, level = $1, $2
985 985 @current_section += 1
986 986 if @current_section > 1
987 987 content_tag('div',
988 988 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
989 989 :class => 'icon-only icon-edit'),
990 990 :class => "contextual heading-#{level}",
991 991 :title => l(:button_edit_section),
992 992 :id => "section-#{@current_section}") + heading.html_safe
993 993 else
994 994 heading
995 995 end
996 996 end
997 997 end
998 998
999 999 # Headings and TOC
1000 1000 # Adds ids and links to headings unless options[:headings] is set to false
1001 1001 def parse_headings(text, project, obj, attr, only_path, options)
1002 1002 return if options[:headings] == false
1003 1003
1004 1004 text.gsub!(HEADING_RE) do
1005 1005 level, attrs, content = $2.to_i, $3, $4
1006 1006 item = strip_tags(content).strip
1007 1007 anchor = sanitize_anchor_name(item)
1008 1008 # used for single-file wiki export
1009 1009 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
1010 1010 @heading_anchors[anchor] ||= 0
1011 1011 idx = (@heading_anchors[anchor] += 1)
1012 1012 if idx > 1
1013 1013 anchor = "#{anchor}-#{idx}"
1014 1014 end
1015 1015 @parsed_headings << [level, anchor, item]
1016 1016 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
1017 1017 end
1018 1018 end
1019 1019
1020 1020 MACROS_RE = /(
1021 1021 (!)? # escaping
1022 1022 (
1023 1023 \{\{ # opening tag
1024 1024 ([\w]+) # macro name
1025 1025 (\(([^\n\r]*?)\))? # optional arguments
1026 1026 ([\n\r].*?[\n\r])? # optional block of text
1027 1027 \}\} # closing tag
1028 1028 )
1029 1029 )/mx unless const_defined?(:MACROS_RE)
1030 1030
1031 1031 MACRO_SUB_RE = /(
1032 1032 \{\{
1033 1033 macro\((\d+)\)
1034 1034 \}\}
1035 1035 )/x unless const_defined?(:MACRO_SUB_RE)
1036 1036
1037 1037 # Extracts macros from text
1038 1038 def catch_macros(text)
1039 1039 macros = {}
1040 1040 text.gsub!(MACROS_RE) do
1041 1041 all, macro = $1, $4.downcase
1042 1042 if macro_exists?(macro) || all =~ MACRO_SUB_RE
1043 1043 index = macros.size
1044 1044 macros[index] = all
1045 1045 "{{macro(#{index})}}"
1046 1046 else
1047 1047 all
1048 1048 end
1049 1049 end
1050 1050 macros
1051 1051 end
1052 1052
1053 1053 # Executes and replaces macros in text
1054 1054 def inject_macros(text, obj, macros, execute=true)
1055 1055 text.gsub!(MACRO_SUB_RE) do
1056 1056 all, index = $1, $2.to_i
1057 1057 orig = macros.delete(index)
1058 1058 if execute && orig && orig =~ MACROS_RE
1059 1059 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
1060 1060 if esc.nil?
1061 1061 h(exec_macro(macro, obj, args, block) || all)
1062 1062 else
1063 1063 h(all)
1064 1064 end
1065 1065 elsif orig
1066 1066 h(orig)
1067 1067 else
1068 1068 h(all)
1069 1069 end
1070 1070 end
1071 1071 end
1072 1072
1073 1073 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1074 1074
1075 1075 # Renders the TOC with given headings
1076 1076 def replace_toc(text, headings)
1077 1077 text.gsub!(TOC_RE) do
1078 1078 left_align, right_align = $2, $3
1079 1079 # Keep only the 4 first levels
1080 1080 headings = headings.select{|level, anchor, item| level <= 4}
1081 1081 if headings.empty?
1082 1082 ''
1083 1083 else
1084 1084 div_class = 'toc'
1085 1085 div_class << ' right' if right_align
1086 1086 div_class << ' left' if left_align
1087 1087 out = "<ul class=\"#{div_class}\"><li><strong>#{l :label_table_of_contents}</strong></li><li>"
1088 1088 root = headings.map(&:first).min
1089 1089 current = root
1090 1090 started = false
1091 1091 headings.each do |level, anchor, item|
1092 1092 if level > current
1093 1093 out << '<ul><li>' * (level - current)
1094 1094 elsif level < current
1095 1095 out << "</li></ul>\n" * (current - level) + "</li><li>"
1096 1096 elsif started
1097 1097 out << '</li><li>'
1098 1098 end
1099 1099 out << "<a href=\"##{anchor}\">#{item}</a>"
1100 1100 current = level
1101 1101 started = true
1102 1102 end
1103 1103 out << '</li></ul>' * (current - root)
1104 1104 out << '</li></ul>'
1105 1105 end
1106 1106 end
1107 1107 end
1108 1108
1109 1109 # Same as Rails' simple_format helper without using paragraphs
1110 1110 def simple_format_without_paragraph(text)
1111 1111 text.to_s.
1112 1112 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1113 1113 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1114 1114 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1115 1115 html_safe
1116 1116 end
1117 1117
1118 1118 def lang_options_for_select(blank=true)
1119 1119 (blank ? [["(auto)", ""]] : []) + languages_options
1120 1120 end
1121 1121
1122 1122 def labelled_form_for(*args, &proc)
1123 1123 args << {} unless args.last.is_a?(Hash)
1124 1124 options = args.last
1125 1125 if args.first.is_a?(Symbol)
1126 1126 options.merge!(:as => args.shift)
1127 1127 end
1128 1128 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1129 1129 form_for(*args, &proc)
1130 1130 end
1131 1131
1132 1132 def labelled_fields_for(*args, &proc)
1133 1133 args << {} unless args.last.is_a?(Hash)
1134 1134 options = args.last
1135 1135 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1136 1136 fields_for(*args, &proc)
1137 1137 end
1138 1138
1139 1139 # Render the error messages for the given objects
1140 1140 def error_messages_for(*objects)
1141 1141 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1142 1142 errors = objects.map {|o| o.errors.full_messages}.flatten
1143 1143 render_error_messages(errors)
1144 1144 end
1145 1145
1146 1146 # Renders a list of error messages
1147 1147 def render_error_messages(errors)
1148 1148 html = ""
1149 1149 if errors.present?
1150 1150 html << "<div id='errorExplanation'><ul>\n"
1151 1151 errors.each do |error|
1152 1152 html << "<li>#{h error}</li>\n"
1153 1153 end
1154 1154 html << "</ul></div>\n"
1155 1155 end
1156 1156 html.html_safe
1157 1157 end
1158 1158
1159 1159 def delete_link(url, options={})
1160 1160 options = {
1161 1161 :method => :delete,
1162 1162 :data => {:confirm => l(:text_are_you_sure)},
1163 1163 :class => 'icon icon-del'
1164 1164 }.merge(options)
1165 1165
1166 1166 link_to l(:button_delete), url, options
1167 1167 end
1168 1168
1169 1169 def preview_link(url, form, target='preview', options={})
1170 1170 content_tag 'a', l(:label_preview), {
1171 1171 :href => "#",
1172 1172 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1173 1173 :accesskey => accesskey(:preview)
1174 1174 }.merge(options)
1175 1175 end
1176 1176
1177 1177 def link_to_function(name, function, html_options={})
1178 1178 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1179 1179 end
1180 1180
1181 1181 # Helper to render JSON in views
1182 1182 def raw_json(arg)
1183 1183 arg.to_json.to_s.gsub('/', '\/').html_safe
1184 1184 end
1185 1185
1186 1186 def back_url
1187 1187 url = params[:back_url]
1188 1188 if url.nil? && referer = request.env['HTTP_REFERER']
1189 1189 url = CGI.unescape(referer.to_s)
1190 1190 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1191 1191 # parsed as invalid by URI.parse so the redirect to the back URL would
1192 1192 # not be accepted (ApplicationController#validate_back_url would return
1193 1193 # false)
1194 1194 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1195 1195 end
1196 1196 url
1197 1197 end
1198 1198
1199 1199 def back_url_hidden_field_tag
1200 1200 url = back_url
1201 1201 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1202 1202 end
1203 1203
1204 1204 def check_all_links(form_name)
1205 1205 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1206 1206 " | ".html_safe +
1207 1207 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1208 1208 end
1209 1209
1210 1210 def toggle_checkboxes_link(selector)
1211 1211 link_to_function '',
1212 1212 "toggleCheckboxesBySelector('#{selector}')",
1213 1213 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1214 1214 :class => 'toggle-checkboxes'
1215 1215 end
1216 1216
1217 1217 def progress_bar(pcts, options={})
1218 1218 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1219 1219 pcts = pcts.collect(&:round)
1220 1220 pcts[1] = pcts[1] - pcts[0]
1221 1221 pcts << (100 - pcts[1] - pcts[0])
1222 1222 titles = options[:titles].to_a
1223 1223 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1224 1224 legend = options[:legend] || ''
1225 1225 content_tag('table',
1226 1226 content_tag('tr',
1227 1227 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1228 1228 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1229 1229 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1230 1230 ), :class => "progress progress-#{pcts[0]}").html_safe +
1231 1231 content_tag('p', legend, :class => 'percent').html_safe
1232 1232 end
1233 1233
1234 1234 def checked_image(checked=true)
1235 1235 if checked
1236 1236 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1237 1237 end
1238 1238 end
1239 1239
1240 1240 def context_menu
1241 1241 unless @context_menu_included
1242 1242 content_for :header_tags do
1243 1243 javascript_include_tag('context_menu') +
1244 1244 stylesheet_link_tag('context_menu')
1245 1245 end
1246 1246 if l(:direction) == 'rtl'
1247 1247 content_for :header_tags do
1248 1248 stylesheet_link_tag('context_menu_rtl')
1249 1249 end
1250 1250 end
1251 1251 @context_menu_included = true
1252 1252 end
1253 1253 nil
1254 1254 end
1255 1255
1256 1256 def calendar_for(field_id)
1257 1257 include_calendar_headers_tags
1258 1258 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1259 1259 end
1260 1260
1261 1261 def include_calendar_headers_tags
1262 1262 unless @calendar_headers_tags_included
1263 1263 tags = ''.html_safe
1264 1264 @calendar_headers_tags_included = true
1265 1265 content_for :header_tags do
1266 1266 start_of_week = Setting.start_of_week
1267 1267 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1268 1268 # Redmine uses 1..7 (monday..sunday) in settings and locales
1269 1269 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1270 1270 start_of_week = start_of_week.to_i % 7
1271 1271 tags << javascript_tag(
1272 1272 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1273 1273 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1274 1274 path_to_image('/images/calendar.png') +
1275 1275 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1276 1276 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1277 1277 "beforeShow: beforeShowDatePicker};")
1278 1278 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1279 1279 unless jquery_locale == 'en'
1280 1280 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1281 1281 end
1282 1282 tags
1283 1283 end
1284 1284 end
1285 1285 end
1286 1286
1287 1287 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1288 1288 # Examples:
1289 1289 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1290 1290 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1291 1291 #
1292 1292 def stylesheet_link_tag(*sources)
1293 1293 options = sources.last.is_a?(Hash) ? sources.pop : {}
1294 1294 plugin = options.delete(:plugin)
1295 1295 sources = sources.map do |source|
1296 1296 if plugin
1297 1297 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1298 1298 elsif current_theme && current_theme.stylesheets.include?(source)
1299 1299 current_theme.stylesheet_path(source)
1300 1300 else
1301 1301 source
1302 1302 end
1303 1303 end
1304 1304 super *sources, options
1305 1305 end
1306 1306
1307 1307 # Overrides Rails' image_tag with themes and plugins support.
1308 1308 # Examples:
1309 1309 # image_tag('image.png') # => picks image.png from the current theme or defaults
1310 1310 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1311 1311 #
1312 1312 def image_tag(source, options={})
1313 1313 if plugin = options.delete(:plugin)
1314 1314 source = "/plugin_assets/#{plugin}/images/#{source}"
1315 1315 elsif current_theme && current_theme.images.include?(source)
1316 1316 source = current_theme.image_path(source)
1317 1317 end
1318 1318 super source, options
1319 1319 end
1320 1320
1321 1321 # Overrides Rails' javascript_include_tag with plugins support
1322 1322 # Examples:
1323 1323 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1324 1324 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1325 1325 #
1326 1326 def javascript_include_tag(*sources)
1327 1327 options = sources.last.is_a?(Hash) ? sources.pop : {}
1328 1328 if plugin = options.delete(:plugin)
1329 1329 sources = sources.map do |source|
1330 1330 if plugin
1331 1331 "/plugin_assets/#{plugin}/javascripts/#{source}"
1332 1332 else
1333 1333 source
1334 1334 end
1335 1335 end
1336 1336 end
1337 1337 super *sources, options
1338 1338 end
1339 1339
1340 1340 def sidebar_content?
1341 1341 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1342 1342 end
1343 1343
1344 1344 def view_layouts_base_sidebar_hook_response
1345 1345 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1346 1346 end
1347 1347
1348 1348 def email_delivery_enabled?
1349 1349 !!ActionMailer::Base.perform_deliveries
1350 1350 end
1351 1351
1352 1352 # Returns the avatar image tag for the given +user+ if avatars are enabled
1353 1353 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1354 1354 def avatar(user, options = { })
1355 1355 if Setting.gravatar_enabled?
1356 1356 options.merge!(:default => Setting.gravatar_default)
1357 1357 email = nil
1358 1358 if user.respond_to?(:mail)
1359 1359 email = user.mail
1360 1360 elsif user.to_s =~ %r{<(.+?)>}
1361 1361 email = $1
1362 1362 end
1363 1363 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1364 1364 else
1365 1365 ''
1366 1366 end
1367 1367 end
1368 1368
1369 1369 # Returns a link to edit user's avatar if avatars are enabled
1370 1370 def avatar_edit_link(user, options={})
1371 1371 if Setting.gravatar_enabled?
1372 1372 url = "https://gravatar.com"
1373 1373 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1374 1374 end
1375 1375 end
1376 1376
1377 1377 def sanitize_anchor_name(anchor)
1378 1378 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1379 1379 end
1380 1380
1381 1381 # Returns the javascript tags that are included in the html layout head
1382 1382 def javascript_heads
1383 1383 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1384 1384 unless User.current.pref.warn_on_leaving_unsaved == '0'
1385 1385 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1386 1386 end
1387 1387 tags
1388 1388 end
1389 1389
1390 1390 def favicon
1391 1391 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1392 1392 end
1393 1393
1394 1394 # Returns the path to the favicon
1395 1395 def favicon_path
1396 1396 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1397 1397 image_path(icon)
1398 1398 end
1399 1399
1400 1400 # Returns the full URL to the favicon
1401 1401 def favicon_url
1402 1402 # TODO: use #image_url introduced in Rails4
1403 1403 path = favicon_path
1404 1404 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1405 1405 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1406 1406 end
1407 1407
1408 1408 def robot_exclusion_tag
1409 1409 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1410 1410 end
1411 1411
1412 1412 # Returns true if arg is expected in the API response
1413 1413 def include_in_api_response?(arg)
1414 1414 unless @included_in_api_response
1415 1415 param = params[:include]
1416 1416 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1417 1417 @included_in_api_response.collect!(&:strip)
1418 1418 end
1419 1419 @included_in_api_response.include?(arg.to_s)
1420 1420 end
1421 1421
1422 1422 # Returns options or nil if nometa param or X-Redmine-Nometa header
1423 1423 # was set in the request
1424 1424 def api_meta(options)
1425 1425 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1426 1426 # compatibility mode for activeresource clients that raise
1427 1427 # an error when deserializing an array with attributes
1428 1428 nil
1429 1429 else
1430 1430 options
1431 1431 end
1432 1432 end
1433 1433
1434 1434 def generate_csv(&block)
1435 1435 decimal_separator = l(:general_csv_decimal_separator)
1436 1436 encoding = l(:general_csv_encoding)
1437 1437 end
1438 1438
1439 1439 private
1440 1440
1441 1441 def wiki_helper
1442 1442 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1443 1443 extend helper
1444 1444 return self
1445 1445 end
1446 1446 end
@@ -1,11 +1,16
1 1 ---
2 2 wikis_001:
3 3 status: 1
4 4 start_page: CookBook documentation
5 5 project_id: 1
6 6 id: 1
7 7 wikis_002:
8 8 status: 1
9 9 start_page: Start page
10 10 project_id: 2
11 11 id: 2
12 wikis_005:
13 status: 1
14 start_page: Wiki
15 project_id: 5
16 id: 5
@@ -1,1575 +1,1579
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../../test_helper', __FILE__)
21 21
22 22 class ApplicationHelperTest < Redmine::HelperTest
23 23 include ERB::Util
24 24 include Rails.application.routes.url_helpers
25 25
26 26 fixtures :projects, :enabled_modules,
27 27 :users, :email_addresses,
28 28 :members, :member_roles, :roles,
29 29 :repositories, :changesets,
30 30 :projects_trackers,
31 31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 32 :wikis, :wiki_pages, :wiki_contents,
33 33 :boards, :messages, :news,
34 34 :attachments, :enumerations
35 35
36 36 def setup
37 37 super
38 38 set_tmp_attachments_directory
39 39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 40 end
41 41
42 42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 43 User.current = User.find_by_login('admin')
44 44
45 45 @project = Issue.first.project # Used by helper
46 46 response = link_to_if_authorized('By controller/actionr',
47 47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 48 assert_match /href/, response
49 49 end
50 50
51 51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 52 User.current = User.find_by_login('dlopper')
53 53 @project = Project.find('private-child')
54 54 issue = @project.issues.first
55 55 assert !issue.visible?
56 56
57 57 response = link_to_if_authorized('Never displayed',
58 58 {:controller => 'issues', :action => 'show', :id => issue})
59 59 assert_nil response
60 60 end
61 61
62 62 def test_auto_links
63 63 to_test = {
64 64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
71 71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
73 73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
74 74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
75 75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
80 80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 86 # two exclamation marks
87 87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
88 88 # escaping
89 89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 90 # wrap in angle brackets
91 91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 92 # invalid urls
93 93 'http://' => 'http://',
94 94 'www.' => 'www.',
95 95 'test-www.bar.com' => 'test-www.bar.com',
96 96 }
97 97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 98 end
99 99
100 100 def test_auto_links_with_non_ascii_characters
101 101 to_test = {
102 102 "http://foo.bar/#{@russian_test}" =>
103 103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
104 104 }
105 105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 106 end
107 107
108 108 def test_auto_mailto
109 109 to_test = {
110 110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 112 }
113 113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 114 end
115 115
116 116 def test_inline_images
117 117 to_test = {
118 118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 119 'floating !>http://foo.bar/image.jpg!' => 'floating <span style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></span>',
120 120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
122 122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
123 123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
124 124 }
125 125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 126 end
127 127
128 128 def test_inline_images_inside_tags
129 129 raw = <<-RAW
130 130 h1. !foo.png! Heading
131 131
132 132 Centered image:
133 133
134 134 p=. !bar.gif!
135 135 RAW
136 136
137 137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 139 end
140 140
141 141 def test_attached_images
142 142 to_test = {
143 143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 147 # link image
148 148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 149 }
150 150 attachments = Attachment.all
151 151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 152 end
153 153
154 154 def test_attached_images_with_textile_and_non_ascii_filename
155 155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 156 with_settings :text_formatting => 'textile' do
157 157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 159 end
160 160 end
161 161
162 162 def test_attached_images_with_markdown_and_non_ascii_filename
163 163 skip unless Object.const_defined?(:Redcarpet)
164 164
165 165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 166 with_settings :text_formatting => 'markdown' do
167 167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
168 168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 169 end
170 170 end
171 171
172 172 def test_attached_images_filename_extension
173 173 set_tmp_attachments_directory
174 174 a1 = Attachment.new(
175 175 :container => Issue.find(1),
176 176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 177 :author => User.find(1))
178 178 assert a1.save
179 179 assert_equal "testtest.JPG", a1.filename
180 180 assert_equal "image/jpeg", a1.content_type
181 181 assert a1.image?
182 182
183 183 a2 = Attachment.new(
184 184 :container => Issue.find(1),
185 185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 186 :author => User.find(1))
187 187 assert a2.save
188 188 assert_equal "testtest.jpeg", a2.filename
189 189 assert_equal "image/jpeg", a2.content_type
190 190 assert a2.image?
191 191
192 192 a3 = Attachment.new(
193 193 :container => Issue.find(1),
194 194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 195 :author => User.find(1))
196 196 assert a3.save
197 197 assert_equal "testtest.JPE", a3.filename
198 198 assert_equal "image/jpeg", a3.content_type
199 199 assert a3.image?
200 200
201 201 a4 = Attachment.new(
202 202 :container => Issue.find(1),
203 203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 204 :author => User.find(1))
205 205 assert a4.save
206 206 assert_equal "Testtest.BMP", a4.filename
207 207 assert_equal "image/x-ms-bmp", a4.content_type
208 208 assert a4.image?
209 209
210 210 to_test = {
211 211 'Inline image: !testtest.jpg!' =>
212 212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 213 'Inline image: !testtest.jpeg!' =>
214 214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 215 'Inline image: !testtest.jpe!' =>
216 216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 217 'Inline image: !testtest.bmp!' =>
218 218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 219 }
220 220
221 221 attachments = [a1, a2, a3, a4]
222 222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 223 end
224 224
225 225 def test_attached_images_should_read_later
226 226 set_fixtures_attachments_directory
227 227 a1 = Attachment.find(16)
228 228 assert_equal "testfile.png", a1.filename
229 229 assert a1.readable?
230 230 assert (! a1.visible?(User.anonymous))
231 231 assert a1.visible?(User.find(2))
232 232 a2 = Attachment.find(17)
233 233 assert_equal "testfile.PNG", a2.filename
234 234 assert a2.readable?
235 235 assert (! a2.visible?(User.anonymous))
236 236 assert a2.visible?(User.find(2))
237 237 assert a1.created_on < a2.created_on
238 238
239 239 to_test = {
240 240 'Inline image: !testfile.png!' =>
241 241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 242 'Inline image: !Testfile.PNG!' =>
243 243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 244 }
245 245 attachments = [a1, a2]
246 246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 247 set_tmp_attachments_directory
248 248 end
249 249
250 250 def test_textile_external_links
251 251 to_test = {
252 252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
256 256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 257 # no multiline link text
258 258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
259 259 # mailto link
260 260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 261 # two exclamation marks
262 262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 263 # escaping
264 264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 265 }
266 266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 267 end
268 268
269 269 def test_textile_external_links_with_non_ascii_characters
270 270 to_test = {
271 271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 273 }
274 274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 275 end
276 276
277 277 def test_redmine_links
278 278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284 284
285 285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289 289
290 290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293 293
294 294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 295 :class => 'document')
296 296
297 297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 298 :class => 'version')
299 299
300 300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301 301
302 302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303 303
304 304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305 305
306 306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307 307
308 308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313 313
314 314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319 319
320 320 to_test = {
321 321 # tickets
322 322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 323 # ticket notes
324 324 '#3-14' => note_link,
325 325 '#3#note-14' => note_link2,
326 326 # should not ignore leading zero
327 327 '#03' => '#03',
328 328 # changesets
329 329 'r1' => revision_link,
330 330 'r1.' => "#{revision_link}.",
331 331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 334 # documents
335 335 'document#1' => document_link,
336 336 'document:"Test document"' => document_link,
337 337 # versions
338 338 'version#2' => version_link,
339 339 'version:1.0' => version_link,
340 340 'version:"1.0"' => version_link,
341 341 # source
342 342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 355 # export
356 356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 361 # forum
362 362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 364 # message
365 365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 367 # news
368 368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 370 # project
371 371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 374 # not found
375 375 '#0123456789' => '#0123456789',
376 376 # invalid expressions
377 377 'source:' => 'source:',
378 378 # url hash
379 379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 380 }
381 381 @project = Project.find(1)
382 382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 383 end
384 384
385 385 def test_should_not_parse_redmine_links_inside_link
386 386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 388 textilizable(raw, :project => Project.find(1))
389 389 end
390 390
391 391 def test_redmine_links_with_a_different_project_before_current_project
392 392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 394 @project = Project.find(3)
395 395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 397 assert_equal "<p>#{result1} #{result2}</p>",
398 398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 399 end
400 400
401 401 def test_escaped_redmine_links_should_not_be_parsed
402 402 to_test = [
403 403 '#3.',
404 404 '#3-14.',
405 405 '#3#-note14.',
406 406 'r1',
407 407 'document#1',
408 408 'document:"Test document"',
409 409 'version#2',
410 410 'version:1.0',
411 411 'version:"1.0"',
412 412 'source:/some/file'
413 413 ]
414 414 @project = Project.find(1)
415 415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 416 end
417 417
418 418 def test_cross_project_redmine_links
419 419 source_link = link_to('ecookbook:source:/some/file',
420 420 {:controller => 'repositories', :action => 'entry',
421 421 :id => 'ecookbook', :path => ['some', 'file']},
422 422 :class => 'source')
423 423 changeset_link = link_to('ecookbook:r2',
424 424 {:controller => 'repositories', :action => 'revision',
425 425 :id => 'ecookbook', :rev => 2},
426 426 :class => 'changeset',
427 427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 428 to_test = {
429 429 # documents
430 430 'document:"Test document"' => 'document:"Test document"',
431 431 'ecookbook:document:"Test document"' =>
432 432 link_to("Test document", "/documents/1", :class => "document"),
433 433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 434 # versions
435 435 'version:"1.0"' => 'version:"1.0"',
436 436 'ecookbook:version:"1.0"' =>
437 437 link_to("1.0", "/versions/2", :class => "version"),
438 438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 439 # changeset
440 440 'r2' => 'r2',
441 441 'ecookbook:r2' => changeset_link,
442 442 'invalid:r2' => 'invalid:r2',
443 443 # source
444 444 'source:/some/file' => 'source:/some/file',
445 445 'ecookbook:source:/some/file' => source_link,
446 446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 447 }
448 448 @project = Project.find(3)
449 449 to_test.each do |text, result|
450 450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 451 end
452 452 end
453 453
454 454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457 457
458 458 @project = v.project
459 459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 460 end
461 461
462 462 def test_link_to_issue_subject
463 463 issue = Issue.generate!(:subject => "01234567890123456789")
464 464 str = link_to_issue(issue, :truncate => 10)
465 465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 466 assert_equal "#{result}: 0123456...", str
467 467
468 468 issue = Issue.generate!(:subject => "<&>")
469 469 str = link_to_issue(issue)
470 470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 471 assert_equal "#{result}: &lt;&amp;&gt;", str
472 472
473 473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 474 str = link_to_issue(issue, :truncate => 10)
475 475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 477 end
478 478
479 479 def test_link_to_issue_title
480 480 long_str = "0123456789" * 5
481 481
482 482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 483 str = link_to_issue(issue, :subject => false)
484 484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 485 :class => issue.css_classes,
486 486 :title => "#{long_str}0123456...")
487 487 assert_equal result, str
488 488
489 489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 490 str = link_to_issue(issue, :subject => false)
491 491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 492 :class => issue.css_classes,
493 493 :title => "<&>#{long_str}0123...")
494 494 assert_equal result, str
495 495 end
496 496
497 497 def test_multiple_repositories_redmine_links
498 498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502 502
503 503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 506 :class => 'changeset', :title => '')
507 507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 508 :class => 'changeset', :title => '')
509 509
510 510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512 512
513 513 to_test = {
514 514 'r2' => changeset_link,
515 515 'svn_repo-1|r123' => svn_changeset_link,
516 516 'invalid|r123' => 'invalid|r123',
517 517 'commit:hg1|abcd' => hg_changeset_link,
518 518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 519 # source
520 520 'source:some/file' => source_link,
521 521 'source:hg1|some/file' => hg_source_link,
522 522 'source:invalid|some/file' => 'source:invalid|some/file',
523 523 }
524 524
525 525 @project = Project.find(1)
526 526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 527 end
528 528
529 529 def test_cross_project_multiple_repositories_redmine_links
530 530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534 534
535 535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 538 :class => 'changeset', :title => '')
539 539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 540 :class => 'changeset', :title => '')
541 541
542 542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544 544
545 545 to_test = {
546 546 'ecookbook:r2' => changeset_link,
547 547 'ecookbook:svn1|r123' => svn_changeset_link,
548 548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 552 # source
553 553 'ecookbook:source:some/file' => source_link,
554 554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 557 }
558 558
559 559 @project = Project.find(3)
560 560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 561 end
562 562
563 563 def test_redmine_links_git_commit
564 564 changeset_link = link_to('abcd',
565 565 {
566 566 :controller => 'repositories',
567 567 :action => 'revision',
568 568 :id => 'subproject1',
569 569 :rev => 'abcd',
570 570 },
571 571 :class => 'changeset', :title => 'test commit')
572 572 to_test = {
573 573 'commit:abcd' => changeset_link,
574 574 }
575 575 @project = Project.find(3)
576 576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 577 assert r
578 578 c = Changeset.new(:repository => r,
579 579 :committed_on => Time.now,
580 580 :revision => 'abcd',
581 581 :scmid => 'abcd',
582 582 :comments => 'test commit')
583 583 assert( c.save )
584 584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 585 end
586 586
587 587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 588 def test_redmine_links_darcs_commit
589 589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 590 {
591 591 :controller => 'repositories',
592 592 :action => 'revision',
593 593 :id => 'subproject1',
594 594 :rev => '123',
595 595 },
596 596 :class => 'changeset', :title => 'test commit')
597 597 to_test = {
598 598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 599 }
600 600 @project = Project.find(3)
601 601 r = Repository::Darcs.create!(
602 602 :project => @project, :url => '/tmp/test/darcs',
603 603 :log_encoding => 'UTF-8')
604 604 assert r
605 605 c = Changeset.new(:repository => r,
606 606 :committed_on => Time.now,
607 607 :revision => '123',
608 608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 609 :comments => 'test commit')
610 610 assert( c.save )
611 611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 612 end
613 613
614 614 def test_redmine_links_mercurial_commit
615 615 changeset_link_rev = link_to('r123',
616 616 {
617 617 :controller => 'repositories',
618 618 :action => 'revision',
619 619 :id => 'subproject1',
620 620 :rev => '123' ,
621 621 },
622 622 :class => 'changeset', :title => 'test commit')
623 623 changeset_link_commit = link_to('abcd',
624 624 {
625 625 :controller => 'repositories',
626 626 :action => 'revision',
627 627 :id => 'subproject1',
628 628 :rev => 'abcd' ,
629 629 },
630 630 :class => 'changeset', :title => 'test commit')
631 631 to_test = {
632 632 'r123' => changeset_link_rev,
633 633 'commit:abcd' => changeset_link_commit,
634 634 }
635 635 @project = Project.find(3)
636 636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 637 assert r
638 638 c = Changeset.new(:repository => r,
639 639 :committed_on => Time.now,
640 640 :revision => '123',
641 641 :scmid => 'abcd',
642 642 :comments => 'test commit')
643 643 assert( c.save )
644 644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 645 end
646 646
647 647 def test_attachment_links
648 648 text = 'attachment:error281.txt'
649 649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 650 :class => "attachment")
651 651 assert_equal "<p>#{result}</p>",
652 652 textilizable(text,
653 653 :attachments => Issue.find(3).attachments),
654 654 "#{text} failed"
655 655 end
656 656
657 657 def test_attachment_link_should_link_to_latest_attachment
658 658 set_tmp_attachments_directory
659 659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 660 a2 = Attachment.generate!(:filename => "test.txt")
661 661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 662 :class => "attachment")
663 663 assert_equal "<p>#{result}</p>",
664 664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 665 end
666 666
667 667 def test_wiki_links
668 User.current = User.find_by_login('jsmith')
668 669 russian_eacape = CGI.escape(@russian_test)
669 670 to_test = {
670 671 '[[CookBook documentation]]' =>
671 672 link_to("CookBook documentation",
672 673 "/projects/ecookbook/wiki/CookBook_documentation",
673 674 :class => "wiki-page"),
674 675 '[[Another page|Page]]' =>
675 676 link_to("Page",
676 677 "/projects/ecookbook/wiki/Another_page",
677 678 :class => "wiki-page"),
678 679 # title content should be formatted
679 680 '[[Another page|With _styled_ *title*]]' =>
680 681 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 682 "/projects/ecookbook/wiki/Another_page",
682 683 :class => "wiki-page"),
683 684 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 685 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 686 "/projects/ecookbook/wiki/Another_page",
686 687 :class => "wiki-page"),
687 688 # link with anchor
688 689 '[[CookBook documentation#One-section]]' =>
689 690 link_to("CookBook documentation",
690 691 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 692 :class => "wiki-page"),
692 693 '[[Another page#anchor|Page]]' =>
693 694 link_to("Page",
694 695 "/projects/ecookbook/wiki/Another_page#anchor",
695 696 :class => "wiki-page"),
696 697 # UTF8 anchor
697 698 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 699 link_to(@russian_test,
699 700 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 701 :class => "wiki-page"),
701 702 # page that doesn't exist
702 703 '[[Unknown page]]' =>
703 704 link_to("Unknown page",
704 705 "/projects/ecookbook/wiki/Unknown_page",
705 706 :class => "wiki-page new"),
706 707 '[[Unknown page|404]]' =>
707 708 link_to("404",
708 709 "/projects/ecookbook/wiki/Unknown_page",
709 710 :class => "wiki-page new"),
710 711 # link to another project wiki
711 712 '[[onlinestore:]]' =>
712 713 link_to("onlinestore",
713 714 "/projects/onlinestore/wiki",
714 715 :class => "wiki-page"),
715 716 '[[onlinestore:|Wiki]]' =>
716 717 link_to("Wiki",
717 718 "/projects/onlinestore/wiki",
718 719 :class => "wiki-page"),
719 720 '[[onlinestore:Start page]]' =>
720 721 link_to("Start page",
721 722 "/projects/onlinestore/wiki/Start_page",
722 723 :class => "wiki-page"),
723 724 '[[onlinestore:Start page|Text]]' =>
724 725 link_to("Text",
725 726 "/projects/onlinestore/wiki/Start_page",
726 727 :class => "wiki-page"),
727 728 '[[onlinestore:Unknown page]]' =>
728 729 link_to("Unknown page",
729 730 "/projects/onlinestore/wiki/Unknown_page",
730 731 :class => "wiki-page new"),
731 732 # struck through link
732 733 '-[[Another page|Page]]-' =>
733 734 "<del>".html_safe +
734 735 link_to("Page",
735 736 "/projects/ecookbook/wiki/Another_page",
736 737 :class => "wiki-page").html_safe +
737 738 "</del>".html_safe,
738 739 '-[[Another page|Page]] link-' =>
739 740 "<del>".html_safe +
740 741 link_to("Page",
741 742 "/projects/ecookbook/wiki/Another_page",
742 743 :class => "wiki-page").html_safe +
743 744 " link</del>".html_safe,
744 745 # escaping
745 746 '![[Another page|Page]]' => '[[Another page|Page]]',
746 747 # project does not exist
747 748 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 749 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
750 # missing permission to view wiki in project
751 '[[private-child:]]' => '[[private-child:]]',
752 '[[private-child:Wiki]]' => '[[private-child:Wiki]]',
749 753 }
750 754 @project = Project.find(1)
751 755 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 756 end
753 757
754 758 def test_wiki_links_within_local_file_generation_context
755 759 to_test = {
756 760 # link to a page
757 761 '[[CookBook documentation]]' =>
758 762 link_to("CookBook documentation", "CookBook_documentation.html",
759 763 :class => "wiki-page"),
760 764 '[[CookBook documentation|documentation]]' =>
761 765 link_to("documentation", "CookBook_documentation.html",
762 766 :class => "wiki-page"),
763 767 '[[CookBook documentation#One-section]]' =>
764 768 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 769 :class => "wiki-page"),
766 770 '[[CookBook documentation#One-section|documentation]]' =>
767 771 link_to("documentation", "CookBook_documentation.html#One-section",
768 772 :class => "wiki-page"),
769 773 # page that doesn't exist
770 774 '[[Unknown page]]' =>
771 775 link_to("Unknown page", "Unknown_page.html",
772 776 :class => "wiki-page new"),
773 777 '[[Unknown page|404]]' =>
774 778 link_to("404", "Unknown_page.html",
775 779 :class => "wiki-page new"),
776 780 '[[Unknown page#anchor]]' =>
777 781 link_to("Unknown page", "Unknown_page.html#anchor",
778 782 :class => "wiki-page new"),
779 783 '[[Unknown page#anchor|404]]' =>
780 784 link_to("404", "Unknown_page.html#anchor",
781 785 :class => "wiki-page new"),
782 786 }
783 787 @project = Project.find(1)
784 788 to_test.each do |text, result|
785 789 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 790 end
787 791 end
788 792
789 793 def test_wiki_links_within_wiki_page_context
790 794 page = WikiPage.find_by_title('Another_page' )
791 795 to_test = {
792 796 '[[CookBook documentation]]' =>
793 797 link_to("CookBook documentation",
794 798 "/projects/ecookbook/wiki/CookBook_documentation",
795 799 :class => "wiki-page"),
796 800 '[[CookBook documentation|documentation]]' =>
797 801 link_to("documentation",
798 802 "/projects/ecookbook/wiki/CookBook_documentation",
799 803 :class => "wiki-page"),
800 804 '[[CookBook documentation#One-section]]' =>
801 805 link_to("CookBook documentation",
802 806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 807 :class => "wiki-page"),
804 808 '[[CookBook documentation#One-section|documentation]]' =>
805 809 link_to("documentation",
806 810 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 811 :class => "wiki-page"),
808 812 # link to the current page
809 813 '[[Another page]]' =>
810 814 link_to("Another page",
811 815 "/projects/ecookbook/wiki/Another_page",
812 816 :class => "wiki-page"),
813 817 '[[Another page|Page]]' =>
814 818 link_to("Page",
815 819 "/projects/ecookbook/wiki/Another_page",
816 820 :class => "wiki-page"),
817 821 '[[Another page#anchor]]' =>
818 822 link_to("Another page",
819 823 "#anchor",
820 824 :class => "wiki-page"),
821 825 '[[Another page#anchor|Page]]' =>
822 826 link_to("Page",
823 827 "#anchor",
824 828 :class => "wiki-page"),
825 829 # page that doesn't exist
826 830 '[[Unknown page]]' =>
827 831 link_to("Unknown page",
828 832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 833 :class => "wiki-page new"),
830 834 '[[Unknown page|404]]' =>
831 835 link_to("404",
832 836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 837 :class => "wiki-page new"),
834 838 '[[Unknown page#anchor]]' =>
835 839 link_to("Unknown page",
836 840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 841 :class => "wiki-page new"),
838 842 '[[Unknown page#anchor|404]]' =>
839 843 link_to("404",
840 844 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 845 :class => "wiki-page new"),
842 846 }
843 847 @project = Project.find(1)
844 848 to_test.each do |text, result|
845 849 assert_equal "<p>#{result}</p>",
846 850 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 851 end
848 852 end
849 853
850 854 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 855 to_test = {
852 856 # link to a page
853 857 '[[CookBook documentation]]' =>
854 858 link_to("CookBook documentation",
855 859 "#CookBook_documentation",
856 860 :class => "wiki-page"),
857 861 '[[CookBook documentation|documentation]]' =>
858 862 link_to("documentation",
859 863 "#CookBook_documentation",
860 864 :class => "wiki-page"),
861 865 '[[CookBook documentation#One-section]]' =>
862 866 link_to("CookBook documentation",
863 867 "#CookBook_documentation_One-section",
864 868 :class => "wiki-page"),
865 869 '[[CookBook documentation#One-section|documentation]]' =>
866 870 link_to("documentation",
867 871 "#CookBook_documentation_One-section",
868 872 :class => "wiki-page"),
869 873 # page that doesn't exist
870 874 '[[Unknown page]]' =>
871 875 link_to("Unknown page",
872 876 "#Unknown_page",
873 877 :class => "wiki-page new"),
874 878 '[[Unknown page|404]]' =>
875 879 link_to("404",
876 880 "#Unknown_page",
877 881 :class => "wiki-page new"),
878 882 '[[Unknown page#anchor]]' =>
879 883 link_to("Unknown page",
880 884 "#Unknown_page_anchor",
881 885 :class => "wiki-page new"),
882 886 '[[Unknown page#anchor|404]]' =>
883 887 link_to("404",
884 888 "#Unknown_page_anchor",
885 889 :class => "wiki-page new"),
886 890 }
887 891 @project = Project.find(1)
888 892 to_test.each do |text, result|
889 893 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 894 end
891 895 end
892 896
893 897 def test_html_tags
894 898 to_test = {
895 899 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 900 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 901 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 902 # do not escape pre/code tags
899 903 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 904 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 905 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 906 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 907 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 908 # remove attributes except class
905 909 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 910 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 911 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 912 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 913 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 914 # xss
911 915 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 916 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 917 }
914 918 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 919 end
916 920
917 921 def test_allowed_html_tags
918 922 to_test = {
919 923 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 924 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 925 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 926 }
923 927 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 928 end
925 929
926 930 def test_pre_tags
927 931 raw = <<-RAW
928 932 Before
929 933
930 934 <pre>
931 935 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 936 </pre>
933 937
934 938 After
935 939 RAW
936 940
937 941 expected = <<-EXPECTED
938 942 <p>Before</p>
939 943 <pre>
940 944 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 945 </pre>
942 946 <p>After</p>
943 947 EXPECTED
944 948
945 949 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 950 end
947 951
948 952 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 953 raw = <<-RAW
950 954 [[CookBook documentation]]
951 955
952 956 #1
953 957
954 958 <pre>
955 959 [[CookBook documentation]]
956 960
957 961 #1
958 962 </pre>
959 963 RAW
960 964
961 965 result1 = link_to("CookBook documentation",
962 966 "/projects/ecookbook/wiki/CookBook_documentation",
963 967 :class => "wiki-page")
964 968 result2 = link_to('#1',
965 969 "/issues/1",
966 970 :class => Issue.find(1).css_classes,
967 971 :title => "Bug: Cannot print recipes (New)")
968 972
969 973 expected = <<-EXPECTED
970 974 <p>#{result1}</p>
971 975 <p>#{result2}</p>
972 976 <pre>
973 977 [[CookBook documentation]]
974 978
975 979 #1
976 980 </pre>
977 981 EXPECTED
978 982
979 983 @project = Project.find(1)
980 984 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 985 end
982 986
983 987 def test_non_closing_pre_blocks_should_be_closed
984 988 raw = <<-RAW
985 989 <pre><code>
986 990 RAW
987 991
988 992 expected = <<-EXPECTED
989 993 <pre><code>
990 994 </code></pre>
991 995 EXPECTED
992 996
993 997 @project = Project.find(1)
994 998 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 999 end
996 1000
997 1001 def test_unbalanced_closing_pre_tag_should_not_error
998 1002 assert_nothing_raised do
999 1003 textilizable("unbalanced</pre>")
1000 1004 end
1001 1005 end
1002 1006
1003 1007 def test_syntax_highlight
1004 1008 raw = <<-RAW
1005 1009 <pre><code class="ruby">
1006 1010 # Some ruby code here
1007 1011 </code></pre>
1008 1012 RAW
1009 1013
1010 1014 expected = <<-EXPECTED
1011 1015 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1012 1016 </code></pre>
1013 1017 EXPECTED
1014 1018
1015 1019 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1016 1020 end
1017 1021
1018 1022 def test_to_path_param
1019 1023 assert_equal 'test1/test2', to_path_param('test1/test2')
1020 1024 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1021 1025 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1022 1026 assert_nil to_path_param('/')
1023 1027 end
1024 1028
1025 1029 def test_wiki_links_in_tables
1026 1030 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1027 1031 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1028 1032 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1029 1033 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1030 1034 result = "<tr><td>#{link1}</td>" +
1031 1035 "<td>#{link2}</td>" +
1032 1036 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1033 1037 @project = Project.find(1)
1034 1038 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1035 1039 end
1036 1040
1037 1041 def test_text_formatting
1038 1042 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1039 1043 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1040 1044 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1041 1045 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1042 1046 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1043 1047 }
1044 1048 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1045 1049 end
1046 1050
1047 1051 def test_wiki_horizontal_rule
1048 1052 assert_equal '<hr />', textilizable('---')
1049 1053 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1050 1054 end
1051 1055
1052 1056 def test_footnotes
1053 1057 raw = <<-RAW
1054 1058 This is some text[1].
1055 1059
1056 1060 fn1. This is the foot note
1057 1061 RAW
1058 1062
1059 1063 expected = <<-EXPECTED
1060 1064 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1061 1065 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1062 1066 EXPECTED
1063 1067
1064 1068 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1065 1069 end
1066 1070
1067 1071 def test_headings
1068 1072 raw = 'h1. Some heading'
1069 1073 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1070 1074
1071 1075 assert_equal expected, textilizable(raw)
1072 1076 end
1073 1077
1074 1078 def test_headings_with_special_chars
1075 1079 # This test makes sure that the generated anchor names match the expected
1076 1080 # ones even if the heading text contains unconventional characters
1077 1081 raw = 'h1. Some heading related to version 0.5'
1078 1082 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1079 1083 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1080 1084
1081 1085 assert_equal expected, textilizable(raw)
1082 1086 end
1083 1087
1084 1088 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1085 1089 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1086 1090 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1087 1091
1088 1092 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1089 1093
1090 1094 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1091 1095 end
1092 1096
1093 1097 def test_table_of_content
1094 1098 set_language_if_valid 'en'
1095 1099
1096 1100 raw = <<-RAW
1097 1101 {{toc}}
1098 1102
1099 1103 h1. Title
1100 1104
1101 1105 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1102 1106
1103 1107 h2. Subtitle with a [[Wiki]] link
1104 1108
1105 1109 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1106 1110
1107 1111 h2. Subtitle with [[Wiki|another Wiki]] link
1108 1112
1109 1113 h2. Subtitle with %{color:red}red text%
1110 1114
1111 1115 <pre>
1112 1116 some code
1113 1117 </pre>
1114 1118
1115 1119 h3. Subtitle with *some* _modifiers_
1116 1120
1117 1121 h3. Subtitle with @inline code@
1118 1122
1119 1123 h1. Another title
1120 1124
1121 1125 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1122 1126
1123 1127 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1124 1128
1125 1129 RAW
1126 1130
1127 1131 expected = '<ul class="toc">' +
1128 1132 '<li><strong>Table of contents</strong></li>' +
1129 1133 '<li><a href="#Title">Title</a>' +
1130 1134 '<ul>' +
1131 1135 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1132 1136 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1133 1137 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1134 1138 '<ul>' +
1135 1139 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1136 1140 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1137 1141 '</ul>' +
1138 1142 '</li>' +
1139 1143 '</ul>' +
1140 1144 '</li>' +
1141 1145 '<li><a href="#Another-title">Another title</a>' +
1142 1146 '<ul>' +
1143 1147 '<li>' +
1144 1148 '<ul>' +
1145 1149 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1146 1150 '</ul>' +
1147 1151 '</li>' +
1148 1152 '<li><a href="#Project-Name">Project Name</a></li>' +
1149 1153 '</ul>' +
1150 1154 '</li>' +
1151 1155 '</ul>'
1152 1156
1153 1157 @project = Project.find(1)
1154 1158 assert textilizable(raw).gsub("\n", "").include?(expected)
1155 1159 end
1156 1160
1157 1161 def test_table_of_content_should_generate_unique_anchors
1158 1162 set_language_if_valid 'en'
1159 1163
1160 1164 raw = <<-RAW
1161 1165 {{toc}}
1162 1166
1163 1167 h1. Title
1164 1168
1165 1169 h2. Subtitle
1166 1170
1167 1171 h2. Subtitle
1168 1172 RAW
1169 1173
1170 1174 expected = '<ul class="toc">' +
1171 1175 '<li><strong>Table of contents</strong></li>' +
1172 1176 '<li><a href="#Title">Title</a>' +
1173 1177 '<ul>' +
1174 1178 '<li><a href="#Subtitle">Subtitle</a></li>' +
1175 1179 '<li><a href="#Subtitle-2">Subtitle</a></li>' +
1176 1180 '</ul>' +
1177 1181 '</li>' +
1178 1182 '</ul>'
1179 1183
1180 1184 @project = Project.find(1)
1181 1185 result = textilizable(raw).gsub("\n", "")
1182 1186 assert_include expected, result
1183 1187 assert_include '<a name="Subtitle">', result
1184 1188 assert_include '<a name="Subtitle-2">', result
1185 1189 end
1186 1190
1187 1191 def test_table_of_content_should_contain_included_page_headings
1188 1192 set_language_if_valid 'en'
1189 1193
1190 1194 raw = <<-RAW
1191 1195 {{toc}}
1192 1196
1193 1197 h1. Included
1194 1198
1195 1199 {{include(Child_1)}}
1196 1200 RAW
1197 1201
1198 1202 expected = '<ul class="toc">' +
1199 1203 '<li><strong>Table of contents</strong></li>' +
1200 1204 '<li><a href="#Included">Included</a></li>' +
1201 1205 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1202 1206 '</ul>'
1203 1207
1204 1208 @project = Project.find(1)
1205 1209 assert textilizable(raw).gsub("\n", "").include?(expected)
1206 1210 end
1207 1211
1208 1212 def test_toc_with_textile_formatting_should_be_parsed
1209 1213 with_settings :text_formatting => 'textile' do
1210 1214 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1211 1215 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1212 1216 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1213 1217 end
1214 1218 end
1215 1219
1216 1220 if Object.const_defined?(:Redcarpet)
1217 1221 def test_toc_with_markdown_formatting_should_be_parsed
1218 1222 with_settings :text_formatting => 'markdown' do
1219 1223 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1220 1224 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1221 1225 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1222 1226 end
1223 1227 end
1224 1228 end
1225 1229
1226 1230 def test_section_edit_links
1227 1231 raw = <<-RAW
1228 1232 h1. Title
1229 1233
1230 1234 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1231 1235
1232 1236 h2. Subtitle with a [[Wiki]] link
1233 1237
1234 1238 h2. Subtitle with *some* _modifiers_
1235 1239
1236 1240 h2. Subtitle with @inline code@
1237 1241
1238 1242 <pre>
1239 1243 some code
1240 1244
1241 1245 h2. heading inside pre
1242 1246
1243 1247 <h2>html heading inside pre</h2>
1244 1248 </pre>
1245 1249
1246 1250 h2. Subtitle after pre tag
1247 1251 RAW
1248 1252
1249 1253 @project = Project.find(1)
1250 1254 set_language_if_valid 'en'
1251 1255 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1252 1256
1253 1257 # heading that contains inline code
1254 1258 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-4">' +
1255 1259 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=4">Edit this section</a></div>' +
1256 1260 '<a name="Subtitle-with-inline-code"></a>' +
1257 1261 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1258 1262 result
1259 1263
1260 1264 # last heading
1261 1265 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-5">' +
1262 1266 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=5">Edit this section</a></div>' +
1263 1267 '<a name="Subtitle-after-pre-tag"></a>' +
1264 1268 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1265 1269 result
1266 1270 end
1267 1271
1268 1272 def test_default_formatter
1269 1273 with_settings :text_formatting => 'unknown' do
1270 1274 text = 'a *link*: http://www.example.net/'
1271 1275 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1272 1276 end
1273 1277 end
1274 1278
1275 1279 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1276 1280 text = '<a>http://example.com</a>'
1277 1281 expected = text.dup
1278 1282 parse_redmine_links(text, nil, nil, nil, true, {})
1279 1283 assert_equal expected, text
1280 1284 end
1281 1285
1282 1286 def test_due_date_distance_in_words
1283 1287 to_test = { Date.today => 'Due in 0 days',
1284 1288 Date.today + 1 => 'Due in 1 day',
1285 1289 Date.today + 100 => 'Due in about 3 months',
1286 1290 Date.today + 20000 => 'Due in over 54 years',
1287 1291 Date.today - 1 => '1 day late',
1288 1292 Date.today - 100 => 'about 3 months late',
1289 1293 Date.today - 20000 => 'over 54 years late',
1290 1294 }
1291 1295 ::I18n.locale = :en
1292 1296 to_test.each do |date, expected|
1293 1297 assert_equal expected, due_date_distance_in_words(date)
1294 1298 end
1295 1299 end
1296 1300
1297 1301 def test_avatar_enabled
1298 1302 with_settings :gravatar_enabled => '1' do
1299 1303 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1300 1304 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1301 1305 # Default size is 50
1302 1306 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1303 1307 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1304 1308 # Non-avatar options should be considered html options
1305 1309 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1306 1310 # The default class of the img tag should be gravatar
1307 1311 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1308 1312 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1309 1313 assert_nil avatar('jsmith')
1310 1314 assert_nil avatar(nil)
1311 1315 end
1312 1316 end
1313 1317
1314 1318 def test_avatar_disabled
1315 1319 with_settings :gravatar_enabled => '0' do
1316 1320 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1317 1321 end
1318 1322 end
1319 1323
1320 1324 def test_link_to_user
1321 1325 user = User.find(2)
1322 1326 result = link_to("John Smith", "/users/2", :class => "user active")
1323 1327 assert_equal result, link_to_user(user)
1324 1328 end
1325 1329
1326 1330 def test_link_to_user_should_not_link_to_locked_user
1327 1331 with_current_user nil do
1328 1332 user = User.find(5)
1329 1333 assert user.locked?
1330 1334 assert_equal 'Dave2 Lopper2', link_to_user(user)
1331 1335 end
1332 1336 end
1333 1337
1334 1338 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1335 1339 with_current_user User.find(1) do
1336 1340 user = User.find(5)
1337 1341 assert user.locked?
1338 1342 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1339 1343 assert_equal result, link_to_user(user)
1340 1344 end
1341 1345 end
1342 1346
1343 1347 def test_link_to_user_should_not_link_to_anonymous
1344 1348 user = User.anonymous
1345 1349 assert user.anonymous?
1346 1350 t = link_to_user(user)
1347 1351 assert_equal ::I18n.t(:label_user_anonymous), t
1348 1352 end
1349 1353
1350 1354 def test_link_to_attachment
1351 1355 a = Attachment.find(3)
1352 1356 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1353 1357 link_to_attachment(a)
1354 1358 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1355 1359 link_to_attachment(a, :text => 'Text')
1356 1360 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1357 1361 assert_equal result,
1358 1362 link_to_attachment(a, :class => 'foo')
1359 1363 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1360 1364 link_to_attachment(a, :download => true)
1361 1365 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1362 1366 link_to_attachment(a, :only_path => false)
1363 1367 end
1364 1368
1365 1369 def test_thumbnail_tag
1366 1370 a = Attachment.find(3)
1367 1371 assert_select_in thumbnail_tag(a),
1368 1372 'a[href=?][title=?] img[alt="3"][src=?]',
1369 1373 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1370 1374 end
1371 1375
1372 1376 def test_link_to_project
1373 1377 project = Project.find(1)
1374 1378 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1375 1379 link_to_project(project)
1376 1380 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1377 1381 link_to_project(project, {:only_path => false, :jump => 'blah'})
1378 1382 end
1379 1383
1380 1384 def test_link_to_project_settings
1381 1385 project = Project.find(1)
1382 1386 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1383 1387
1384 1388 project.status = Project::STATUS_CLOSED
1385 1389 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1386 1390
1387 1391 project.status = Project::STATUS_ARCHIVED
1388 1392 assert_equal 'eCookbook', link_to_project_settings(project)
1389 1393 end
1390 1394
1391 1395 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1392 1396 # numeric identifier are no longer allowed
1393 1397 Project.where(:id => 1).update_all(:identifier => 25)
1394 1398 assert_equal '<a href="/projects/1">eCookbook</a>',
1395 1399 link_to_project(Project.find(1))
1396 1400 end
1397 1401
1398 1402 def test_principals_options_for_select_with_users
1399 1403 User.current = nil
1400 1404 users = [User.find(2), User.find(4)]
1401 1405 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1402 1406 principals_options_for_select(users)
1403 1407 end
1404 1408
1405 1409 def test_principals_options_for_select_with_selected
1406 1410 User.current = nil
1407 1411 users = [User.find(2), User.find(4)]
1408 1412 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1409 1413 principals_options_for_select(users, User.find(4))
1410 1414 end
1411 1415
1412 1416 def test_principals_options_for_select_with_users_and_groups
1413 1417 User.current = nil
1414 1418 set_language_if_valid 'en'
1415 1419 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1416 1420 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1417 1421 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1418 1422 principals_options_for_select(users)
1419 1423 end
1420 1424
1421 1425 def test_principals_options_for_select_with_empty_collection
1422 1426 assert_equal '', principals_options_for_select([])
1423 1427 end
1424 1428
1425 1429 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1426 1430 set_language_if_valid 'en'
1427 1431 users = [User.find(2), User.find(4)]
1428 1432 User.current = User.find(4)
1429 1433 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1430 1434 end
1431 1435
1432 1436 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1433 1437 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1434 1438 end
1435 1439
1436 1440 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1437 1441 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1438 1442 end
1439 1443
1440 1444 def test_image_tag_should_pick_the_default_image
1441 1445 assert_match 'src="/images/image.png"', image_tag("image.png")
1442 1446 end
1443 1447
1444 1448 def test_image_tag_should_pick_the_theme_image_if_it_exists
1445 1449 theme = Redmine::Themes.themes.last
1446 1450 theme.images << 'image.png'
1447 1451
1448 1452 with_settings :ui_theme => theme.id do
1449 1453 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1450 1454 assert_match %|src="/images/other.png"|, image_tag("other.png")
1451 1455 end
1452 1456 ensure
1453 1457 theme.images.delete 'image.png'
1454 1458 end
1455 1459
1456 1460 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1457 1461 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1458 1462 end
1459 1463
1460 1464 def test_javascript_include_tag_should_pick_the_default_javascript
1461 1465 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1462 1466 end
1463 1467
1464 1468 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1465 1469 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1466 1470 end
1467 1471
1468 1472 def test_raw_json_should_escape_closing_tags
1469 1473 s = raw_json(["<foo>bar</foo>"])
1470 1474 assert_include '\/foo', s
1471 1475 end
1472 1476
1473 1477 def test_raw_json_should_be_html_safe
1474 1478 s = raw_json(["foo"])
1475 1479 assert s.html_safe?
1476 1480 end
1477 1481
1478 1482 def test_html_title_should_app_title_if_not_set
1479 1483 assert_equal 'Redmine', html_title
1480 1484 end
1481 1485
1482 1486 def test_html_title_should_join_items
1483 1487 html_title 'Foo', 'Bar'
1484 1488 assert_equal 'Foo - Bar - Redmine', html_title
1485 1489 end
1486 1490
1487 1491 def test_html_title_should_append_current_project_name
1488 1492 @project = Project.find(1)
1489 1493 html_title 'Foo', 'Bar'
1490 1494 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1491 1495 end
1492 1496
1493 1497 def test_title_should_return_a_h2_tag
1494 1498 assert_equal '<h2>Foo</h2>', title('Foo')
1495 1499 end
1496 1500
1497 1501 def test_title_should_set_html_title
1498 1502 title('Foo')
1499 1503 assert_equal 'Foo - Redmine', html_title
1500 1504 end
1501 1505
1502 1506 def test_title_should_turn_arrays_into_links
1503 1507 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1504 1508 assert_equal 'Foo - Redmine', html_title
1505 1509 end
1506 1510
1507 1511 def test_title_should_join_items
1508 1512 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1509 1513 assert_equal 'Bar - Foo - Redmine', html_title
1510 1514 end
1511 1515
1512 1516 def test_favicon_path
1513 1517 assert_match %r{^/favicon\.ico}, favicon_path
1514 1518 end
1515 1519
1516 1520 def test_favicon_path_with_suburi
1517 1521 Redmine::Utils.relative_url_root = '/foo'
1518 1522 assert_match %r{^/foo/favicon\.ico}, favicon_path
1519 1523 ensure
1520 1524 Redmine::Utils.relative_url_root = ''
1521 1525 end
1522 1526
1523 1527 def test_favicon_url
1524 1528 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1525 1529 end
1526 1530
1527 1531 def test_favicon_url_with_suburi
1528 1532 Redmine::Utils.relative_url_root = '/foo'
1529 1533 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1530 1534 ensure
1531 1535 Redmine::Utils.relative_url_root = ''
1532 1536 end
1533 1537
1534 1538 def test_truncate_single_line
1535 1539 str = "01234"
1536 1540 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1537 1541 assert_equal "01234 0...", result
1538 1542 assert !result.html_safe?
1539 1543 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1540 1544 assert_equal "01234<&#> 012...", result
1541 1545 assert !result.html_safe?
1542 1546 end
1543 1547
1544 1548 def test_truncate_single_line_non_ascii
1545 1549 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1546 1550 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1547 1551 assert_equal "#{ja} #{ja}...", result
1548 1552 assert !result.html_safe?
1549 1553 end
1550 1554
1551 1555 def test_back_url_should_remove_utf8_checkmark_from_referer
1552 1556 stubs(:request).returns(stub(:env => {'HTTP_REFERER' => "/path?utf8=\u2713&foo=bar"}))
1553 1557 assert_equal "/path?foo=bar", back_url
1554 1558 end
1555 1559
1556 1560 def test_hours_formatting
1557 1561 set_language_if_valid 'en'
1558 1562
1559 1563 with_settings :timespan_format => 'minutes' do
1560 1564 assert_equal '0:45', format_hours(0.75)
1561 1565 assert_equal '0:45 h', l_hours_short(0.75)
1562 1566 assert_equal '0:45 hour', l_hours(0.75)
1563 1567 end
1564 1568 with_settings :timespan_format => 'decimal' do
1565 1569 assert_equal '0.75', format_hours(0.75)
1566 1570 assert_equal '0.75 h', l_hours_short(0.75)
1567 1571 assert_equal '0.75 hour', l_hours(0.75)
1568 1572 end
1569 1573 end
1570 1574
1571 1575 def test_html_hours
1572 1576 assert_equal '<span class="hours hours-int">0</span><span class="hours hours-dec">:45</span>', html_hours('0:45')
1573 1577 assert_equal '<span class="hours hours-int">0</span><span class="hours hours-dec">.75</span>', html_hours('0.75')
1574 1578 end
1575 1579 end
General Comments 0
You need to be logged in to leave comments. Login now