Initial Redmine tooling and local plugin forks

This commit is contained in:
Jason Thistlethwaite
2026-04-24 22:01:18 +00:00
commit 9f682af0eb
683 changed files with 56878 additions and 0 deletions
@@ -0,0 +1,106 @@
class CannedResponsesController < ApplicationController
unloadable
before_filter :find_canned_response, :except => [:new, :create, :index]
before_filter :find_optional_project, :only => [:new, :create, :add, :destroy]
before_filter :find_issue, :only => [:add]
before_filter :require_admin, :only => [:index]
accept_api_auth :index
def index
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
scope = CannedResponse.visible
scope = scope.in_project_or_public(@project) if @project
@canned_response_count = scope.count
@canned_response_pages = Paginator.new @canned_response_count, @limit, params['page']
@offset ||= @canned_response_pages.offset
@canned_responses = scope.limit(@limit).offset(@offset).order("#{CannedResponse.table_name}.name")
respond_to do |format|
format.html
end
end
def add
@content = HelpdeskMailer.apply_macro(@canned_response.content, @issue.customer, @issue, User.current)
end
def new
@canned_response = CannedResponse.new
@canned_response.user = User.current
@canned_response.project = @project
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
end
def create
@canned_response = CannedResponse.new(params[:canned_response])
@canned_response.user = User.current
@canned_response.project = params[:canned_response_is_for_all] ? nil : @project
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
if @canned_response.save
flash[:notice] = l(:notice_successful_create)
redirect_to_project_or_global
else
render :action => 'new', :layout => !request.xhr?
end
end
def edit
end
def update
@canned_response.attributes = params[:canned_response]
@canned_response.project = nil if params[:canned_response_is_for_all]
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
if @canned_response.save
flash[:notice] = l(:notice_successful_update)
redirect_to_project_or_global
else
render :action => 'edit'
end
end
def destroy
@canned_response.destroy
redirect_to_project_or_global
end
private
def redirect_to_project_or_global
redirect_to @project ? settings_project_path(@project, :tab => 'helpdesk_canned_responses') : path_to_global_setting
end
def path_to_global_setting
{
:action =>"plugin",
:id => "redmine_contacts_helpdesk",
:controller => "settings",
:tab => 'canned_responses'
}
end
def find_issue
@issue = Issue.find(params[:issue_id])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_canned_response
@canned_response = CannedResponse.find(params[:id])
@project = @canned_response.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,244 @@
class HelpdeskController < ApplicationController
unloadable
before_filter :find_project, :authorize, :except => [:email_note, :update_customer_email]
accept_api_auth :email_note, :create_ticket
def save_settings
if request.put?
set_settings
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => params[:tab] || 'helpdesk', :id => @project
end
def show_original
@attachment = Attachment.find(params[:id])
email = Mail.read(@attachment.diskfile)
part = email.text_part || email.html_part || email
body_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset
plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, body_charset)
headers = email.header.fields.map{|f| "#{f.name}: #{Mail::Encodings.unquote_and_convert_to(f.value, 'utf-8')}"}.join("\n")
@content = headers + "\n\n" + plain_text_body
render "attachments/file"
end
def delete_spam
if User.current.allowed_to?(:delete_issues, @project) && User.current.allowed_to?(:delete_contacts, @project)
begin
@issue = Issue.find(params[:issue_id])
@customer = @issue.customer
rescue ActiveRecord::RecordNotFound
render_404
end
ActiveRecord::Base.transaction do
ContactsSetting["helpdesk_blacklist", @project.id] = (ContactsSetting["helpdesk_blacklist", @project.id].split("\n") | [@issue.customer.primary_email.strip]).join("\n")
@customer.tickets.map(&:destroy)
@customer.destroy
end
respond_to do |format|
format.html { redirect_back_or_default(:controller => 'issues', :action => 'index', :project_id => @project) }
format.api { render_api_ok }
end
else
deny_access
end
end
def email_note
raise Exception, "Param 'message' should be set" unless params[:message]
@issue = Issue.find(params[:message][:issue_id])
raise Exception, "Issue with ID: #{params[:message][:issue_id].to_i} should be present and relate to customer" if @issue.nil? || @issue.customer.nil?
@journal = @issue.init_journal(User.current)
@issue.status_id = params[:message][:status_id] if params[:message][:status_id].blank? && IssueStatus.find_by_id(params[:message][:status_id])
@journal.notes = params[:message][:content]
@issue.save!
contact = @issue.customer
HelpdeskMailer.with_activated_perform_deliveries do
if HelpdeskMailer.issue_response(contact, @journal, params).deliver
@journal_message = JournalMessage.create(:from_address => "",
:to_address => contact.primary_email.downcase,
:is_incoming => false,
:message_date => Time.now,
:contact => contact,
:journal => @journal)
end
end
respond_to do |format|
format.api { render :action => 'show', :status => :created }
end
rescue Exception => e
respond_to do |format|
format.api do
@error_messages = [e.message]
render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
end
end
end
def create_ticket
raise Exception, "Param 'ticket' should be set" if params[:ticket].blank?
@issue = Issue.new
@issue.project = @project
@issue.author ||= User.current
@issue.safe_attributes = params[:ticket][:issue]
raise Exception, "Contact should have email address" unless params[:ticket][:contact] || params[:ticket][:contact][:email]
@contact = Contact.find_by_emails([params[:ticket][:contact][:email]]).first
@contact ||= Contact.new(params[:ticket][:contact])
@contact.projects << @project
helpdesk_ticket = HelpdeskTicket.new(:from_address => @contact.primary_email,
:to_address => '',
:ticket_date => Time.now,
:customer => @contact,
:is_incoming => true,
:issue => @issue,
:source => HelpdeskTicket::HELPDESK_WEB_SOURCE)
@issue.helpdesk_ticket = helpdesk_ticket
@issue.assigned_to = @contact.find_assigned_user(@project, @issue.assigned_to)
@issue.save_attachments(params[:attachments] || (params[:ticket][:issue] && params[:ticket][:issue][:uploads]))
if @issue.save
HelpdeskMailer.auto_answer(@contact, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0
respond_to do |format|
format.api { redirect_on_create(params) }
end
else
raise Exception, "Can't create issue: #{@issue.errors.full_messages}"
end
rescue Exception => e
respond_to do |format|
format.api do
@error_messages = [e.message]
HelpdeskLogger.error "API Create Ticket Error: #{e.message}" if HelpdeskLogger
render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
end
end
end
def get_mail
set_settings
msg_count = HelpdeskMailer.check_project(@project.id)
respond_to do |format|
format.js do
@message = "<div class='flash notice'> #{l(:label_helpdesk_get_mail_success, :count => msg_count)} </div>"
flash.discard
end
format.html {redirect_to :back}
end
rescue Exception => e
respond_to do |format|
format.js do
@message = "<div class='flash error'> Error: #{e.message} </div>"
Rails.logger.error "Helpdesk MailHandler Error: #{e.message}" if Rails.logger && Rails.logger.error
flash.discard
end
format.html {redirect_to :back}
end
end
def update_customer_email
@journal = Journal.find(params[:journal_id])
@issue = @journal.journalized
@project = @issue.project
@display = HelpdeskSettings[:send_note_by_default, @project] ? 'inline' : 'none'
if @journal.is_incoming?
@contact = @journal.contact
@email = @journal.journal_message.from_address
from_address = HelpdeskSettings['helpdesk_answer_from', @issue.project].blank? ? Setting.mail_from : HelpdeskSettings['helpdesk_answer_from', @issue.project]
@cc_emails = (@issue.helpdesk_ticket.cc_addresses + @journal.journal_message.cc_address.split(',') - [@email, from_address]).uniq
else
@contact = @issue.helpdesk_ticket.last_reply_customer
@email = @issue.helpdesk_ticket.default_to_address
@cc_emails = @issue.helpdesk_ticket.cc_addresses - [@email]
end
end
private
def find_project
project_id = params[:project_id] || (params[:ticket] && params[:ticket][:issue] && params[:ticket][:issue][:project_id])
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def set_settings
set_settings_param("helpdesk_answer_from")
set_settings_param("helpdesk_send_notification")
set_settings_param("helpdesk_is_not_create_contacts")
set_settings_param("helpdesk_created_contact_tag")
set_settings_param("helpdesk_blacklist")
set_settings_param("helpdesk_emails_header")
set_settings_param("helpdesk_answer_subject")
set_settings_param("helpdesk_first_answer_subject")
set_settings_param("helpdesk_first_answer_template")
set_settings_param("helpdesk_emails_footer")
set_settings_param("helpdesk_answered_status")
set_settings_param("helpdesk_reopen_status")
set_settings_param("helpdesk_tracker")
set_settings_param("helpdesk_assigned_to")
set_settings_param("helpdesk_lifetime")
set_settings_param(:helpdesk_protocol)
set_settings_param(:helpdesk_host)
set_settings_param(:helpdesk_port)
set_settings_param(:helpdesk_password)
set_settings_param(:helpdesk_username)
set_settings_param(:helpdesk_use_ssl)
set_settings_param(:helpdesk_imap_folder)
set_settings_param(:helpdesk_move_on_success)
set_settings_param(:helpdesk_move_on_failure)
set_settings_param(:helpdesk_apop)
set_settings_param(:helpdesk_delete_unprocessed)
set_settings_param(:helpdesk_smtp_use_default_settings)
set_settings_param(:helpdesk_smtp_server)
set_settings_param(:helpdesk_smtp_domain)
set_settings_param(:helpdesk_smtp_port)
set_settings_param(:helpdesk_smtp_authentication)
set_settings_param(:helpdesk_smtp_username)
set_settings_param(:helpdesk_smtp_password)
set_settings_param(:helpdesk_smtp_tls)
set_settings_param(:helpdesk_smtp_ssl)
end
def set_settings_param(param)
if param == :helpdesk_password || param == :helpdesk_smtp_password
ContactsSetting[param, @project.id] = params[param] if params[param] && !params[param].blank?
else
ContactsSetting[param, @project.id] = params[param] if params[param]
end
end
def redirect_on_create(options)
if options[:redirect_on_success].to_s.match('^(http|https):\/\/')
redirect_to options[:redirect_on_success].to_s
else
render :text => "Issue #{@issue.id} created", :status => :created, :location => issue_url(@issue)
end
end
end
@@ -0,0 +1,44 @@
class HelpdeskMailerController < ActionController::Base
unloadable
before_filter :check_credential
# Submits an incoming email to ContactsMailer
def index
options = params.dup
if options[:issue].present?
project = Project.find_by_identifier(options[:issue][:project])
options = HelpdeskMailer.get_issue_options(options, project.id) if project
end
email = options.delete(:email)
if HelpdeskMailer.receive(email, options)
render :nothing => true, :status => :created
else
render :nothing => true, :status => :unprocessable_entity
end
end
def get_mail
msg_count = 0
errors = []
Project.active.has_module(:contacts_helpdesk).each do |project|
begin
msg_count += HelpdeskMailer.check_project(project.id)
rescue Exception => e
errors << e.message
end
end
render :status => :ok, :text => {:count => msg_count, :errors => errors}.to_json
end
private
def check_credential
User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
end
end
end
@@ -0,0 +1,35 @@
class HelpdeskReportsController < ApplicationController
unloadable
menu_item :issues
helper :helpdesk
helper :queries
include QueriesHelper
before_filter :find_optional_project, :authorize_global
def show
retrieve_reports_query
@collector = HelpdeskDataCollectorManager.new(@report).collect_data(@query)
return render_404 unless @collector
respond_to do |format|
format.html
end
end
private
def retrieve_reports_query
@report = params[:report] || 'first_response_time'
report_query_class = @report == 'first_response_time' ? HelpdeskReportsFirstResponseQuery : HelpdeskReportsBusiestTimeQuery
if params[:set_filter] || session[:helpdesk_reports_query].nil? || session[:helpdesk_reports_query][:project_id] != (@project ? @project.id : nil)
@query = report_query_class.new(:name => '_', :project => @project)
params.merge!('f' => ['message_date'], 'op' => { 'message_date' => 'm' }) if params['f'].nil? || params['f'].all?(&:blank?)
@query.build_from_params(params)
@query[:filters] = { 'message_date' => { :operator => 'm', :values => [''] } } unless @query[:filters]
session[:helpdesk_reports_query] = { :project_id => @query.project_id, :filters => @query.filters || {} }
else
@query = report_query_class.new(:name => '_', :project => @project, :filters => session[:helpdesk_reports_query][:filters] || {})
end
end
end
@@ -0,0 +1,201 @@
class HelpdeskSearchController < ApplicationController
unloadable
accept_api_auth :ticket_by_issue, :issues_by_contact, :messages_by_issue, :contact_timeline
before_filter :require_login
def usage
# Human/browser probes often stop at /helpdesk_search/issues. Return a
# machine-readable usage response instead of letting Rails raise a route
# exception that looks like an application failure in production.log.
render :json => {
:helpdesk_search => {
:ticket_by_issue => '/helpdesk_search/issues/:issue_id/ticket',
:ticket_by_issue_alias => '/helpdesk_search/issues/:issue_id',
:issues_by_contact => '/helpdesk_search/contacts/:contact_id/issues',
:messages_by_issue => '/helpdesk_search/issues/:issue_id/messages',
:contact_timeline => '/helpdesk_search/contacts/:contact_id/timeline'
}
}, :status => :bad_request
end
def ticket_by_issue
issue = Issue.find(params[:issue_id])
return unless authorize_helpdesk_project!(issue.project)
ticket = HelpdeskTicket.where(:issue_id => issue.id).first
unless ticket
render_404
return
end
render :json => {:helpdesk_ticket => serialize_ticket(ticket)}
rescue ActiveRecord::RecordNotFound
render_404
end
def issues_by_contact
contact = Contact.find(params[:contact_id])
tickets = HelpdeskTicket.
includes(:issue => [:project, :status, :tracker, :assigned_to]).
where(:contact_id => contact.id).
order("#{HelpdeskTicket.table_name}.ticket_date DESC").
limit(api_limit)
render :json => {
:contact_id => contact.id,
:issues => tickets.map { |ticket| serialize_ticket_issue(ticket) }.compact
}
rescue ActiveRecord::RecordNotFound
render_404
end
def messages_by_issue
issue = Issue.find(params[:issue_id])
return unless authorize_helpdesk_project!(issue.project)
messages = JournalMessage.
includes(:contact, :journal).
joins(:journal).
where(:journals => {:journalized_type => 'Issue', :journalized_id => issue.id}).
order("#{JournalMessage.table_name}.message_date ASC").
limit(api_limit)
render :json => {
:issue_id => issue.id,
:journal_messages => messages.map { |message| serialize_journal_message(message) }
}
rescue ActiveRecord::RecordNotFound
render_404
end
def contact_timeline
contact = Contact.find(params[:contact_id])
tickets = HelpdeskTicket.
includes(:issue => [:project, :status, :tracker]).
where(:contact_id => contact.id).
order("#{HelpdeskTicket.table_name}.ticket_date DESC").
limit(api_limit)
messages = JournalMessage.
includes(:contact, :journal).
where(:contact_id => contact.id).
order("#{JournalMessage.table_name}.message_date DESC").
limit(api_limit)
events = []
tickets.each do |ticket|
next unless visible_issue?(ticket.issue)
events << serialize_ticket(ticket).merge(:type => 'helpdesk_ticket', :date => iso8601(ticket.ticket_date))
end
messages.each do |message|
issue = message_issue(message)
next unless visible_issue?(issue)
events << serialize_journal_message(message).merge(:type => 'journal_message', :date => iso8601(message.message_date))
end
events.sort_by! { |event| event[:date].to_s }
events.reverse!
render :json => {
:contact_id => contact.id,
:timeline => events.first(api_limit)
}
rescue ActiveRecord::RecordNotFound
render_404
end
private
def authorize_helpdesk_project!(project)
# Search endpoints expose customer/email context, so they reuse the native
# per-project helpdesk visibility permission instead of adding a new role.
return true if project && User.current.allowed_to?(:view_helpdesk_tickets, project)
deny_access
false
end
def visible_issue?(issue)
issue && issue.project && User.current.allowed_to?(:view_helpdesk_tickets, issue.project)
end
def api_limit
requested_limit = params[:limit].present? ? params[:limit].to_i : 100
[[requested_limit, 1].max, 200].min
end
def serialize_ticket_issue(ticket)
issue = ticket.issue
return nil unless visible_issue?(issue)
serialize_ticket(ticket).merge(
:issue => {
:id => issue.id,
:project_id => issue.project_id,
:tracker_id => issue.tracker_id,
:tracker_name => issue.tracker.try(:name),
:status_id => issue.status_id,
:status_name => issue.status.try(:name),
:assigned_to_id => issue.assigned_to_id,
:assigned_to_name => issue.assigned_to.try(:name),
:subject => issue.subject,
:created_on => iso8601(issue.created_on),
:updated_on => iso8601(issue.updated_on)
}
)
end
def serialize_ticket(ticket)
{
:id => ticket.id,
:issue_id => ticket.issue_id,
:contact_id => ticket.contact_id,
:message_id => ticket.message_id,
:source => ticket.source,
:is_incoming => ticket.is_incoming?,
:from_address => ticket.from_address,
:to_address => ticket.to_address,
:cc_address => ticket.cc_address,
:ticket_date => iso8601(ticket.ticket_date)
}
end
def serialize_journal_message(message)
journal = message.journal
issue = message_issue(message)
# Keep this API metadata-only. The external indexer can fetch journal.notes
# with its own read policy; this endpoint should not leak message bodies,
# private notes, attachments, or BCC addresses by accident.
{
:id => message.id,
:journal_id => message.journal_id,
:issue_id => issue.try(:id),
:project_id => issue.try(:project_id),
:contact_id => message.contact_id,
:message_id => message.message_id,
:source => message.source,
:is_incoming => message.is_incoming?,
:from_address => message.from_address,
:to_address => message.to_address,
:cc_address => message.cc_address,
:has_bcc_address => message.bcc_address.present?,
:message_date => iso8601(message.message_date),
:journal_user_id => journal.try(:user_id),
:journal_private_notes => journal.try(:private_notes?),
:journal_has_notes => journal.try(:notes).present?
}
end
def message_issue(message)
journal = message.journal
return nil unless journal
journal.try(:issue) || journal.try(:journalized)
end
def iso8601(value)
value.try(:utc).try(:iso8601)
end
end
@@ -0,0 +1,67 @@
class HelpdeskTicketsController < ApplicationController
unloadable
before_filter :find_issue, :except => :destroy
before_filter :find_helpdesk_ticket, :only => :destroy
before_filter :authorize
helper :helpdesk
def edit
@show_form = "true"
respond_to do |format|
format.js
end
end
def update
@helpdesk_ticket.attributes = params[:helpdesk_ticket]
@helpdesk_ticket.cc_address = params[:helpdesk_ticket][:cc_address].reject(&:empty?).join(',') if params[:helpdesk_ticket][:cc_address]
@helpdesk_ticket.issue = @issue
@helpdesk_ticket.from_address = @helpdesk_ticket.customer.primary_email if @helpdesk_ticket.customer
if @helpdesk_ticket.save
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
format.api { render_api_ok }
end
else
flash[:error] = @helpdesk_ticket.errors.full_messages.flatten.join("\n")
respond_to do |format|
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
format.api { render_validation_errors(@helpdesk_ticket) }
end
end
end
def destroy
if @helpdesk_ticket.destroy
flash[:notice] = l(:notice_successful_delete)
respond_to do |format|
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
format.api { render_api_ok }
end
else
flash[:error] = l(:notice_unsuccessful_save)
end
end
private
def find_helpdesk_ticket
@helpdesk_ticket = HelpdeskTicket.find(params[:id])
@issue = @helpdesk_ticket.issue
@project = @issue.project if @issue
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
@project = @issue.project
@helpdesk_ticket = @issue.helpdesk_ticket || HelpdeskTicket.new(:ticket_date => Time.now, :issue => @issue)
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,46 @@
class HelpdeskVotesController < ApplicationController
unloadable
layout 'public_tickets'
skip_before_filter :check_if_login_required
before_filter :find_ticket, :authorize_ticket
before_filter :fill_data
helper :issues
def vote
@ticket.update_vote(params[:vote], params[:vote_comment]) if params[:vote]
end
def fast_vote
if RedmineHelpdesk.vote_comment_allow?
@ticket.vote = params[:vote] if params[:vote]
render :action => "show"
else
@ticket.update_vote(params[:vote]) if params[:vote]
render :action => "vote"
end
end
private
def find_ticket
@ticket = HelpdeskTicket.find(params[:id])
@issue = @ticket.issue
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
def authorize_ticket(action = params[:action])
allow = true
allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.vote_allow?
allow &&= !@issue.is_private
render_404 unless allow
end
def fill_data
@previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status
@total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours)
end
end
@@ -0,0 +1,206 @@
class HelpdeskWidgetController < ApplicationController
unloadable
layout false
helper :custom_fields
protect_from_forgery :except => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
skip_before_filter :check_if_login_required, :only => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
before_filter :prepare_data, :only => [:load_custom_fields, :create_ticket]
after_filter :set_access_control_header
def load_form
render :json => schema.to_json
end
def load_custom_fields
@issue = @project.issues.build(:tracker => @tracker) if @tracker
@enabled_cf = HelpdeskSettings["helpdesk_widget_available_custom_fields", nil]
end
def avatar
user = User.where(:login => params[:login]).first
return render :nothing => true, :status => 404 unless user
if user.try(:avatar).nil?
avatar_thumb, avatar_type = gravatar_avatar(user) if Setting.gravatar_enabled?
else
avatar_thumb, avatar_type = local_avatar(user.avatar)
end
return render :nothing => true, :status => 404 unless avatar_thumb
send_avatar(avatar_thumb, avatar_type)
end
def send_avatar(avatar_thumb, avatar_type)
send_file avatar_thumb, :filename => (request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(params[:login]) : params[:login]),
:type => avatar_type,
:disposition => 'inline'
end
def gravatar_avatar(user)
email = user.mail if user.respond_to?(:mail)
email = user.to_s[/<(.+?)>/, 1] unless email
return [nil, nil] unless email
default = Setting.gravatar_default ? CGI::escape(Setting.gravatar_default) : ''
temp_file = Tempfile.new([user.login, '.jpeg'])
temp_file.binmode
open("http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}?rating=PG&size=54&default=#{default}") do |url_file|
temp_file.write(url_file.read)
end
temp_file.rewind
[temp_file, 'image/jpeg']
end
def local_avatar(user_avatar)
return nil unless user_avatar.readable? || user_avatar.thumbnailable?
if (defined?(RedmineContacts::Thumbnail) == 'constant') && Redmine::Thumbnail.convert_available?
target = File.join(user_avatar.class.thumbnails_storage_path, "#{user_avatar.id}_#{user_avatar.digest}_54x54.thumb")
thumbnail = RedmineContacts::Thumbnail.generate(user_avatar.diskfile, target, '54x54')
elsif Redmine::Thumbnail.convert_available?
thumbnail = user_avatar.thumbnail(:size => '54x54')
else
thumbnail = user_avatar.diskfile
end
[thumbnail, detect_content_type(user_avatar)]
end
def create_ticket
@issue = prepare_issue
@issue.helpdesk_ticket = prepare_helpdesk_ticket
result =
if valid_email? && @issue.save
save_attachment(@issue)
HelpdeskMailer.auto_answer(@issue.helpdesk_ticket.customer, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0
{ :result => true, :errors => [] }
else
{ :result => false, :errors => prepared_errors }
end
render :json => result
end
private
def prepare_data
@project = Project.find(params[:project_id])
@tracker = @project.trackers.where(:id => params[:tracker_id]).first
end
def schema
if HelpdeskSettings["helpdesk_widget_enable", nil].to_i > 0
projects = Project.has_module('contacts_helpdesk').where(:id => HelpdeskSettings[:helpdesk_widget_available_projects, nil])
else
projects = []
end
data_schema = {}
data_schema[:projects] = Hash[projects.map { |project| [project.name.capitalize, project.id] }]
data_schema[:projects_data] = {}
projects.each do |project|
data_schema[:projects_data][project.id] = {}
if HelpdeskSettings["helpdesk_tracker", project] && HelpdeskSettings["helpdesk_tracker", project] != 'all'
data_schema[:projects_data][project.id][:trackers] = Hash[Tracker.where(id: HelpdeskSettings["helpdesk_tracker", project])
.map { |tracker| [tracker.name, tracker.id] }]
else
data_schema[:projects_data][project.id][:trackers] = Hash[project.trackers.map { |tracker| [tracker.name, tracker.id] }]
end
end
data_schema[:custom_fields] = Hash[IssueCustomField.where(id: HelpdeskSettings["helpdesk_widget_available_custom_fields", nil])
.map { |custom_field| [custom_field.name, custom_field.id] }]
data_schema[:avatar] = HelpdeskSettings[:helpdesk_widget_avatar_login, nil]
data_schema
end
def prepared_errors
errors_hash = @issue.errors.dup
# Username
if errors_hash[:'helpdesk_ticket.customer.first_name'].present?
@issue.errors.delete(:'helpdesk_ticket.customer.first_name')
@issue.errors[:username] = errors_hash[:'helpdesk_ticket.customer.first_name'].collect { |error| ['Username', error].join(' ') }
end
# Subject
if errors_hash[:subject].present?
errors = errors_hash[:subject].collect { |error| ['Subject', error].join(' ') }
@issue.errors[:subject].clear
@issue.errors[:subject] = errors
end
# Description
if params[:issue][:description].empty?
@issue.errors[:description] = I18n.t(:label_helpdesk_widget_ticket_error_description)
end
# Nested objects
if errors_hash[:'helpdesk_ticket.customer.projects'].present?
@issue.errors.delete(:'helpdesk_ticket.customer.projects')
end
@issue.errors
end
def prepare_issue
redmine_user = User.where(id: params[:redmine_user]).first
author = redmine_user.present? && redmine_user.allowed_to?(:edit_helpdesk_tickets, @project) ? redmine_user : User.anonymous
issue = @project.issues.build(:tracker => @tracker, :author => author)
issue.safe_attributes = params[:issue].deep_dup
issue.assigned_to = widget_contact.find_assigned_user(@project, HelpdeskSettings["helpdesk_assigned_to", @project])
issue
end
def prepare_helpdesk_ticket
HelpdeskTicket.new(:from_address => params[:email],
:ticket_date => Time.now,
:customer => widget_contact,
:issue => @issue,
:source => HelpdeskTicket::HELPDESK_WEB_SOURCE)
end
def save_attachment(issue)
return unless params[:attachment].present?
attachment_hash = split_base64(params[:attachment])
attachment = Attachment.new(file: Base64.decode64(attachment_hash[:data]))
attachment.filename = params[:attachment_name] || [Redmine::Utils.random_hex(16), attachment_hash[:extension]].join('.')
attachment.content_type = attachment_hash[:type]
attachment.author = User.anonymous
issue.attachments << attachment
issue.save
end
def split_base64(uri)
matcher = uri.match(/^data:(.*?)\;(.*?),(.*)$/)
{ type: matcher[1],
encoder: matcher[2],
data: matcher[3],
extension: matcher[1].split('/')[1] }
end
def widget_contact
return @widget_contact if @widget_contact
contacts = Contact.find_by_emails([params[:email]])
return @widget_contact = contacts.first if contacts.any?
@widget_contact = Contact.new(:email => params[:email])
@widget_contact.first_name, @widget_contact.last_name = params[:username].split(' ')
@widget_contact.projects << @project
@widget_contact
end
def set_access_control_header
headers['Access-Control-Allow-Origin'] = '*'
headers['X-Frame-Options'] = '*'
end
def valid_email?
if params[:email].empty?
@issue.errors[:email] = 'Email cannot be empty'
return false
elsif params[:email].match(/\A([\w\.\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i).nil?
@issue.errors[:email] = 'Email is incorrect'
return false
end
true
end
def detect_content_type(attachment)
content_type = attachment.content_type
if content_type.blank?
content_type = Redmine::MimeType.of(attachment.filename)
end
content_type.to_s
end
end
@@ -0,0 +1,89 @@
class MailFetcherController < ApplicationController
unloadable
require 'timeout'
before_filter :check_credential
def receive_imap
imap_options = {:host => params['host'],
:port => params['port'],
:ssl => params['ssl'],
:username => params['username'],
:password => params['password'],
:folder => params['folder'],
:move_on_success => params['move_on_success'],
:move_on_failure => params['move_on_failure']}
options = { :issue => {} }
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] }
options[:allow_override] = params['allow_override'] if params['allow_override']
options[:unknown_user] = params['unknown_user'] if params['unknown_user']
options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check']
begin
Timeout::timeout(15){ Redmine::IMAP.check(imap_options, options) }
rescue Exception => e
@error_messages = [e.message]
end
if @error_messages.blank?
respond_to do |format|
format.html { render :nothing => true, :status => :ok }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil }
format.api { render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil}
end
end
end
def receive_pop3
pop_options = {:host => params['host'],
:port => params['port'],
:apop => params['apop'],
:username => params['username'],
:password => params['password'],
:delete_unprocessed => params['delete_unprocessed']}
options = { :issue => {} }
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] }
options[:allow_override] = params['allow_override'] if params['allow_override']
options[:unknown_user] = params['unknown_user'] if params['unknown_user']
options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check']
begin
Timeout::timeout(15){ Redmine::POP3.check(pop_options, options) }
rescue Exception => e
@error_messages = [e.message]
end
if @error_messages.blank?
respond_to do |format|
format.html { render :nothing => true, :status => :ok }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil }
format.api { render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil}
end
end
end
private
def check_credential
User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
end
end
end
@@ -0,0 +1,81 @@
class PublicTicketsController < ApplicationController
unloadable
layout 'public_tickets'
skip_before_filter :check_if_login_required
before_filter :find_ticket, :authorize_ticket
helper :issues
helper :attachments
helper :journals
helper :custom_fields
def show
@previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status
@total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours)
@journals = @issue.journals.includes(:user).
includes(:details).
order("#{Journal.table_name}.created_on ASC").
where(:private_notes => false).
where("EXISTS (SELECT * FROM #{JournalMessage.table_name} WHERE #{JournalMessage.table_name}.journal_id = #{Journal.table_name}.id)")
@journals = @journals.each_with_index {|j,i| j.indice = i+1}.to_a
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@journal = @issue.journals.new
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.active
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
prepend_view_path "app/views/issues"
end
def add_comment
@journal = @issue.journals.new(params[:journal])
@issue.status_id = HelpdeskSettings["helpdesk_reopen_status", @issue.project_id] unless HelpdeskSettings["helpdesk_reopen_status", @issue.project_id].blank?
@journal.user = User.current
@journal.journal_message = JournalMessage.new(:from_address => @ticket.customer_email,
:contact => @ticket.customer,
:journal => @journal,
:is_incoming => true,
:message_date => Time.now)
if @issue.save
flash[:notice] = l(:notice_successful_create)
@journal.save
end
redirect_back_or_default(public_ticket_path(@ticket, @ticket.token))
end
def render_404(options={})
@message = l(:notice_file_not_found)
respond_to do |format|
format.html {
render :template => 'common/error', :status => 404
}
format.any { head 404 }
end
return false
end
private
def find_ticket
@ticket = HelpdeskTicket.find(params[:id])
@issue = @ticket.issue
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
def authorize_ticket(action = params[:action])
allow = true
allow &&= RedmineHelpdesk.public_comments? if (action.to_s == "add_comment")
allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.public_tickets?
allow &&= !@issue.is_private
render_404 unless allow
end
end