# HG changeset patch # User Chris Cannam # Date 1296747297 0 # Node ID 752184172a34249ec94405b9b8e01652f58e03d8 # Parent 011f2ec6018feb6f283ec3e6022c68847d00c9d3# Parent 594ed6aef7bdac3cdef023df68f129e67a06d93e Merge from branch "feature_55" diff -r 594ed6aef7bd -r 752184172a34 app/controllers/members_controller.rb --- a/app/controllers/members_controller.rb Thu Feb 03 15:31:33 2011 +0000 +++ b/app/controllers/members_controller.rb Thu Feb 03 15:34:57 2011 +0000 @@ -94,6 +94,7 @@ def autocomplete_for_member @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals + logger.debug "Query for #{params[:q]} returned #{@principals.size} results" render :layout => false end diff -r 594ed6aef7bd -r 752184172a34 app/controllers/projects_controller.rb --- a/app/controllers/projects_controller.rb Thu Feb 03 15:31:33 2011 +0000 +++ b/app/controllers/projects_controller.rb Thu Feb 03 15:34:57 2011 +0000 @@ -45,12 +45,22 @@ helper :repositories include RepositoriesHelper include ProjectsHelper - + # Lists visible projects def index respond_to do |format| format.html { - @projects = Project.visible.find(:all, :order => 'lft') + sort_init 'lft' + sort_update %w(lft title created_on updated_on) + @limit = per_page_option + @project_count = Project.visible.count + @project_pages = Paginator.new self, @project_count, @limit, params['page'] + @offset ||= @project_pages.current.offset + @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => sort_clause) + if User.current.logged? + @user_projects = User.current.projects.sort_by(&:lft) + end + render :template => 'projects/index.rhtml', :layout => !request.xhr? } format.xml { @projects = Project.visible.find(:all, :order => 'lft') diff -r 594ed6aef7bd -r 752184172a34 app/helpers/application_helper.rb --- a/app/helpers/application_helper.rb Thu Feb 03 15:31:33 2011 +0000 +++ b/app/helpers/application_helper.rb Thu Feb 03 15:34:57 2011 +0000 @@ -52,7 +52,7 @@ if user.is_a?(User) name = h(user.name(options[:format])) if user.active? - link_to name, :controller => 'users', :action => 'show', :id => user + link_to(name, :controller => 'users', :action => 'show', :id => user) else name end @@ -273,7 +273,7 @@ def principals_check_box_tags(name, principals) s = '' principals.sort.each do |principal| - s << "\n" + s << "\n" end s end diff -r 594ed6aef7bd -r 752184172a34 app/helpers/application_helper.rb.orig --- a/app/helpers/application_helper.rb.orig Thu Feb 03 15:31:33 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,801 +0,0 @@ -# redMine - project management software -# Copyright (C) 2006-2007 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'forwardable' -require 'cgi' - -module ApplicationHelper - include Redmine::WikiFormatting::Macros::Definitions - include Redmine::I18n - include GravatarHelper::PublicMethods - - extend Forwardable - def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter - - # Return true if user is authorized for controller/action, otherwise false - def authorize_for(controller, action) - User.current.allowed_to?({:controller => controller, :action => action}, @project) - end - - # Display a link if user is authorized - def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) - link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) - end - - # Display a link to remote if user is authorized - def link_to_remote_if_authorized(name, options = {}, html_options = nil) - url = options[:url] || {} - link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) - end - - # Displays a link to user's account page if active - def link_to_user(user, options={}) - if user.is_a?(User) - name = h(user.name(options[:format])) - if user.active? - link_to name, :controller => 'users', :action => 'show', :id => user - else - name - end - else - h(user.to_s) - end - end - - # Displays a link to +issue+ with its subject. - # Examples: - # - # link_to_issue(issue) # => Defect #6: This is the subject - # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... - # link_to_issue(issue, :subject => false) # => Defect #6 - # link_to_issue(issue, :project => true) # => Foo - Defect #6 - # - def link_to_issue(issue, options={}) - title = nil - subject = nil - if options[:subject] == false - title = truncate(issue.subject, :length => 60) - else - subject = issue.subject - if options[:truncate] - subject = truncate(subject, :length => options[:truncate]) - end - end - s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, - :class => issue.css_classes, - :title => title - s << ": #{h subject}" if subject - s = "#{h issue.project} - " + s if options[:project] - s - end - - # Generates a link to an attachment. - # Options: - # * :text - Link text (default to attachment filename) - # * :download - Force download (default: false) - def link_to_attachment(attachment, options={}) - text = options.delete(:text) || attachment.filename - action = options.delete(:download) ? 'download' : 'show' - - link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) - end - - # Generates a link to a SCM revision - # Options: - # * :text - Link text (default to the formatted revision) - def link_to_revision(revision, project, options={}) - text = options.delete(:text) || format_revision(revision) - - link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) - end - - def toggle_link(name, id, options={}) - onclick = "Element.toggle('#{id}'); " - onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") - onclick << "return false;" - link_to(name, "#", :onclick => onclick) - end - - def image_to_function(name, function, html_options = {}) - html_options.symbolize_keys! - tag(:input, html_options.merge({ - :type => "image", :src => image_path(name), - :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" - })) - end - - def prompt_to_remote(name, text, param, url, html_options = {}) - html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" - link_to name, {}, html_options - end - - def format_activity_title(text) - h(truncate_single_line(text, :length => 100)) - end - - def format_activity_day(date) - date == Date.today ? l(:label_today).titleize : format_date(date) - end - - def format_activity_description(text) - h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "
") - end - - def format_version_name(version) - if version.project == @project - h(version) - else - h("#{version.project} - #{version}") - end - end - - def due_date_distance_in_words(date) - if date - l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) - end - end - - def render_page_hierarchy(pages, node=nil) - content = '' - if pages[node] - content << "\n" - end - content - end - - # Renders flash messages - def render_flash_messages - s = '' - flash.each do |k,v| - s << content_tag('div', v, :class => "flash #{k}") - end - s - end - - # Renders tabs and their content - def render_tabs(tabs) - if tabs.any? - render :partial => 'common/tabs', :locals => {:tabs => tabs} - else - content_tag 'p', l(:label_no_data), :class => "nodata" - end - end - - # Renders the project quick-jump box - def render_project_jump_box - # Retrieve them now to avoid a COUNT query - projects = User.current.projects.all - if projects.any? - s = '' - s - end - end - - def project_tree_options_for_select(projects, options = {}) - s = '' - project_tree(projects) do |project, level| - name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') - tag_options = {:value => project.id} - if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) - tag_options[:selected] = 'selected' - else - tag_options[:selected] = nil - end - tag_options.merge!(yield(project)) if block_given? - s << content_tag('option', name_prefix + h(project), tag_options) - end - s - end - - # Yields the given block for each project with its level in the tree - def project_tree(projects, &block) - ancestors = [] - projects.sort_by(&:lft).each do |project| - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - end - yield project, ancestors.size - ancestors << project - end - end - - def project_nested_ul(projects, &block) - s = '' - if projects.any? - ancestors = [] - projects.sort_by(&:lft).each do |project| - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "\n" - end - end - s << "
  • " - s << yield(project).to_s - ancestors << project - end - s << ("
  • \n" * ancestors.size) - end - s - end - - def principals_check_box_tags(name, principals) - s = '' - principals.sort.each do |principal| - s << "\n" - end - s - end - - # Truncates and returns the string as a single line - def truncate_single_line(string, *args) - truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') - end - - # Truncates at line break after 250 characters or options[:length] - def truncate_lines(string, options={}) - length = options[:length] || 250 - if string.to_s =~ /\A(.{#{length}}.*?)$/m - "#{$1}..." - else - string - end - end - - def html_hours(text) - text.gsub(%r{(\d+)\.(\d+)}, '\1.\2') - end - - def authoring(created, author, options={}) - l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)) - end - - def time_tag(time) - text = distance_of_time_in_words(Time.now, time) - if @project - link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time)) - else - content_tag('acronym', text, :title => format_time(time)) - end - end - - def syntax_highlight(name, content) - Redmine::SyntaxHighlighting.highlight_by_filename(content, name) - end - - def to_path_param(path) - path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} - end - - def pagination_links_full(paginator, count=nil, options={}) - page_param = options.delete(:page_param) || :page - per_page_links = options.delete(:per_page_links) - url_param = params.dup - # don't reuse query params if filters are present - url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter) - - html = '' - if paginator.current.previous - html << link_to_remote_content_update('« ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' ' - end - - html << (pagination_links_each(paginator, options) do |n| - link_to_remote_content_update(n.to_s, url_param.merge(page_param => n)) - end || '') - - if paginator.current.next - html << ' ' + link_to_remote_content_update((l(:label_next) + ' »'), url_param.merge(page_param => paginator.current.next)) - end - - unless count.nil? - html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" - if per_page_links != false && links = per_page_links(paginator.items_per_page) - html << " | #{links}" - end - end - - html - end - - def per_page_links(selected=nil) - url_param = params.dup - url_param.clear if url_param.has_key?(:set_filter) - - links = Setting.per_page_options_array.collect do |n| - n == selected ? n : link_to_remote(n, {:update => "content", - :url => params.dup.merge(:per_page => n), - :method => :get}, - {:href => url_for(url_param.merge(:per_page => n))}) - end - links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil - end - - def reorder_links(name, url) - link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) + - link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) + - link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) + - link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest)) - end - - def breadcrumb(*args) - elements = args.flatten - elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil - end - - def other_formats_links(&block) - concat('

    ' + l(:label_export_to)) - yield Redmine::Views::OtherFormatsBuilder.new(self) - concat('

    ') - end - - def page_header_title - if @project.nil? || @project.new_record? - h(Setting.app_title) - else - b = [] - ancestors = (@project.root? ? [] : @project.ancestors.visible) - if ancestors.any? - root = ancestors.shift - b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root') - if ancestors.size > 2 - b << '…' - ancestors = ancestors[-2, 2] - end - b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') } - end - b << h(@project) - b.join(' » ') - end - end - - def html_title(*args) - if args.empty? - title = [] - title << @project.name if @project - title += @html_title if @html_title - title << Setting.app_title - title.select {|t| !t.blank? }.join(' - ') - else - @html_title ||= [] - @html_title += args - end - end - - def accesskey(s) - Redmine::AccessKeys.key_for s - end - - # Formats text according to system settings. - # 2 ways to call this method: - # * with a String: textilizable(text, options) - # * with an object and one of its attribute: textilizable(issue, :description, options) - def textilizable(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - case args.size - when 1 - obj = options[:object] - text = args.shift - when 2 - obj = args.shift - attr = args.shift - text = obj.send(attr).to_s - else - raise ArgumentError, 'invalid arguments to textilizable' - end - return '' if text.blank? - project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) - only_path = options.delete(:only_path) == false ? false : true - - text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) } - - parse_non_pre_blocks(text) do |text| - [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| - send method_name, text, project, obj, attr, only_path, options - end - end - end - - def parse_non_pre_blocks(text) - s = StringScanner.new(text) - tags = [] - parsed = '' - while !s.eos? - s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) - text, full_tag, closing, tag = s[1], s[2], s[3], s[4] - if tags.empty? - yield text - end - parsed << text - if tag - if closing - if tags.last == tag.downcase - tags.pop - end - else - tags << tag.downcase - end - parsed << full_tag - end - end - # Close any non closing tags - while tag = tags.pop - parsed << "" - end - parsed - end - - def parse_inline_attachments(text, project, obj, attr, only_path, options) - # when using an image link, try to use an attachment, if possible - if options[:attachments] || (obj && obj.respond_to?(:attachments)) - attachments = nil - text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| - filename, ext, alt, alttext = $1.downcase, $2, $3, $4 - attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse - # search for the picture in attachments - if found = attachments.detect { |att| att.filename.downcase == filename } - image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found - desc = found.description.to_s.gsub('"', '') - if !desc.blank? && alttext.blank? - alt = " title=\"#{desc}\" alt=\"#{desc}\"" - end - "src=\"#{image_url}\"#{alt}" - else - m - end - end - end - end - - # Wiki links - # - # Examples: - # [[mypage]] - # [[mypage|mytext]] - # wiki links can refer other project wikis, using project name or identifier: - # [[project:]] -> wiki starting page - # [[project:|mytext]] - # [[project:mypage]] - # [[project:mypage|mytext]] - def parse_wiki_links(text, project, obj, attr, only_path, options) - text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| - link_project = project - esc, all, page, title = $1, $2, $3, $5 - if esc.nil? - if page =~ /^([^\:]+)\:(.*)$/ - link_project = Project.find_by_name($1) || Project.find_by_identifier($1) - page = $2 - title ||= $1 if page.blank? - end - - if link_project && link_project.wiki - # extract anchor - anchor = nil - if page =~ /^(.+?)\#(.+)$/ - page, anchor = $1, $2 - end - # check if page exists - wiki_page = link_project.wiki.find_page(page) - url = case options[:wiki_links] - when :local; "#{title}.html" - when :anchor; "##{title}" # used for single-file wiki export - else - url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => link_project, :page => Wiki.titleize(page), :anchor => anchor) - end - link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) - else - # project or wiki doesn't exist - all - end - else - all - end - end - end - - # Redmine links - # - # Examples: - # Issues: - # #52 -> Link to issue #52 - # Changesets: - # r52 -> Link to revision 52 - # commit:a85130f -> Link to scmid starting with a85130f - # Documents: - # document#17 -> Link to document with id 17 - # document:Greetings -> Link to the document with title "Greetings" - # document:"Some document" -> Link to the document with title "Some document" - # Versions: - # version#3 -> Link to version with id 3 - # version:1.0.0 -> Link to version named "1.0.0" - # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" - # Attachments: - # attachment:file.zip -> Link to the attachment of the current object named file.zip - # Source files: - # source:some/file -> Link to the file located at /some/file in the project's repository - # source:some/file@52 -> Link to the file's revision 52 - # source:some/file#L120 -> Link to line 120 of the file - # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 - # export:some/file -> Force the download of the file - # Forum messages: - # message#1218 -> Link to message with id 1218 - def parse_redmine_links(text, project, obj, attr, only_path, options) - text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m| - leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8 - link = nil - if esc.nil? - if prefix.nil? && sep == 'r' - if project && (changeset = project.changesets.find_by_revision(identifier)) - link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, - :class => 'changeset', - :title => truncate_single_line(changeset.comments, :length => 100)) - end - elsif sep == '#' - oid = identifier.to_i - case prefix - when nil - if issue = Issue.visible.find_by_id(oid, :include => :status) - link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, - :class => issue.css_classes, - :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") - end - when 'document' - if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) - link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, - :class => 'document' - end - when 'version' - if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) - link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, - :class => 'version' - end - when 'message' - if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current)) - link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path, - :controller => 'messages', - :action => 'show', - :board_id => message.board, - :id => message.root, - :anchor => (message.parent ? "message-#{message.id}" : nil)}, - :class => 'message' - end - when 'project' - if p = Project.visible.find_by_id(oid) - link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p}, - :class => 'project' - end - end - elsif sep == ':' - # removes the double quotes if any - name = identifier.gsub(%r{^"(.*)"$}, "\\1") - case prefix - when 'document' - if project && document = project.documents.find_by_title(name) - link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, - :class => 'document' - end - when 'version' - if project && version = project.versions.find_by_name(name) - link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, - :class => 'version' - end - when 'commit' - if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])) - link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, - :class => 'changeset', - :title => truncate_single_line(changeset.comments, :length => 100) - end - when 'source', 'export' - if project && project.repository - name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} - path, rev, anchor = $1, $3, $5 - link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, - :path => to_path_param(path), - :rev => rev, - :anchor => anchor, - :format => (prefix == 'export' ? 'raw' : nil)}, - :class => (prefix == 'export' ? 'source download' : 'source') - end - when 'attachment' - attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) - if attachments && attachment = attachments.detect {|a| a.filename == name } - link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, - :class => 'attachment' - end - when 'project' - if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) - link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p}, - :class => 'project' - end - end - end - end - leading + (link || "#{prefix}#{sep}#{identifier}") - end - end - - # Same as Rails' simple_format helper without using paragraphs - def simple_format_without_paragraph(text) - text.to_s. - gsub(/\r\n?/, "\n"). # \r\n and \r -> \n - gsub(/\n\n+/, "

    "). # 2+ newline -> 2 br - gsub(/([^\n]\n)(?=[^\n])/, '\1
    ') # 1 newline -> br - end - - def lang_options_for_select(blank=true) - (blank ? [["(auto)", ""]] : []) + - valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } - end - - def label_tag_for(name, option_tags = nil, options = {}) - label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") - content_tag("label", label_text) - end - - def labelled_tabular_form_for(name, object, options, &proc) - options[:html] ||= {} - options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) - form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) - end - - def back_url_hidden_field_tag - back_url = params[:back_url] || request.env['HTTP_REFERER'] - back_url = CGI.unescape(back_url.to_s) - hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank? - end - - def check_all_links(form_name) - link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + - " | " + - link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") - end - - def progress_bar(pcts, options={}) - pcts = [pcts, pcts] unless pcts.is_a?(Array) - pcts = pcts.collect(&:round) - pcts[1] = pcts[1] - pcts[0] - pcts << (100 - pcts[1] - pcts[0]) - width = options[:width] || '100px;' - legend = options[:legend] || '' - content_tag('table', - content_tag('tr', - (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') + - (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') + - (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '') - ), :class => 'progress', :style => "width: #{width};") + - content_tag('p', legend, :class => 'pourcent') - end - - def checked_image(checked=true) - if checked - image_tag 'toggle_check.png' - end - end - - def context_menu(url) - unless @context_menu_included - content_for :header_tags do - javascript_include_tag('context_menu') + - stylesheet_link_tag('context_menu') - end - @context_menu_included = true - end - javascript_tag "new ContextMenu('#{ url_for(url) }')" - end - - def context_menu_link(name, url, options={}) - options[:class] ||= '' - if options.delete(:selected) - options[:class] << ' icon-checked disabled' - options[:disabled] = true - end - if options.delete(:disabled) - options.delete(:method) - options.delete(:confirm) - options.delete(:onclick) - options[:class] << ' disabled' - url = '#' - end - link_to name, url, options - end - - def calendar_for(field_id) - include_calendar_headers_tags - image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + - javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") - end - - def include_calendar_headers_tags - unless @calendar_headers_tags_included - @calendar_headers_tags_included = true - content_for :header_tags do - start_of_week = case Setting.start_of_week.to_i - when 1 - 'Calendar._FD = 1;' # Monday - when 7 - 'Calendar._FD = 0;' # Sunday - else - '' # use language - end - - javascript_include_tag('calendar/calendar') + - javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") + - javascript_tag(start_of_week) + - javascript_include_tag('calendar/calendar-setup') + - stylesheet_link_tag('calendar') - end - end - end - - def content_for(name, content = nil, &block) - @has_content ||= {} - @has_content[name] = true - super(name, content, &block) - end - - def has_content?(name) - (@has_content && @has_content[name]) || false - end - - # Returns the avatar image tag for the given +user+ if avatars are enabled - # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') - def avatar(user, options = { }) - if Setting.gravatar_enabled? - options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default}) - email = nil - if user.respond_to?(:mail) - email = user.mail - elsif user.to_s =~ %r{<(.+?)>} - email = $1 - end - return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil - end - end - - private - - def wiki_helper - helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) - extend helper - return self - end - - def link_to_remote_content_update(text, url_params) - link_to_remote(text, - {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, - {:href => url_for(:params => url_params)} - ) - end - -end diff -r 594ed6aef7bd -r 752184172a34 app/helpers/projects_helper.rb --- a/app/helpers/projects_helper.rb Thu Feb 03 15:31:33 2011 +0000 +++ b/app/helpers/projects_helper.rb Thu Feb 03 15:34:57 2011 +0000 @@ -121,7 +121,12 @@ classes = (ancestors.empty? ? 'root' : 'child') s << "
  • " + - link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") + link_to_project(project, {}, :class => "project my-project") + if project.is_public? + s << " " << l("field_is_public") << "" + else + s << " " << l("field_is_private") << "" + end s << "
    #{textilizable(project.short_description, :project => project)}
    " unless project.description.blank? s << "
    \n" ancestors << project @@ -143,66 +148,96 @@ a end - # Renders a tree of projects where the current DOES NOT belong - # as a nested set of unordered lists - # The given collection may be a subset of the whole project tree - # (eg. some intermediate nodes are private and can not be seen) - def render_other_project_hierarchy(projects) - a = '' - s = '' + # Renders a tree of projects that the current user does not belong + # to, or of all projects if the current user is not logged in. The + # given collection may be a subset of the whole project tree + # (eg. some intermediate nodes are private and can not be seen). We + # are potentially interested in various things: the project name, + # description, manager(s), creation date, last activity date, + # general activity level, whether there is anything actually hosted + # here for the project, etc. + def render_project_table(projects) - # True if user has any projects (affects the heading used) - t = FALSE + s = "" + s << "
    " + s << "" + s << "" + + s << sort_header_tag('lft', :caption => l("field_name"), :default_order => 'desc') + s << "" + s << sort_header_tag('created_on', :default_order => 'desc') + s << sort_header_tag('updated_on', :default_order => 'desc') - if projects.any? - ancestors = [] - original_project = @project - projects.each do |project| - # set the project environment to please macros. + s << "" - @project = project + ancestors = [] + original_project = @project + oddeven = 'even' + level = 0 - if not User.current.member_of?(project): + projects.each do |project| - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "
      \n" - else - ancestors.pop - s << "" - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - s << "
    \n" + # set the project environment to please macros. + + @project = project + + if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) + level = level + 1 + else + level = 0 + oddeven = cycle('odd','even') + ancestors.pop + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + end + end + + classes = (ancestors.empty? ? 'root' : 'child') + + s << "" + s << "" + s << "" + s << "" - s << ("\n" * ancestors.size) - @project = original_project + s << "" + + ancestors << project end - if t == TRUE - a << "

    " - a << l("label_other_project_plural") - a << "

    " - a << s - else - a << "

    " - a << l("label_project_all") - a << "

    " - a << s - end + s << "
    " << l("label_managers") << "
    " << link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}"); + s << "
    " + unless project.description.blank? + s << "
    " + s << textilizable(project.short_description, :project => project) + s << "
    " + end + + s << "
    " + + u = project.users_by_role + if u + u.keys.each do |r| + if r.allowed_to?(:edit_project) + mgrs = [] + u[r].sort.each do |m| + mgrs << link_to_user(m) + end + if mgrs.size < 3 + s << '' << mgrs.join(', ') << '' + else + s << mgrs.join(', ') end end + end + end - classes = (ancestors.empty? ? 'root' : 'child') - s << "
  • " + - link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") - s << "
    #{textilizable(project.short_description, :project => project)}
    " unless project.description.blank? - s << "
    \n" - ancestors << project - else - t = TRUE - end - end + s << "
  • " << format_date(project.created_on) << "" << format_date(project.updated_on) << "
    " - a + @project = original_project + + s end diff -r 594ed6aef7bd -r 752184172a34 app/models/issue.rb --- a/app/models/issue.rb Thu Feb 03 15:31:33 2011 +0000 +++ b/app/models/issue.rb Thu Feb 03 15:34:57 2011 +0000 @@ -527,7 +527,8 @@ # Returns a string of css classes that apply to the issue def css_classes - s = "issue status-#{status.position} priority-#{priority.position}" + s = "issue status-#{status.position} " + s << "priority-#{priority.position}" s << ' closed' if closed? s << ' overdue' if overdue? s << ' created-by-me' if User.current.logged? && author_id == User.current.id diff -r 594ed6aef7bd -r 752184172a34 app/models/project.rb --- a/app/models/project.rb Thu Feb 03 15:31:33 2011 +0000 +++ b/app/models/project.rb Thu Feb 03 15:34:57 2011 +0000 @@ -418,7 +418,14 @@ # Returns a short description of the projects (first lines) def short_description(length = 255) - description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description + ## Original Redmine code: this truncates to the CR that is more + ## than "length" characters from the start. + # description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description + ## That's too much for us, and also we want to omit images and the + ## like. Truncate instead to the first CR that follows _any_ + ## non-blank text, and to the next word break beyond "length" + ## characters if the result is still longer than that. + description.gsub(/![^\s]+!/, '').gsub(/^(\s*[^\n\r]*).*$/m, '\1').gsub(/^(.{#{length}}\b).*$/m, '\1 ...').strip if description end def css_classes diff -r 594ed6aef7bd -r 752184172a34 app/views/members/autocomplete_for_member.rhtml --- a/app/views/members/autocomplete_for_member.rhtml Thu Feb 03 15:31:33 2011 +0000 +++ b/app/views/members/autocomplete_for_member.rhtml Thu Feb 03 15:34:57 2011 +0000 @@ -1,1 +1,3 @@ -<%= principals_check_box_tags 'member[user_ids][]', @principals %> \ No newline at end of file +<% if params[:q] && params[:q].length > 1 %> +<%= principals_check_box_tags 'member[user_ids][]', @principals %> +<% end %> diff -r 594ed6aef7bd -r 752184172a34 app/views/projects/index.rhtml --- a/app/views/projects/index.rhtml Thu Feb 03 15:31:33 2011 +0000 +++ b/app/views/projects/index.rhtml Thu Feb 03 15:34:57 2011 +0000 @@ -8,18 +8,19 @@ <%= '| ' + link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') if User.current.allowed_to?(:add_project, nil, :global => true) %>
    -<% if User.current.logged? %> +<% if @user_projects %> - <%= render_my_project_hierarchy(@projects)%> + <%= render_my_project_hierarchy(@user_projects)%> - <%= render_other_project_hierarchy(@projects)%> - -<% else %> - -

    <%=l(:label_project_plural)%>

    - <%= render_project_hierarchy(@projects)%> <% end %> +

    +<%= l("label_project_all") %> +

    + +<%= render_project_table(@projects) %> + +

    <%= pagination_links_full @project_pages, @project_count %>

    <% other_formats_links do |f| %> diff -r 594ed6aef7bd -r 752184172a34 app/views/projects/settings/_members.rhtml --- a/app/views/projects/settings/_members.rhtml Thu Feb 03 15:31:33 2011 +0000 +++ b/app/views/projects/settings/_members.rhtml Thu Feb 03 15:34:57 2011 +0000 @@ -50,7 +50,7 @@ -<% principals = Principal.active.find(:all, :limit => 10, :order => 'type, login, lastname ASC') - @project.principals %> +<% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %>
    <% if roles.any? && principals.any? %> @@ -66,9 +66,11 @@ :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project }, :with => 'q') %> - +
    - <%= principals_check_box_tags 'member[user_ids][]', principals %> + <% if params[:q] && params[:q].length > 1 %> + <%= principals_check_box_tags 'member[user_ids][]', @principals %> + <% end %>

    <%= l(:label_role_plural) %>: diff -r 594ed6aef7bd -r 752184172a34 app/views/projects/settings/_repository.rhtml --- a/app/views/projects/settings/_repository.rhtml Thu Feb 03 15:31:33 2011 +0000 +++ b/app/views/projects/settings/_repository.rhtml Thu Feb 03 15:34:57 2011 +0000 @@ -7,7 +7,7 @@

    <% if !@repository || !@repository.url %> -
    • The repository for a project will normally be set up automatically within a few minutes of the project being created.
    +
    • <%= l(:text_settings_repo_creation) %>
    <% end %>

    <%= label_tag('repository_scm', l(:label_scm)) %><%= scm_select_tag(@repository) %>

    <%= repository_field_tags(f, @repository) if @repository %> diff -r 594ed6aef7bd -r 752184172a34 app/views/welcome/index.rhtml --- a/app/views/welcome/index.rhtml Thu Feb 03 15:31:33 2011 +0000 +++ b/app/views/welcome/index.rhtml Thu Feb 03 15:34:57 2011 +0000 @@ -9,14 +9,6 @@
    <%= textilizable Setting.welcome_text %> - <% if not @tipsoftheday.empty? %> -
    -

    <%=l(:label_tipoftheday)%>

    - <%= textilizable @tipsoftheday %> -
    - <% end %> - - <% if @news.any? %>

    <%=l(:label_news_latest)%>

    @@ -28,6 +20,13 @@
    + <% if not @tipsoftheday.empty? %> +
    +

    <%=l(:label_tipoftheday)%>

    + <%= textilizable @tipsoftheday %> +
    + <% end %> + <% if @projects.any? %>

    <%=l(:label_project_latest)%>

    diff -r 594ed6aef7bd -r 752184172a34 config/locales/en-GB.yml --- a/config/locales/en-GB.yml Thu Feb 03 15:31:33 2011 +0000 +++ b/config/locales/en-GB.yml Thu Feb 03 15:34:57 2011 +0000 @@ -241,6 +241,7 @@ field_role: Role field_homepage: Homepage field_is_public: Public + field_is_private: Private field_parent: Subproject of field_is_in_roadmap: Issues displayed in roadmap field_login: Login @@ -432,6 +433,7 @@ other: "{{count}} projects" label_project_all: All Projects label_project_latest: Latest projects + label_managers: Managed by label_issue: Issue label_issue_new: New issue label_issue_plural: Issues @@ -843,6 +845,7 @@ text_tip_issue_end_day: task ending this day text_tip_issue_begin_end_day: task beginning and ending this day text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.
    This will be used in all project-related URLs, and as the repository name. Once saved, the identifier can not be changed.' + text_project_homepage_info: 'Link to an external project page.' text_project_name_info: "This will be the name of your project throughout this site.
    You can change your project's name at any time, in the project's settings." text_project_visibility_info: "If your project is not public, it will only be visible to users that you have added as project members." text_user_ssamr_description_info: 'Please describe your current research or development interests, within the fields of audio and music.
    This information is publicly visible in your profile and you can edit it at any time.' @@ -887,6 +890,7 @@ text_wiki_page_destroy_children: "Delete child pages and all their descendants" text_wiki_page_reassign_children: "Reassign child pages to this parent page" text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?" + text_settings_repo_creation: The repository for a project should be set up automatically within a few minutes of the project being created.
    You should not have to adjust any settings here; please check again in ten minutes. default_role_manager: Manager default_role_developer: Developer @@ -945,7 +949,7 @@ label_user_mail_option_only_my_events: Only for things I watch or I'm involved in label_user_mail_option_only_assigned: Only for things I am assigned to notice_not_authorized_archived_project: The project you're trying to access has been archived. - label_principal_search: "Search for user or group:" + label_principal_search: "Search by name:" label_user_search: "Search for user:" field_visible: Visible setting_emails_header: Emails header diff -r 594ed6aef7bd -r 752184172a34 config/locales/en.yml --- a/config/locales/en.yml Thu Feb 03 15:31:33 2011 +0000 +++ b/config/locales/en.yml Thu Feb 03 15:34:57 2011 +0000 @@ -246,6 +246,7 @@ field_role: Role field_homepage: Homepage field_is_public: Public + field_is_private: Private field_parent: Subproject of field_is_in_roadmap: Issues displayed in roadmap field_login: Login @@ -448,6 +449,7 @@ other: "{{count}} projects" label_project_all: All Projects label_project_latest: Latest projects + label_managers: Managed by label_issue: Issue label_issue_new: New issue label_issue_plural: Issues @@ -797,7 +799,7 @@ label_profile: Profile label_subtask_plural: Subtasks label_project_copy_notifications: Send email notifications during the project copy - label_principal_search: "Search for user or group:" + label_principal_search: "Search by name:" label_user_search: "Search for user:" button_login: Login @@ -918,6 +920,7 @@ text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?" text_zoom_in: Zoom in text_zoom_out: Zoom out + text_settings_repo_creation: The repository for a project should be set up automatically within a few minutes of the project being created.
    You should not have to adjust any settings here.
    Please check again in ten minutes, and contact us if there is any problem. default_role_manager: Manager default_role_developer: Developer diff -r 594ed6aef7bd -r 752184172a34 extra/soundsoftware/SoundSoftware.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/SoundSoftware.pm Thu Feb 03 15:34:57 2011 +0000 @@ -0,0 +1,422 @@ +package Apache::Authn::SoundSoftware; + +=head1 Apache::Authn::SoundSoftware + +SoundSoftware - a mod_perl module for Apache authentication against a +Redmine database and optional LDAP implementing the access control +rules required for the SoundSoftware.ac.uk repository site. + +=head1 SYNOPSIS + +This module is closely based on the Redmine.pm authentication module +provided with Redmine. It is intended to be used for authentication +in front of a repository service such as hgwebdir. + +Requirements: + +1. Clone/pull from repo for public project: Any user, no +authentication required + +2. Clone/pull from repo for private project: Project members only + +3. Push to repo for public project: "Permitted" users only (this +probably means project members who are also identified in the hgrc web +section for the repository and so will be approved by hgwebdir?) + +4. Push to repo for private project: "Permitted" users only (as above) + +=head1 INSTALLATION + +Debian/ubuntu: + + apt-get install libapache-dbi-perl libapache2-mod-perl2 \ + libdbd-mysql-perl libauthen-simple-ldap-perl libio-socket-ssl-perl + +Note that LDAP support is hardcoded "on" in this script (it is +optional in the original Redmine.pm). + +=head1 CONFIGURATION + + ## This module has to be in your perl path + ## eg: /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm + PerlLoadModule Apache::Authn::SoundSoftware + + # Example when using hgwebdir + ScriptAlias / "/var/hg/hgwebdir.cgi/" + + + AuthName "Mercurial" + AuthType Basic + Require valid-user + PerlAccessHandler Apache::Authn::SoundSoftware::access_handler + PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler + SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost" + SoundSoftwareDbUser "redmine" + SoundSoftwareDbPass "password" + Options +ExecCGI + AddHandler cgi-script .cgi + ## Optional where clause (fulltext search would be slow and + ## database dependant). + # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)" + ## Optional prefix for local repository URLs + # SoundSoftwareRepoPrefix "/var/hg/" + + +See the original Redmine.pm for further configuration notes. + +=cut + +use strict; +use warnings FATAL => 'all', NONFATAL => 'redefine'; + +use DBI; +use Digest::SHA1; +use Authen::Simple::LDAP; +use Apache2::Module; +use Apache2::Access; +use Apache2::ServerRec qw(); +use Apache2::RequestRec qw(); +use Apache2::RequestUtil qw(); +use Apache2::Const qw(:common :override :cmd_how); +use APR::Pool (); +use APR::Table (); + +my @directives = ( + { + name => 'SoundSoftwareDSN', + req_override => OR_AUTHCFG, + args_how => TAKE1, + errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"', + }, + { + name => 'SoundSoftwareDbUser', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'SoundSoftwareDbPass', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'SoundSoftwareDbWhereClause', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'SoundSoftwareRepoPrefix', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, +); + +sub SoundSoftwareDSN { + my ($self, $parms, $arg) = @_; + $self->{SoundSoftwareDSN} = $arg; + my $query = "SELECT + hashed_password, auth_source_id, permissions + FROM members, projects, users, roles, member_roles + WHERE + projects.id=members.project_id + AND member_roles.member_id=members.id + AND users.id=members.user_id + AND roles.id=member_roles.role_id + AND users.status=1 + AND login=? + AND identifier=? "; + $self->{SoundSoftwareQuery} = trim($query); +} + +sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); } +sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); } +sub SoundSoftwareDbWhereClause { + my ($self, $parms, $arg) = @_; + $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." "); +} + +sub SoundSoftwareRepoPrefix { + my ($self, $parms, $arg) = @_; + if ($arg) { + $self->{SoundSoftwareRepoPrefix} = $arg; + } +} + +sub trim { + my $string = shift; + $string =~ s/\s{2,}/ /g; + return $string; +} + +sub set_val { + my ($key, $self, $parms, $arg) = @_; + $self->{$key} = $arg; +} + +Apache2::Module::add(__PACKAGE__, \@directives); + + +my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; + +sub access_handler { + my $r = shift; + + print STDERR "SoundSoftware.pm: In access handler\n"; + + unless ($r->some_auth_required) { + $r->log_reason("No authentication has been configured"); + return FORBIDDEN; + } + + my $method = $r->method; + + print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n"; + print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n"; + + if (!defined $read_only_methods{$method}) { + print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n"; + return OK; + } + + my $dbh = connect_database($r); + + my $project_id = get_project_identifier($dbh, $r); + my $status = get_project_status($dbh, $project_id, $r); + + $dbh->disconnect(); + undef $dbh; + + if ($status == 0) { # nonexistent + print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n"; + return FORBIDDEN; + } elsif ($status == 1) { # public + print STDERR "SoundSoftware.pm: Project is public, no restriction here\n"; + $r->set_handlers(PerlAuthenHandler => [\&OK]) + } else { # private + print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n"; + } + + return OK +} + +sub authen_handler { + my $r = shift; + + print STDERR "SoundSoftware.pm: In authentication handler\n"; + + my $dbh = connect_database($r); + + my $project_id = get_project_identifier($dbh, $r); + my $realm = get_realm($dbh, $project_id, $r); + $r->auth_name($realm); + + my ($res, $redmine_pass) = $r->get_basic_auth_pw(); + unless ($res == OK) { + $dbh->disconnect(); + undef $dbh; + return $res; + } + + print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n"; + + my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r); + + $dbh->disconnect(); + undef $dbh; + + if ($permitted) { + return OK; + } else { + print STDERR "SoundSoftware.pm: Not permitted\n"; + $r->note_auth_failure(); + return AUTH_REQUIRED; + } +} + +sub get_project_status { + my $dbh = shift; + my $project_id = shift; + my $r = shift; + + if (!defined $project_id or $project_id eq '') { + return 0; # nonexistent + } + + my $sth = $dbh->prepare( + "SELECT is_public FROM projects WHERE projects.identifier = ?;" + ); + + $sth->execute($project_id); + my $ret = 0; # nonexistent + if (my @row = $sth->fetchrow_array) { + if ($row[0] eq "1" || $row[0] eq "t") { + $ret = 1; # public + } else { + $ret = 2; # private + } + } + $sth->finish(); + undef $sth; + + $ret; +} + +sub is_permitted { + my $dbh = shift; + my $project_id = shift; + my $redmine_user = shift; + my $redmine_pass = shift; + my $r = shift; + + my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); + + my $cfg = Apache2::Module::get_config + (__PACKAGE__, $r->server, $r->per_dir_config); + + my $query = $cfg->{SoundSoftwareQuery}; + my $sth = $dbh->prepare($query); + $sth->execute($redmine_user, $project_id); + + my $ret; + while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) { + + # Test permissions for this user before we verify credentials + # -- if the user is not permitted this action anyway, there's + # not much point in e.g. contacting the LDAP + + my $method = $r->method; + + if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) + || $permissions =~ /:commit_access/) { + + # User would be permitted this action, if their + # credentials checked out -- test those now + + print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n"; + + unless ($auth_source_id) { + if ($hashed_password eq $pass_digest) { + print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n"; + $ret = 1; + last; + } + } else { + my $sthldap = $dbh->prepare( + "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;" + ); + $sthldap->execute($auth_source_id); + while (my @rowldap = $sthldap->fetchrow_array) { + my $ldap = Authen::Simple::LDAP->new( + host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], + port => $rowldap[1], + basedn => $rowldap[5], + binddn => $rowldap[3] ? $rowldap[3] : "", + bindpw => $rowldap[4] ? $rowldap[4] : "", + filter => "(".$rowldap[6]."=%s)" + ); + if ($ldap->authenticate($redmine_user, $redmine_pass)) { + print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n"; + $ret = 1; + } + } + $sthldap->finish(); + undef $sthldap; + } + } else { + print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n"; + } + } + + $sth->finish(); + undef $sth; + + $ret; +} + +sub get_project_identifier { + my $dbh = shift; + my $r = shift; + + my $location = $r->location; + my ($repo) = $r->uri =~ m{$location/*([^/]+)}; + + return $repo if (!$repo); + + $repo =~ s/[^a-zA-Z0-9\._-]//g; + + # The original Redmine.pm returns the string just calculated as + # the project identifier. That won't do for us -- we may have + # (and in fact already do have, in our test instance) projects + # whose repository names differ from the project identifiers. + + # This is a rather fundamental change because it means that almost + # every request needs more than one database query -- which + # prompts us to start passing around $dbh instead of connecting + # locally within each function as is done in Redmine.pm. + + my $sth = $dbh->prepare( + "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;" + ); + + my $cfg = Apache2::Module::get_config + (__PACKAGE__, $r->server, $r->per_dir_config); + + my $prefix = $cfg->{SoundSoftwareRepoPrefix}; + if (!defined $prefix) { $prefix = '%/'; } + + my $identifier = ''; + + $sth->execute($prefix . $repo); + my $ret = 0; + if (my @row = $sth->fetchrow_array) { + $identifier = $row[0]; + } + $sth->finish(); + undef $sth; + + print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n"; + + $identifier; +} + +sub get_realm { + my $dbh = shift; + my $project_id = shift; + my $r = shift; + + my $sth = $dbh->prepare( + "SELECT projects.name FROM projects WHERE projects.identifier = ?;" + ); + + my $name = $project_id; + + $sth->execute($project_id); + my $ret = 0; + if (my @row = $sth->fetchrow_array) { + $name = $row[0]; + } + $sth->finish(); + undef $sth; + + # be timid about characters not permitted in auth realm and revert + # to project identifier if any are found + if ($name =~ m/[^\w\d\s\._-]/) { + $name = $project_id; + } + + my $realm = '"Mercurial repository for ' . "'$name'" . '"'; + + $realm; +} + +sub connect_database { + my $r = shift; + + my $cfg = Apache2::Module::get_config + (__PACKAGE__, $r->server, $r->per_dir_config); + + return DBI->connect($cfg->{SoundSoftwareDSN}, + $cfg->{SoundSoftwareDbUser}, + $cfg->{SoundSoftwareDbPass}); +} + +1; diff -r 594ed6aef7bd -r 752184172a34 extra/soundsoftware/reposman-soundsoftware.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/reposman-soundsoftware.rb Thu Feb 03 15:34:57 2011 +0000 @@ -0,0 +1,350 @@ +#!/usr/bin/env ruby + +# == Synopsis +# +# reposman: manages your repositories with Redmine +# +# == Usage +# +# reposman [OPTIONS...] -s [DIR] -r [HOST] +# +# Examples: +# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net --scm subversion +# reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git +# +# == Arguments (mandatory) +# +# -s, --svn-dir=DIR use DIR as base directory for svn repositories +# -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples: +# -r redmine.example.net +# -r http://redmine.example.net +# -r https://example.net/redmine +# -k, --key=KEY use KEY as the Redmine API key +# +# == Options +# +# -o, --owner=OWNER owner of the repository. using the rails login +# allow user to browse the repository within +# Redmine even for private project. If you want to +# share repositories through Redmine.pm, you need +# to use the apache owner. +# -g, --group=GROUP group of the repository. (default: root) +# --scm=SCM the kind of SCM repository you want to create (and +# register) in Redmine (default: Subversion). +# reposman is able to create Git and Subversion +# repositories. For all other kind, you must specify +# a --command option +# -u, --url=URL the base url Redmine will use to access your +# repositories. This option is used to automatically +# register the repositories in Redmine. The project +# identifier will be appended to this url. Examples: +# -u https://example.net/svn +# -u file:///var/svn/ +# if this option isn't set, reposman will register +# the repositories with local file paths in Redmine +# -c, --command=COMMAND use this command instead of "svnadmin create" to +# create a repository. This option can be used to +# create repositories other than subversion and git +# kind. +# This command override the default creation for git +# and subversion. +# --http-user=USER User for HTTP Basic authentication with Redmine WS +# --http-pass=PASSWORD Password for Basic authentication with Redmine WS +# -t, --test only show what should be done +# -h, --help show help and exit +# -v, --verbose verbose +# -V, --version print version and exit +# -q, --quiet no log +# +# == References +# +# You can find more information on the redmine's wiki : http://www.redmine.org/wiki/redmine/HowTos + + +require 'getoptlong' +require 'rdoc/usage' +require 'find' +require 'etc' + +Version = "1.3" +SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem ) + +opts = GetoptLong.new( + ['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], + ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], + ['--key', '-k', GetoptLong::REQUIRED_ARGUMENT], + ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT], + ['--group', '-g', GetoptLong::REQUIRED_ARGUMENT], + ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT], + ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT], + ['--scm', GetoptLong::REQUIRED_ARGUMENT], + ['--http-user', GetoptLong::REQUIRED_ARGUMENT], + ['--http-pass', GetoptLong::REQUIRED_ARGUMENT], + ['--test', '-t', GetoptLong::NO_ARGUMENT], + ['--verbose', '-v', GetoptLong::NO_ARGUMENT], + ['--version', '-V', GetoptLong::NO_ARGUMENT], + ['--help' , '-h', GetoptLong::NO_ARGUMENT], + ['--quiet' , '-q', GetoptLong::NO_ARGUMENT] + ) + +$verbose = 0 +$quiet = false +$redmine_host = '' +$repos_base = '' +$http_user = '' +$http_pass = '' +$svn_owner = 'root' +$svn_group = 'root' +$use_groupid = true +$svn_url = false +$test = false +$scm = 'Subversion' + +def log(text, options={}) + level = options[:level] || 0 + puts text unless $quiet or level > $verbose + exit 1 if options[:exit] +end + +def system_or_raise(command) + raise "\"#{command}\" failed" unless system command +end + +module SCM + + module Subversion + def self.create(path) + system_or_raise "svnadmin create #{path}" + end + end + + module Git + def self.create(path) + Dir.mkdir path + Dir.chdir(path) do + system_or_raise "git --bare init --shared" + system_or_raise "git update-server-info" + end + end + end + +end + +begin + opts.each do |opt, arg| + case opt + when '--svn-dir'; $repos_base = arg.dup + when '--redmine-host'; $redmine_host = arg.dup + when '--key'; $api_key = arg.dup + when '--owner'; $svn_owner = arg.dup; $use_groupid = false; + when '--group'; $svn_group = arg.dup; $use_groupid = false; + when '--url'; $svn_url = arg.dup + when '--scm'; $scm = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm) + when '--http-user'; $http_user = arg.dup + when '--http-pass'; $http_pass = arg.dup + when '--command'; $command = arg.dup + when '--verbose'; $verbose += 1 + when '--test'; $test = true + when '--version'; puts Version; exit + when '--help'; RDoc::usage + when '--quiet'; $quiet = true + end + end +rescue + exit 1 +end + +if $test + log("running in test mode") +end + +# Make sure command is overridden if SCM vendor is not handled internally (for the moment Subversion and Git) +if $command.nil? + begin + scm_module = SCM.const_get($scm) + rescue + log("Please use --command option to specify how to create a #{$scm} repository.", :exit => true) + end +end + +$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/) + +if ($redmine_host.empty? or $repos_base.empty?) + RDoc::usage +end + +unless File.directory?($repos_base) + log("directory '#{$repos_base}' doesn't exists", :exit => true) +end + +begin + require 'active_resource' +rescue LoadError + log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true) +end + +class Project < ActiveResource::Base + self.headers["User-agent"] = "Redmine repository manager/#{Version}" +end + +log("querying Redmine for projects...", :level => 1); + +$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://") +$redmine_host.gsub!(/\/$/, '') + +Project.site = "#{$redmine_host}/sys"; +Project.user = $http_user; +Project.password = $http_pass; + +begin + # Get all active projects that have the Repository module enabled + projects = Project.find(:all, :params => {:key => $api_key}) +rescue => e + log("Unable to connect to #{Project.site}: #{e}", :exit => true) +end + +if projects.nil? + log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true) +end + +log("retrieved #{projects.size} projects", :level => 1) + +def set_owner_and_rights(project, repos_path, &block) + if RUBY_PLATFORM =~ /mswin/ + yield if block_given? + else + uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid) + right = project.is_public ? 02775 : 02770 + yield if block_given? + Find.find(repos_path) do |f| + File.chmod right, f + File.chown uid, gid, f + end + end +end + +def other_read_right?(file) + (File.stat(file).mode & 0007).zero? ? false : true +end + +def owner_name(file) + mswin? ? + $svn_owner : + Etc.getpwuid( File.stat(file).uid ).name +end + +def mswin? + (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) +end + +projects.each do |project| + log("treating project #{project.name}", :level => 1) + + if project.identifier.empty? + log("\tno identifier for project #{project.name}") + next + elsif not project.identifier.match(/^[a-z0-9\-]+$/) + log("\tinvalid identifier for project #{project.name} : #{project.identifier}"); + next; + end + + repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR) + + create_repos = false + + # Logic required for SoundSoftware.ac.uk repositories: + # + # * If the project has a repository path declared already, + # - if it's a local path, + # - if it does not exist + # - if it has the right root + # - create it + # - else + # - leave alone (remote repository) + # * else + # - create repository with same name as project + # - set to project + + if project.respond_to?(:repository) + + repos_url = project.repository.url; + log("\texisting url for project #{project.identifier} is #{repos_url}"); + + if repos_url.match(/^file:\//) || repos_url.match(/^\//) + + repos_url = repos_url.gsub(/^file:\/*/, "/"); + log("\tthis is a local file path, at #{repos_url}"); + + if repos_url.slice(0, $repos_base.length) != $repos_base + log("\tit is in the wrong place: replacing it"); + # leave repos_path set to our original suggestion + create_repos = true + else + if !File.directory?(repos_url) + log("\tit doesn't exist; we should create it"); + repos_path = repos_url + create_repos = true + else + log("\tit exists and is in the right place"); + end + end + else + log("\tthis is a remote path, leaving alone"); + end + else + log("\tproject #{project.identifier} has no repository registered") +# if File.directory?(repos_path) +# log("\trepository path #{repos_path} already exists, not creating") +# else + create_repos = true +# end + end + + if create_repos + + registration_url = repos_path + if $svn_url + registration_url = "#{$svn_url}#{project.identifier}" + end + + if $test + log("\tproposal: create repository #{repos_path}") + log("\tproposal: register repository #{repos_path} in Redmine with vendor #{$scm}, url #{registration_url}") + next + end + +# No -- we need "other" users to be able to read it. Access control +# is not handled through Unix user id anyway +# project.is_public ? File.umask(0002) : File.umask(0007) + File.umask(0002) + + log("\taction: create repository #{repos_path}") + + begin + if !File.directory?(repos_path) + set_owner_and_rights(project, repos_path) do + if scm_module.nil? + log("\trunning command: #{$command} #{repos_path}") + system_or_raise "#{$command} #{repos_path}" + else + scm_module.create(repos_path) + end + end + end + rescue => e + log("\tunable to create #{repos_path} : #{e}\n") + next + end + + begin + log("\taction: register repository #{repos_path} in Redmine with vendor #{$scm}, url #{registration_url}"); + project.post(:repository, :vendor => $scm, :repository => {:url => "#{registration_url}"}, :key => $api_key) + rescue => e + log("\trepository #{repos_path} not registered in Redmine: #{e.message}"); + end + + log("\trepository #{repos_path} created"); + end + +end + diff -r 594ed6aef7bd -r 752184172a34 extra/svn/SoundSoftware.pm --- a/extra/svn/SoundSoftware.pm Thu Feb 03 15:31:33 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,422 +0,0 @@ -package Apache::Authn::SoundSoftware; - -=head1 Apache::Authn::SoundSoftware - -SoundSoftware - a mod_perl module for Apache authentication against a -Redmine database and optional LDAP implementing the access control -rules required for the SoundSoftware.ac.uk repository site. - -=head1 SYNOPSIS - -This module is closely based on the Redmine.pm authentication module -provided with Redmine. It is intended to be used for authentication -in front of a repository service such as hgwebdir. - -Requirements: - -1. Clone/pull from repo for public project: Any user, no -authentication required - -2. Clone/pull from repo for private project: Project members only - -3. Push to repo for public project: "Permitted" users only (this -probably means project members who are also identified in the hgrc web -section for the repository and so will be approved by hgwebdir?) - -4. Push to repo for private project: "Permitted" users only (as above) - -=head1 INSTALLATION - -Debian/ubuntu: - - apt-get install libapache-dbi-perl libapache2-mod-perl2 \ - libdbd-mysql-perl libauthen-simple-ldap-perl libio-socket-ssl-perl - -Note that LDAP support is hardcoded "on" in this script (it is -optional in the original Redmine.pm). - -=head1 CONFIGURATION - - ## This module has to be in your perl path - ## eg: /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm - PerlLoadModule Apache::Authn::SoundSoftware - - # Example when using hgwebdir - ScriptAlias / "/var/hg/hgwebdir.cgi/" - - - AuthName "Mercurial" - AuthType Basic - Require valid-user - PerlAccessHandler Apache::Authn::SoundSoftware::access_handler - PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler - SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost" - SoundSoftwareDbUser "redmine" - SoundSoftwareDbPass "password" - Options +ExecCGI - AddHandler cgi-script .cgi - ## Optional where clause (fulltext search would be slow and - ## database dependant). - # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)" - ## Optional prefix for local repository URLs - # SoundSoftwareRepoPrefix "/var/hg/" - - -See the original Redmine.pm for further configuration notes. - -=cut - -use strict; -use warnings FATAL => 'all', NONFATAL => 'redefine'; - -use DBI; -use Digest::SHA1; -use Authen::Simple::LDAP; -use Apache2::Module; -use Apache2::Access; -use Apache2::ServerRec qw(); -use Apache2::RequestRec qw(); -use Apache2::RequestUtil qw(); -use Apache2::Const qw(:common :override :cmd_how); -use APR::Pool (); -use APR::Table (); - -my @directives = ( - { - name => 'SoundSoftwareDSN', - req_override => OR_AUTHCFG, - args_how => TAKE1, - errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"', - }, - { - name => 'SoundSoftwareDbUser', - req_override => OR_AUTHCFG, - args_how => TAKE1, - }, - { - name => 'SoundSoftwareDbPass', - req_override => OR_AUTHCFG, - args_how => TAKE1, - }, - { - name => 'SoundSoftwareDbWhereClause', - req_override => OR_AUTHCFG, - args_how => TAKE1, - }, - { - name => 'SoundSoftwareRepoPrefix', - req_override => OR_AUTHCFG, - args_how => TAKE1, - }, -); - -sub SoundSoftwareDSN { - my ($self, $parms, $arg) = @_; - $self->{SoundSoftwareDSN} = $arg; - my $query = "SELECT - hashed_password, auth_source_id, permissions - FROM members, projects, users, roles, member_roles - WHERE - projects.id=members.project_id - AND member_roles.member_id=members.id - AND users.id=members.user_id - AND roles.id=member_roles.role_id - AND users.status=1 - AND login=? - AND identifier=? "; - $self->{SoundSoftwareQuery} = trim($query); -} - -sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); } -sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); } -sub SoundSoftwareDbWhereClause { - my ($self, $parms, $arg) = @_; - $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." "); -} - -sub SoundSoftwareRepoPrefix { - my ($self, $parms, $arg) = @_; - if ($arg) { - $self->{SoundSoftwareRepoPrefix} = $arg; - } -} - -sub trim { - my $string = shift; - $string =~ s/\s{2,}/ /g; - return $string; -} - -sub set_val { - my ($key, $self, $parms, $arg) = @_; - $self->{$key} = $arg; -} - -Apache2::Module::add(__PACKAGE__, \@directives); - - -my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; - -sub access_handler { - my $r = shift; - - print STDERR "SoundSoftware.pm: In access handler\n"; - - unless ($r->some_auth_required) { - $r->log_reason("No authentication has been configured"); - return FORBIDDEN; - } - - my $method = $r->method; - - print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n"; - print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n"; - - if (!defined $read_only_methods{$method}) { - print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n"; - return OK; - } - - my $dbh = connect_database($r); - - my $project_id = get_project_identifier($dbh, $r); - my $status = get_project_status($dbh, $project_id, $r); - - $dbh->disconnect(); - undef $dbh; - - if ($status == 0) { # nonexistent - print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n"; - return FORBIDDEN; - } elsif ($status == 1) { # public - print STDERR "SoundSoftware.pm: Project is public, no restriction here\n"; - $r->set_handlers(PerlAuthenHandler => [\&OK]) - } else { # private - print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n"; - } - - return OK -} - -sub authen_handler { - my $r = shift; - - print STDERR "SoundSoftware.pm: In authentication handler\n"; - - my $dbh = connect_database($r); - - my $project_id = get_project_identifier($dbh, $r); - my $realm = get_realm($dbh, $project_id, $r); - $r->auth_name($realm); - - my ($res, $redmine_pass) = $r->get_basic_auth_pw(); - unless ($res == OK) { - $dbh->disconnect(); - undef $dbh; - return $res; - } - - print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n"; - - my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r); - - $dbh->disconnect(); - undef $dbh; - - if ($permitted) { - return OK; - } else { - print STDERR "SoundSoftware.pm: Not permitted\n"; - $r->note_auth_failure(); - return AUTH_REQUIRED; - } -} - -sub get_project_status { - my $dbh = shift; - my $project_id = shift; - my $r = shift; - - if (!defined $project_id or $project_id eq '') { - return 0; # nonexistent - } - - my $sth = $dbh->prepare( - "SELECT is_public FROM projects WHERE projects.identifier = ?;" - ); - - $sth->execute($project_id); - my $ret = 0; # nonexistent - if (my @row = $sth->fetchrow_array) { - if ($row[0] eq "1" || $row[0] eq "t") { - $ret = 1; # public - } else { - $ret = 2; # private - } - } - $sth->finish(); - undef $sth; - - $ret; -} - -sub is_permitted { - my $dbh = shift; - my $project_id = shift; - my $redmine_user = shift; - my $redmine_pass = shift; - my $r = shift; - - my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); - - my $cfg = Apache2::Module::get_config - (__PACKAGE__, $r->server, $r->per_dir_config); - - my $query = $cfg->{SoundSoftwareQuery}; - my $sth = $dbh->prepare($query); - $sth->execute($redmine_user, $project_id); - - my $ret; - while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) { - - # Test permissions for this user before we verify credentials - # -- if the user is not permitted this action anyway, there's - # not much point in e.g. contacting the LDAP - - my $method = $r->method; - - if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) - || $permissions =~ /:commit_access/) { - - # User would be permitted this action, if their - # credentials checked out -- test those now - - print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n"; - - unless ($auth_source_id) { - if ($hashed_password eq $pass_digest) { - print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n"; - $ret = 1; - last; - } - } else { - my $sthldap = $dbh->prepare( - "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;" - ); - $sthldap->execute($auth_source_id); - while (my @rowldap = $sthldap->fetchrow_array) { - my $ldap = Authen::Simple::LDAP->new( - host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], - port => $rowldap[1], - basedn => $rowldap[5], - binddn => $rowldap[3] ? $rowldap[3] : "", - bindpw => $rowldap[4] ? $rowldap[4] : "", - filter => "(".$rowldap[6]."=%s)" - ); - if ($ldap->authenticate($redmine_user, $redmine_pass)) { - print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n"; - $ret = 1; - } - } - $sthldap->finish(); - undef $sthldap; - } - } else { - print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n"; - } - } - - $sth->finish(); - undef $sth; - - $ret; -} - -sub get_project_identifier { - my $dbh = shift; - my $r = shift; - - my $location = $r->location; - my ($repo) = $r->uri =~ m{$location/*([^/]+)}; - - return $repo if (!$repo); - - $repo =~ s/[^a-zA-Z0-9\._-]//g; - - # The original Redmine.pm returns the string just calculated as - # the project identifier. That won't do for us -- we may have - # (and in fact already do have, in our test instance) projects - # whose repository names differ from the project identifiers. - - # This is a rather fundamental change because it means that almost - # every request needs more than one database query -- which - # prompts us to start passing around $dbh instead of connecting - # locally within each function as is done in Redmine.pm. - - my $sth = $dbh->prepare( - "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;" - ); - - my $cfg = Apache2::Module::get_config - (__PACKAGE__, $r->server, $r->per_dir_config); - - my $prefix = $cfg->{SoundSoftwareRepoPrefix}; - if (!defined $prefix) { $prefix = '%/'; } - - my $identifier = ''; - - $sth->execute($prefix . $repo); - my $ret = 0; - if (my @row = $sth->fetchrow_array) { - $identifier = $row[0]; - } - $sth->finish(); - undef $sth; - - print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n"; - - $identifier; -} - -sub get_realm { - my $dbh = shift; - my $project_id = shift; - my $r = shift; - - my $sth = $dbh->prepare( - "SELECT projects.name FROM projects WHERE projects.identifier = ?;" - ); - - my $name = $project_id; - - $sth->execute($project_id); - my $ret = 0; - if (my @row = $sth->fetchrow_array) { - $name = $row[0]; - } - $sth->finish(); - undef $sth; - - # be timid about characters not permitted in auth realm and revert - # to project identifier if any are found - if ($name =~ m/[^\w\d\s\._-]/) { - $name = $project_id; - } - - my $realm = '"Mercurial repository for ' . "'$name'" . '"'; - - $realm; -} - -sub connect_database { - my $r = shift; - - my $cfg = Apache2::Module::get_config - (__PACKAGE__, $r->server, $r->per_dir_config); - - return DBI->connect($cfg->{SoundSoftwareDSN}, - $cfg->{SoundSoftwareDbUser}, - $cfg->{SoundSoftwareDbPass}); -} - -1; diff -r 594ed6aef7bd -r 752184172a34 extra/svn/reposman-soundsoftware.rb --- a/extra/svn/reposman-soundsoftware.rb Thu Feb 03 15:31:33 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,350 +0,0 @@ -#!/usr/bin/env ruby - -# == Synopsis -# -# reposman: manages your repositories with Redmine -# -# == Usage -# -# reposman [OPTIONS...] -s [DIR] -r [HOST] -# -# Examples: -# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net --scm subversion -# reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git -# -# == Arguments (mandatory) -# -# -s, --svn-dir=DIR use DIR as base directory for svn repositories -# -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples: -# -r redmine.example.net -# -r http://redmine.example.net -# -r https://example.net/redmine -# -k, --key=KEY use KEY as the Redmine API key -# -# == Options -# -# -o, --owner=OWNER owner of the repository. using the rails login -# allow user to browse the repository within -# Redmine even for private project. If you want to -# share repositories through Redmine.pm, you need -# to use the apache owner. -# -g, --group=GROUP group of the repository. (default: root) -# --scm=SCM the kind of SCM repository you want to create (and -# register) in Redmine (default: Subversion). -# reposman is able to create Git and Subversion -# repositories. For all other kind, you must specify -# a --command option -# -u, --url=URL the base url Redmine will use to access your -# repositories. This option is used to automatically -# register the repositories in Redmine. The project -# identifier will be appended to this url. Examples: -# -u https://example.net/svn -# -u file:///var/svn/ -# if this option isn't set, reposman will register -# the repositories with local file paths in Redmine -# -c, --command=COMMAND use this command instead of "svnadmin create" to -# create a repository. This option can be used to -# create repositories other than subversion and git -# kind. -# This command override the default creation for git -# and subversion. -# --http-user=USER User for HTTP Basic authentication with Redmine WS -# --http-pass=PASSWORD Password for Basic authentication with Redmine WS -# -t, --test only show what should be done -# -h, --help show help and exit -# -v, --verbose verbose -# -V, --version print version and exit -# -q, --quiet no log -# -# == References -# -# You can find more information on the redmine's wiki : http://www.redmine.org/wiki/redmine/HowTos - - -require 'getoptlong' -require 'rdoc/usage' -require 'find' -require 'etc' - -Version = "1.3" -SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem ) - -opts = GetoptLong.new( - ['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], - ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], - ['--key', '-k', GetoptLong::REQUIRED_ARGUMENT], - ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT], - ['--group', '-g', GetoptLong::REQUIRED_ARGUMENT], - ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT], - ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT], - ['--scm', GetoptLong::REQUIRED_ARGUMENT], - ['--http-user', GetoptLong::REQUIRED_ARGUMENT], - ['--http-pass', GetoptLong::REQUIRED_ARGUMENT], - ['--test', '-t', GetoptLong::NO_ARGUMENT], - ['--verbose', '-v', GetoptLong::NO_ARGUMENT], - ['--version', '-V', GetoptLong::NO_ARGUMENT], - ['--help' , '-h', GetoptLong::NO_ARGUMENT], - ['--quiet' , '-q', GetoptLong::NO_ARGUMENT] - ) - -$verbose = 0 -$quiet = false -$redmine_host = '' -$repos_base = '' -$http_user = '' -$http_pass = '' -$svn_owner = 'root' -$svn_group = 'root' -$use_groupid = true -$svn_url = false -$test = false -$scm = 'Subversion' - -def log(text, options={}) - level = options[:level] || 0 - puts text unless $quiet or level > $verbose - exit 1 if options[:exit] -end - -def system_or_raise(command) - raise "\"#{command}\" failed" unless system command -end - -module SCM - - module Subversion - def self.create(path) - system_or_raise "svnadmin create #{path}" - end - end - - module Git - def self.create(path) - Dir.mkdir path - Dir.chdir(path) do - system_or_raise "git --bare init --shared" - system_or_raise "git update-server-info" - end - end - end - -end - -begin - opts.each do |opt, arg| - case opt - when '--svn-dir'; $repos_base = arg.dup - when '--redmine-host'; $redmine_host = arg.dup - when '--key'; $api_key = arg.dup - when '--owner'; $svn_owner = arg.dup; $use_groupid = false; - when '--group'; $svn_group = arg.dup; $use_groupid = false; - when '--url'; $svn_url = arg.dup - when '--scm'; $scm = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm) - when '--http-user'; $http_user = arg.dup - when '--http-pass'; $http_pass = arg.dup - when '--command'; $command = arg.dup - when '--verbose'; $verbose += 1 - when '--test'; $test = true - when '--version'; puts Version; exit - when '--help'; RDoc::usage - when '--quiet'; $quiet = true - end - end -rescue - exit 1 -end - -if $test - log("running in test mode") -end - -# Make sure command is overridden if SCM vendor is not handled internally (for the moment Subversion and Git) -if $command.nil? - begin - scm_module = SCM.const_get($scm) - rescue - log("Please use --command option to specify how to create a #{$scm} repository.", :exit => true) - end -end - -$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/) - -if ($redmine_host.empty? or $repos_base.empty?) - RDoc::usage -end - -unless File.directory?($repos_base) - log("directory '#{$repos_base}' doesn't exists", :exit => true) -end - -begin - require 'active_resource' -rescue LoadError - log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true) -end - -class Project < ActiveResource::Base - self.headers["User-agent"] = "Redmine repository manager/#{Version}" -end - -log("querying Redmine for projects...", :level => 1); - -$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://") -$redmine_host.gsub!(/\/$/, '') - -Project.site = "#{$redmine_host}/sys"; -Project.user = $http_user; -Project.password = $http_pass; - -begin - # Get all active projects that have the Repository module enabled - projects = Project.find(:all, :params => {:key => $api_key}) -rescue => e - log("Unable to connect to #{Project.site}: #{e}", :exit => true) -end - -if projects.nil? - log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true) -end - -log("retrieved #{projects.size} projects", :level => 1) - -def set_owner_and_rights(project, repos_path, &block) - if RUBY_PLATFORM =~ /mswin/ - yield if block_given? - else - uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid) - right = project.is_public ? 02775 : 02770 - yield if block_given? - Find.find(repos_path) do |f| - File.chmod right, f - File.chown uid, gid, f - end - end -end - -def other_read_right?(file) - (File.stat(file).mode & 0007).zero? ? false : true -end - -def owner_name(file) - mswin? ? - $svn_owner : - Etc.getpwuid( File.stat(file).uid ).name -end - -def mswin? - (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) -end - -projects.each do |project| - log("treating project #{project.name}", :level => 1) - - if project.identifier.empty? - log("\tno identifier for project #{project.name}") - next - elsif not project.identifier.match(/^[a-z0-9\-]+$/) - log("\tinvalid identifier for project #{project.name} : #{project.identifier}"); - next; - end - - repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR) - - create_repos = false - - # Logic required for SoundSoftware.ac.uk repositories: - # - # * If the project has a repository path declared already, - # - if it's a local path, - # - if it does not exist - # - if it has the right root - # - create it - # - else - # - leave alone (remote repository) - # * else - # - create repository with same name as project - # - set to project - - if project.respond_to?(:repository) - - repos_url = project.repository.url; - log("\texisting url for project #{project.identifier} is #{repos_url}"); - - if repos_url.match(/^file:\//) || repos_url.match(/^\//) - - repos_url = repos_url.gsub(/^file:\/*/, "/"); - log("\tthis is a local file path, at #{repos_url}"); - - if repos_url.slice(0, $repos_base.length) != $repos_base - log("\tit is in the wrong place: replacing it"); - # leave repos_path set to our original suggestion - create_repos = true - else - if !File.directory?(repos_url) - log("\tit doesn't exist; we should create it"); - repos_path = repos_url - create_repos = true - else - log("\tit exists and is in the right place"); - end - end - else - log("\tthis is a remote path, leaving alone"); - end - else - log("\tproject #{project.identifier} has no repository registered") -# if File.directory?(repos_path) -# log("\trepository path #{repos_path} already exists, not creating") -# else - create_repos = true -# end - end - - if create_repos - - registration_url = repos_path - if $svn_url - registration_url = "#{$svn_url}#{project.identifier}" - end - - if $test - log("\tproposal: create repository #{repos_path}") - log("\tproposal: register repository #{repos_path} in Redmine with vendor #{$scm}, url #{registration_url}") - next - end - -# No -- we need "other" users to be able to read it. Access control -# is not handled through Unix user id anyway -# project.is_public ? File.umask(0002) : File.umask(0007) - File.umask(0002) - - log("\taction: create repository #{repos_path}") - - begin - if !File.directory?(repos_path) - set_owner_and_rights(project, repos_path) do - if scm_module.nil? - log("\trunning command: #{$command} #{repos_path}") - system_or_raise "#{$command} #{repos_path}" - else - scm_module.create(repos_path) - end - end - end - rescue => e - log("\tunable to create #{repos_path} : #{e}\n") - next - end - - begin - log("\taction: register repository #{repos_path} in Redmine with vendor #{$scm}, url #{registration_url}"); - project.post(:repository, :vendor => $scm, :repository => {:url => "#{registration_url}"}, :key => $api_key) - rescue => e - log("\trepository #{repos_path} not registered in Redmine: #{e.message}"); - end - - log("\trepository #{repos_path} created"); - end - -end - diff -r 594ed6aef7bd -r 752184172a34 public/themes/soundsoftware/images/home.png Binary file public/themes/soundsoftware/images/home.png has changed diff -r 594ed6aef7bd -r 752184172a34 public/themes/soundsoftware/stylesheets/application.css --- a/public/themes/soundsoftware/stylesheets/application.css Thu Feb 03 15:31:33 2011 +0000 +++ b/public/themes/soundsoftware/stylesheets/application.css Thu Feb 03 15:34:57 2011 +0000 @@ -26,7 +26,7 @@ color: #000; margin: 0; margin-bottom: 40px; -/* font-size: 95%; */ + min-width: 620px; } h1 { @@ -67,6 +67,20 @@ tr.entry { border-left: 1px solid #a9b680; border-right: 1px solid #a9b680; } tr.entry:last-child { border-bottom: 1px solid #a9b680; } +table.projects th { text-align: left; } +table.projects th.managers { color: #3e442c; } +table.projects .root .name { font-size: 1.2em; padding-top: 0.3em; } +table.projects .description { padding-bottom: 0.5em; } +table.projects .no_description { padding-bottom: 0.5em; } +table.projects .hosted_here { font-weight: bold; } +table.projects .child .name { font-weight: normal; } +table.projects .child .description { font-size: 0.95em; } +table.projects .child .firstcol { padding-left: 1em } +table.projects .level2 .firstcol { padding-left: 2em; } +table.projects .level3 .firstcol { padding-left: 3em; } + +ul.projects .public, ul.projects .private { padding-left: 0.5em; color: #3e442c; font-size: 0.95em } + #top-menu { position: absolute; top: 0; z-index: 1; left: 0px; width: 100%; font-size: 90%; /* height: 2em; */ margin: 0; padding: 0; padding-top: 0.5em; background-color: #3e442c; } #top-menu ul { margin-left: 10px; } #top-menu a { font-weight: bold; } @@ -74,9 +88,9 @@ #header a { color: #be5700; } #header h1 { color: #525a38; margin-top: 25px; font-size: 3em; font-weight: normal; margin-left: 10px; } .header-general h1 { - background: url('soundsoftware-logo-title-only-transparent.png') no-repeat 0 0; + background: url('soundsoftware-logo-title-only-transparent-beta.png') no-repeat 0 0; text-indent: -9999px; - width: 446px; + width: 500px; height: 34px; } diff -r 594ed6aef7bd -r 752184172a34 public/themes/soundsoftware/stylesheets/soundsoftware-logo-title-only-transparent-beta.png Binary file public/themes/soundsoftware/stylesheets/soundsoftware-logo-title-only-transparent-beta.png has changed diff -r 594ed6aef7bd -r 752184172a34 vendor/plugins/redmine_checkout/config/locales/en-GB.yml --- a/vendor/plugins/redmine_checkout/config/locales/en-GB.yml Thu Feb 03 15:31:33 2011 +0000 +++ b/vendor/plugins/redmine_checkout/config/locales/en-GB.yml Thu Feb 03 15:34:57 2011 +0000 @@ -1,4 +1,4 @@ -en: +en-GB: label_checkout: "Checkout" setting_checkout_display_checkout_info: "Display checkout information"