Feature #29664 » full patch based on 5.1-stable.patch
| Gemfile (revision f9673061812c2f08e3775f61597c44826a430ea3) → Gemfile (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 15 | 15 |
gem 'rbpdf', '~> 1.21.3' |
| 16 | 16 |
gem 'addressable' |
| 17 | 17 |
gem 'rubyzip', '~> 2.3.0' |
| 18 |
gem 'rest-client', '~> 2.1' |
|
| 18 | 19 | |
| 19 | 20 |
# Ruby Standard Gems |
| 20 | 21 |
gem 'csv', '~> 3.2.6' |
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/controllers/webhooks_controller.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
class WebhooksController < ApplicationController |
|
| 4 |
self.main_menu = false |
|
| 5 | ||
| 6 |
before_action :require_login |
|
| 7 |
before_action :check_enabled |
|
| 8 |
before_action :authorize |
|
| 9 |
before_action :find_webhook, :only => [:edit, :update, :destroy] |
|
| 10 | ||
| 11 |
require_sudo_mode :create, :update, :destroy |
|
| 12 | ||
| 13 |
helper :projects |
|
| 14 | ||
| 15 |
def index |
|
| 16 |
@webhooks = webhooks_scope.order(:url) |
|
| 17 |
end |
|
| 18 | ||
| 19 |
def new |
|
| 20 |
@webhook = User.current.webhooks.build |
|
| 21 |
end |
|
| 22 | ||
| 23 |
def edit |
|
| 24 |
end |
|
| 25 | ||
| 26 |
def create |
|
| 27 |
@webhook = User.current.webhooks.build(webhook_params) |
|
| 28 |
if @webhook.save |
|
| 29 |
redirect_to webhooks_path |
|
| 30 |
else |
|
| 31 |
render :new |
|
| 32 |
end |
|
| 33 |
end |
|
| 34 | ||
| 35 |
def update |
|
| 36 |
if @webhook.update(webhook_params) |
|
| 37 |
redirect_to webhooks_path |
|
| 38 |
else |
|
| 39 |
render :edit |
|
| 40 |
end |
|
| 41 |
end |
|
| 42 | ||
| 43 |
def destroy |
|
| 44 |
@webhook.destroy |
|
| 45 |
redirect_to webhooks_path |
|
| 46 |
end |
|
| 47 | ||
| 48 |
private |
|
| 49 | ||
| 50 |
def webhooks_scope |
|
| 51 |
if User.current.admin? |
|
| 52 |
Webhook.includes(:user).references(:users) |
|
| 53 |
else |
|
| 54 |
User.current.webhooks |
|
| 55 |
end |
|
| 56 |
end |
|
| 57 | ||
| 58 |
def webhook_params |
|
| 59 |
attrs = params.require(:webhook).permit(:url, :secret, :active, :events => [], :project_ids => [], :tracker_ids => []) |
|
| 60 |
attrs[:events] = Array(attrs[:events]).reject(&:blank?) |
|
| 61 |
attrs[:project_ids] = Array(attrs[:project_ids]).reject(&:blank?) |
|
| 62 |
attrs[:tracker_ids] = Array(attrs[:tracker_ids]).reject(&:blank?) |
|
| 63 |
attrs |
|
| 64 |
end |
|
| 65 | ||
| 66 |
def find_webhook |
|
| 67 |
@webhook = webhooks_scope.find_by(:id => params[:id]) |
|
| 68 |
render_404 unless @webhook |
|
| 69 |
end |
|
| 70 | ||
| 71 |
def authorize |
|
| 72 |
deny_access unless User.current.allowed_to?(:use_webhooks, nil, :global => true) |
|
| 73 |
end |
|
| 74 | ||
| 75 |
def check_enabled |
|
| 76 |
render_403 unless Webhook.enabled? |
|
| 77 |
end |
|
| 78 |
end |
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/jobs/webhook_job.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
class WebhookJob < ApplicationJob |
|
| 4 |
def perform(hook_id, payload_json) |
|
| 5 |
previous_user = User.current |
|
| 6 |
hook = Webhook.find_by(:id => hook_id) |
|
| 7 |
return unless hook&.user&.active? |
|
| 8 | ||
| 9 |
User.current = hook.user |
|
| 10 |
hook.call(payload_json) |
|
| 11 |
ensure |
|
| 12 |
User.current = previous_user |
|
| 13 |
end |
|
| 14 |
end |
|
| app/models/issue.rb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/models/issue.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 127 | 127 |
# https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback |
| 128 | 128 |
after_create_commit :send_notification |
| 129 | 129 |
after_create_commit :add_auto_watcher |
| 130 |
after_create_commit -> {Webhook.trigger('issue.created', self)}
|
|
| 131 |
after_update_commit :trigger_issue_webhooks |
|
| 132 |
after_destroy_commit -> {Webhook.trigger('issue.deleted', self)}
|
|
| 130 | 133 |
after_commit :create_parent_issue_journal |
| 131 | 134 | |
| 132 | 135 |
# Returns a SQL conditions string used to find all issues visible by the specified user |
| ... | ... | |
| 164 | 167 |
end |
| 165 | 168 |
end |
| 166 | 169 | |
| 170 |
def trigger_issue_webhooks |
|
| 171 |
Webhook.trigger('issue.updated', self)
|
|
| 172 | ||
| 173 |
return unless saved_change_to_status_id? |
|
| 174 | ||
| 175 |
previous_status = IssueStatus.find_by(:id => saved_change_to_status_id.first) |
|
| 176 |
current_status = status |
|
| 177 | ||
| 178 |
return unless current_status&.is_closed? |
|
| 179 |
return if previous_status&.is_closed? |
|
| 180 | ||
| 181 |
Webhook.trigger('issue.closed', self)
|
|
| 182 |
end |
|
| 183 | ||
| 167 | 184 |
# Returns true if usr or current user is allowed to view the issue |
| 168 | 185 |
def visible?(usr=nil) |
| 169 | 186 |
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| |
| app/models/project.rb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/models/project.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 47 | 47 |
has_many :queries, :dependent => :destroy |
| 48 | 48 |
has_many :documents, :dependent => :destroy |
| 49 | 49 |
has_many :news, lambda {includes(:author)}, :dependent => :destroy
|
| 50 |
has_and_belongs_to_many :webhooks |
|
| 50 | 51 |
has_many :issue_categories, lambda {order(:name)}, :dependent => :delete_all
|
| 51 | 52 |
has_many :boards, lambda {order(:position)}, :inverse_of => :project, :dependent => :destroy
|
| 52 | 53 |
has_one :repository, lambda {where(:is_default => true)}
|
| app/models/user.rb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/models/user.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 91 | 91 |
has_one :api_token, lambda {where "#{table.name}.action='api'"}, :class_name => 'Token'
|
| 92 | 92 |
has_one :email_address, lambda {where :is_default => true}, :autosave => true
|
| 93 | 93 |
has_many :email_addresses, :dependent => :delete_all |
| 94 |
has_many :webhooks, :dependent => :destroy |
|
| 94 | 95 |
belongs_to :auth_source |
| 95 | 96 | |
| 96 | 97 |
scope :logged, lambda {where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}")}
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/models/webhook.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
require 'rest-client' |
|
| 4 | ||
| 5 |
class Webhook < ApplicationRecord |
|
| 6 |
belongs_to :user |
|
| 7 |
has_and_belongs_to_many :projects |
|
| 8 |
has_and_belongs_to_many :trackers |
|
| 9 | ||
| 10 |
serialize :events, Array |
|
| 11 | ||
| 12 |
validates :url, :presence => true, |
|
| 13 |
:length => {:maximum => 2000},
|
|
| 14 |
:webhook_endpoint => true |
|
| 15 |
validates :secret, :length => {:maximum => 255}, :allow_blank => true
|
|
| 16 |
validate :validate_events |
|
| 17 |
validate :validate_projects |
|
| 18 |
validate :validate_trackers |
|
| 19 | ||
| 20 |
scope :active, -> {where(:active => true)}
|
|
| 21 | ||
| 22 |
before_validation :filter_projects |
|
| 23 |
before_validation :filter_trackers |
|
| 24 |
after_initialize :ensure_events_array |
|
| 25 | ||
| 26 |
def self.enabled? |
|
| 27 |
Setting.webhooks_enabled? |
|
| 28 |
end |
|
| 29 | ||
| 30 |
def self.trigger(event, object) |
|
| 31 |
return unless enabled? |
|
| 32 | ||
| 33 |
hooks_for(event, object).each do |hook| |
|
| 34 |
payload = hook.payload(event, object) |
|
| 35 |
WebhookJob.perform_later(hook.id, payload.to_json) |
|
| 36 |
end |
|
| 37 |
end |
|
| 38 | ||
| 39 |
def self.hooks_for(event, object) |
|
| 40 |
project = object.respond_to?(:project) ? object.project : nil |
|
| 41 |
return [] unless project |
|
| 42 | ||
| 43 |
active |
|
| 44 |
.joins(:projects, :user) |
|
| 45 |
.where(:projects => {:id => project.id})
|
|
| 46 |
.where(:users => {:status => User::STATUS_ACTIVE})
|
|
| 47 |
.select do |hook| |
|
| 48 |
hook.events.include?(event) && |
|
| 49 |
(!object.respond_to?(:visible?) || object.visible?(hook.user)) && |
|
| 50 |
hook.user.allowed_to?(:use_webhooks, project) && |
|
| 51 |
hook.tracker_allowed?(object) |
|
| 52 |
end |
|
| 53 |
end |
|
| 54 | ||
| 55 |
def setable_projects |
|
| 56 |
member = user || User.current |
|
| 57 |
return Project.none unless member |
|
| 58 | ||
| 59 |
Project.allowed_to(member, :use_webhooks).sorted |
|
| 60 |
end |
|
| 61 | ||
| 62 |
def setable_events |
|
| 63 |
WebhookPayload::EVENTS |
|
| 64 |
end |
|
| 65 | ||
| 66 |
def setable_event_names |
|
| 67 |
setable_events.flat_map {|type, actions| actions.map {|action| "#{type}.#{action}"}}
|
|
| 68 |
end |
|
| 69 | ||
| 70 |
def setable_trackers |
|
| 71 |
project_trackers = setable_projects.includes(:trackers).flat_map(&:trackers) |
|
| 72 |
project_trackers.uniq.sort_by(&:position) |
|
| 73 |
end |
|
| 74 | ||
| 75 |
def payload(event, object) |
|
| 76 |
WebhookPayload.new(event, object, user).to_h |
|
| 77 |
end |
|
| 78 | ||
| 79 |
def call(payload_json) |
|
| 80 |
Executor.new(url, payload_json, secret).call |
|
| 81 |
true |
|
| 82 |
rescue => e |
|
| 83 |
Rails.logger.warn do |
|
| 84 |
"Webhook delivery failed: #{e.class}: #{e.message}\n#{Array(e.backtrace).join("\n")}"
|
|
| 85 |
end |
|
| 86 |
false |
|
| 87 |
end |
|
| 88 | ||
| 89 |
class Executor |
|
| 90 |
def initialize(url, payload, secret) |
|
| 91 |
@url = url |
|
| 92 |
@payload = payload |
|
| 93 |
@secret = secret |
|
| 94 |
end |
|
| 95 | ||
| 96 |
def call |
|
| 97 |
raise URI::BadURIError unless WebhookEndpointValidator.safe_webhook_uri?(@url) |
|
| 98 | ||
| 99 |
headers = {
|
|
| 100 |
:accept => '*/*', |
|
| 101 |
:content_type => :json, |
|
| 102 |
:user_agent => 'Redmine' |
|
| 103 |
} |
|
| 104 |
headers['X-Redmine-Signature-256'] = compute_signature if @secret.present? |
|
| 105 | ||
| 106 |
Rails.logger.debug {"Webhook: POST #{@url}"}
|
|
| 107 |
RestClient.post(@url, @payload, headers) |
|
| 108 |
end |
|
| 109 | ||
| 110 |
private |
|
| 111 | ||
| 112 |
def compute_signature |
|
| 113 |
'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @secret, @payload)
|
|
| 114 |
end |
|
| 115 |
end |
|
| 116 | ||
| 117 |
def tracker_allowed?(object) |
|
| 118 |
return true unless object.respond_to?(:tracker_id) |
|
| 119 |
tracker_ids.blank? || tracker_ids.include?(object.tracker_id) |
|
| 120 |
end |
|
| 121 | ||
| 122 |
private |
|
| 123 | ||
| 124 |
def ensure_events_array |
|
| 125 |
self.events ||= [] |
|
| 126 |
end |
|
| 127 | ||
| 128 |
def filter_projects |
|
| 129 |
return if project_ids.blank? |
|
| 130 | ||
| 131 |
allowed_ids = setable_projects.map(&:id) |
|
| 132 |
self.project_ids = project_ids & allowed_ids |
|
| 133 |
end |
|
| 134 | ||
| 135 |
def filter_trackers |
|
| 136 |
return if tracker_ids.blank? |
|
| 137 | ||
| 138 |
allowed_ids = setable_trackers.map(&:id) |
|
| 139 |
self.tracker_ids = tracker_ids & allowed_ids |
|
| 140 |
end |
|
| 141 | ||
| 142 |
def validate_events |
|
| 143 |
self.events = Array(events).reject(&:blank?) |
|
| 144 |
if events.blank? || (events - setable_event_names).any? |
|
| 145 |
errors.add(:events, :invalid) |
|
| 146 |
end |
|
| 147 |
end |
|
| 148 | ||
| 149 |
def validate_projects |
|
| 150 |
if project_ids.blank? |
|
| 151 |
errors.add(:projects, :blank) |
|
| 152 |
end |
|
| 153 |
end |
|
| 154 | ||
| 155 |
def validate_trackers |
|
| 156 |
self.tracker_ids = tracker_ids & setable_trackers.map(&:id) |
|
| 157 | ||
| 158 |
if issue_events_selected? && tracker_ids.blank? |
|
| 159 |
errors.add(:trackers, :blank) |
|
| 160 |
end |
|
| 161 |
end |
|
| 162 | ||
| 163 |
def issue_events_selected? |
|
| 164 |
Array(events).any? {|event| event.to_s.start_with?('issue.')}
|
|
| 165 |
end |
|
| 166 |
end |
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/models/webhook_payload.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
class WebhookPayload |
|
| 4 |
EVENTS = {
|
|
| 5 |
:issue => %w[created updated closed deleted], |
|
| 6 |
:wiki_page => %w[created updated deleted] |
|
| 7 |
}.freeze |
|
| 8 | ||
| 9 |
attr_reader :event, :object, :user |
|
| 10 | ||
| 11 |
def initialize(event, object, user) |
|
| 12 |
@event = event |
|
| 13 |
@object = object |
|
| 14 |
@user = user |
|
| 15 |
end |
|
| 16 | ||
| 17 |
def to_h |
|
| 18 |
type, action = event.split('.')
|
|
| 19 |
type = type&.to_sym |
|
| 20 |
unless EVENTS[type]&.include?(action) |
|
| 21 |
raise ArgumentError, "Unsupported webhook event: #{event}"
|
|
| 22 |
end |
|
| 23 | ||
| 24 |
payload, timestamp = send("#{type}_payload", action)
|
|
| 25 |
timestamp ||= Time.current |
|
| 26 |
{
|
|
| 27 |
:type => event, |
|
| 28 |
:timestamp => timestamp.utc.iso8601, |
|
| 29 |
:data => payload |
|
| 30 |
} |
|
| 31 |
end |
|
| 32 | ||
| 33 |
private |
|
| 34 | ||
| 35 |
def issue_payload(action) |
|
| 36 |
issue = object |
|
| 37 |
journal = %w[updated closed].include?(action) ? issue.current_journal : nil |
|
| 38 | ||
| 39 |
payload = {
|
|
| 40 |
:issue => issue_hash(issue) |
|
| 41 |
} |
|
| 42 |
payload[:journal] = journal_hash(journal) if journal |
|
| 43 | ||
| 44 |
timestamp = |
|
| 45 |
case action |
|
| 46 |
when 'created' |
|
| 47 |
issue.created_on |
|
| 48 |
when 'closed' |
|
| 49 |
issue.closed_on || Time.current |
|
| 50 |
when 'deleted' |
|
| 51 |
Time.current |
|
| 52 |
else |
|
| 53 |
journal&.created_on || issue.updated_on |
|
| 54 |
end |
|
| 55 | ||
| 56 |
[payload, timestamp] |
|
| 57 |
end |
|
| 58 | ||
| 59 |
def wiki_page_payload(action) |
|
| 60 |
page, content = wiki_page_and_content |
|
| 61 |
payload = {
|
|
| 62 |
:wiki_page => wiki_page_hash(page) |
|
| 63 |
} |
|
| 64 |
payload[:content] = wiki_content_hash(content) if content |
|
| 65 | ||
| 66 |
timestamp = |
|
| 67 |
case action |
|
| 68 |
when 'created' |
|
| 69 |
content&.created_on || Time.current |
|
| 70 |
when 'deleted' |
|
| 71 |
Time.current |
|
| 72 |
else |
|
| 73 |
content&.updated_on || Time.current |
|
| 74 |
end |
|
| 75 | ||
| 76 |
[payload, timestamp] |
|
| 77 |
end |
|
| 78 | ||
| 79 |
def issue_hash(issue) |
|
| 80 |
hash = {
|
|
| 81 |
:id => issue.id, |
|
| 82 |
:subject => issue.subject, |
|
| 83 |
:description => issue.description, |
|
| 84 |
:is_private => issue.is_private?, |
|
| 85 |
:project => project_hash(issue.project), |
|
| 86 |
:tracker => reference_hash(issue.tracker), |
|
| 87 |
:status => status_hash(issue.status), |
|
| 88 |
:priority => reference_hash(issue.priority), |
|
| 89 |
:author => user_hash(issue.author), |
|
| 90 |
:assigned_to => user_hash(issue.assigned_to), |
|
| 91 |
:category => reference_hash(issue.category), |
|
| 92 |
:fixed_version => reference_hash(issue.fixed_version), |
|
| 93 |
:parent => issue.parent_id ? {:id => issue.parent_id} : nil,
|
|
| 94 |
:start_date => issue.start_date&.to_s, |
|
| 95 |
:due_date => issue.due_date&.to_s, |
|
| 96 |
:done_ratio => issue.done_ratio, |
|
| 97 |
:estimated_hours => issue.estimated_hours, |
|
| 98 |
:total_estimated_hours => issue.total_estimated_hours, |
|
| 99 |
:created_on => issue.created_on&.iso8601, |
|
| 100 |
:updated_on => issue.updated_on&.iso8601, |
|
| 101 |
:closed_on => issue.closed_on&.iso8601 |
|
| 102 |
} |
|
| 103 | ||
| 104 |
if user&.allowed_to?(:view_time_entries, issue.project) |
|
| 105 |
hash[:spent_hours] = issue.spent_hours |
|
| 106 |
hash[:total_spent_hours] = issue.total_spent_hours |
|
| 107 |
end |
|
| 108 | ||
| 109 |
custom_fields = issue.visible_custom_field_values(user).map do |value| |
|
| 110 |
{
|
|
| 111 |
:id => value.custom_field_id, |
|
| 112 |
:name => value.custom_field.name, |
|
| 113 |
:value => value.value |
|
| 114 |
} |
|
| 115 |
end |
|
| 116 |
hash[:custom_fields] = custom_fields if custom_fields.any? |
|
| 117 | ||
| 118 |
hash.compact |
|
| 119 |
end |
|
| 120 | ||
| 121 |
def journal_hash(journal) |
|
| 122 |
return unless journal |
|
| 123 | ||
| 124 |
{
|
|
| 125 |
:id => journal.id, |
|
| 126 |
:notes => journal.notes, |
|
| 127 |
:created_on => journal.created_on&.iso8601, |
|
| 128 |
:user => user_hash(journal.user), |
|
| 129 |
:details => journal.visible_details(user).map do |detail| |
|
| 130 |
{
|
|
| 131 |
:property => detail.property, |
|
| 132 |
:prop_key => detail.prop_key, |
|
| 133 |
:old_value => detail.old_value, |
|
| 134 |
:value => detail.value |
|
| 135 |
} |
|
| 136 |
end |
|
| 137 |
} |
|
| 138 |
end |
|
| 139 | ||
| 140 |
def wiki_page_hash(page) |
|
| 141 |
return {} unless page
|
|
| 142 | ||
| 143 |
{
|
|
| 144 |
:id => page.id, |
|
| 145 |
:title => page.title, |
|
| 146 |
:project => project_hash(page.project), |
|
| 147 |
:parent_id => page.parent_id, |
|
| 148 |
:created_on => page.created_on&.iso8601, |
|
| 149 |
:updated_on => page.updated_on&.iso8601 |
|
| 150 |
}.compact |
|
| 151 |
end |
|
| 152 | ||
| 153 |
def wiki_content_hash(content) |
|
| 154 |
return unless content |
|
| 155 | ||
| 156 |
{
|
|
| 157 |
:version => content.version, |
|
| 158 |
:text => content.text, |
|
| 159 |
:comments => content.comments, |
|
| 160 |
:author => user_hash(content.author), |
|
| 161 |
:created_on => content.created_on&.iso8601, |
|
| 162 |
:updated_on => content.updated_on&.iso8601 |
|
| 163 |
} |
|
| 164 |
end |
|
| 165 | ||
| 166 |
def wiki_page_and_content |
|
| 167 |
if object.is_a?(WikiContent) |
|
| 168 |
[object.page, object] |
|
| 169 |
else |
|
| 170 |
[object, object.respond_to?(:content) ? object.content : nil] |
|
| 171 |
end |
|
| 172 |
end |
|
| 173 | ||
| 174 |
def project_hash(project) |
|
| 175 |
return unless project |
|
| 176 | ||
| 177 |
{
|
|
| 178 |
:id => project.id, |
|
| 179 |
:identifier => project.identifier, |
|
| 180 |
:name => project.name |
|
| 181 |
} |
|
| 182 |
end |
|
| 183 | ||
| 184 |
def reference_hash(record) |
|
| 185 |
return unless record |
|
| 186 | ||
| 187 |
{:id => record.id, :name => record.name}
|
|
| 188 |
end |
|
| 189 | ||
| 190 |
def status_hash(status) |
|
| 191 |
return unless status |
|
| 192 | ||
| 193 |
{:id => status.id, :name => status.name, :is_closed => status.is_closed?}
|
|
| 194 |
end |
|
| 195 | ||
| 196 |
def user_hash(user_record) |
|
| 197 |
return unless user_record |
|
| 198 | ||
| 199 |
{:id => user_record.id, :name => user_record.name}
|
|
| 200 |
end |
|
| 201 |
end |
|
| app/models/wiki_content.rb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/models/wiki_content.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 33 | 33 |
after_save :create_version |
| 34 | 34 |
after_create_commit :send_notification_create |
| 35 | 35 |
after_update_commit :send_notification_update |
| 36 |
after_create_commit :trigger_webhook_create |
|
| 37 |
after_update_commit :trigger_webhook_update |
|
| 36 | 38 | |
| 37 | 39 |
scope :without_text, lambda {select(:id, :page_id, :version, :updated_on)}
|
| 38 | 40 | |
| ... | ... | |
| 101 | 103 |
Mailer.deliver_wiki_content_updated(self) |
| 102 | 104 |
end |
| 103 | 105 |
end |
| 106 | ||
| 107 |
def trigger_webhook_create |
|
| 108 |
Webhook.trigger('wiki_page.created', self)
|
|
| 109 |
end |
|
| 110 | ||
| 111 |
def trigger_webhook_update |
|
| 112 |
Webhook.trigger('wiki_page.updated', self)
|
|
| 113 |
end |
|
| 104 | 114 |
end |
| app/models/wiki_page.rb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/models/wiki_page.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 61 | 61 |
before_destroy :delete_redirects |
| 62 | 62 |
before_save :handle_rename_or_move, :update_wiki_start_page |
| 63 | 63 |
after_save :handle_children_move, :delete_selected_attachments |
| 64 |
after_destroy_commit -> {Webhook.trigger('wiki_page.deleted', self)}
|
|
| 64 | 65 | |
| 65 | 66 |
# eager load information about last updates, without loading text |
| 66 | 67 |
scope :with_updated_on, lambda {preload(:content_without_text)}
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/validators/webhook_endpoint_validator.rb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
require 'set' |
|
| 4 |
require 'uri' |
|
| 5 |
require 'resolv' |
|
| 6 |
require 'ipaddr' |
|
| 7 | ||
| 8 |
class WebhookEndpointValidator < ActiveModel::EachValidator |
|
| 9 |
BAD_PORTS = Set[25, 111, 135, 139, 445, 1433, 1521, 2049, 3306, 3389, 5985, 5986].freeze |
|
| 10 | ||
| 11 |
def validate_each(record, attribute, value) |
|
| 12 |
return if value.blank? |
|
| 13 | ||
| 14 |
valid, reason = self.class.validate_uri(value) |
|
| 15 |
record.errors.add(attribute, :invalid, message: reason && " (#{reason})") unless valid
|
|
| 16 |
end |
|
| 17 | ||
| 18 |
class << self |
|
| 19 |
def safe_webhook_uri?(value) |
|
| 20 |
valid, = validate_uri(value) |
|
| 21 |
valid |
|
| 22 |
end |
|
| 23 | ||
| 24 |
def validate_uri(value) |
|
| 25 |
uri = value.is_a?(URI) ? value : URI.parse(value.to_s) |
|
| 26 | ||
| 27 |
return [false, 'must use http or https'] unless valid_scheme?(uri.scheme) |
|
| 28 | ||
| 29 |
host_reason = host_validation_error(uri.host) |
|
| 30 |
return [false, host_reason] if host_reason |
|
| 31 | ||
| 32 |
return [false, 'uses a disallowed port'] unless valid_port?(uri.port) |
|
| 33 | ||
| 34 |
[true, nil] |
|
| 35 |
rescue URI::Error, ArgumentError => e |
|
| 36 |
Rails.logger.warn {"Webhook endpoint rejected: #{value.inspect} (#{e.message})"}
|
|
| 37 |
[false, e.message] |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def valid_scheme?(scheme) |
|
| 41 |
%w[http https].include?(scheme) |
|
| 42 |
end |
|
| 43 | ||
| 44 |
def host_validation_error(host) |
|
| 45 |
return 'host is missing' if host.blank? |
|
| 46 | ||
| 47 |
blocked = blocked_targets |
|
| 48 |
allowed = allowed_targets |
|
| 49 | ||
| 50 |
host_allowlisted = allowed[:host_pattern]&.match?(host) |
|
| 51 | ||
| 52 |
return 'host is blocklisted' if host_blocked?(host, blocked) |
|
| 53 |
if !allowlist_empty?(allowed) && !host_allowlisted && !allowlist_ip_allowed_for_host?(host, allowed) |
|
| 54 |
return "host is not on the allowlist" |
|
| 55 |
end |
|
| 56 | ||
| 57 |
addresses = ip_literal?(host) ? [host] : resolve_addresses(host) |
|
| 58 |
return nil if addresses.blank? && allowlist_empty?(allowed) |
|
| 59 | ||
| 60 |
addresses.each do |ip| |
|
| 61 |
ipaddr = IPAddr.new(ip) |
|
| 62 |
allowlisted_ip = host_allowlisted || allowlist_ip_allowed?(ipaddr, allowed) |
|
| 63 | ||
| 64 |
return "address #{ipaddr} is blocklisted" if blocked[:ips].any? {|entry| entry.include?(ipaddr)}
|
|
| 65 |
return "address #{ipaddr} is not on the allowlist" if !allowlist_empty?(allowed) && !allowlisted_ip
|
|
| 66 |
return "address #{ipaddr} is not reachable from the public network" if !allowlisted_ip && (ipaddr.loopback? || ipaddr.link_local? || ipaddr.private?)
|
|
| 67 |
rescue IPAddr::Error |
|
| 68 |
return "address #{ip.inspect} is invalid"
|
|
| 69 |
end |
|
| 70 | ||
| 71 |
nil |
|
| 72 |
end |
|
| 73 | ||
| 74 |
def valid_port?(port) |
|
| 75 |
port.present? && !BAD_PORTS.include?(port) |
|
| 76 |
end |
|
| 77 | ||
| 78 |
def resolve_addresses(host) |
|
| 79 |
addresses = [] |
|
| 80 |
Resolv.each_address(host) {|ip| addresses << ip}
|
|
| 81 |
addresses |
|
| 82 |
rescue Resolv::ResolvError |
|
| 83 |
[] |
|
| 84 |
end |
|
| 85 | ||
| 86 |
def ip_literal?(host) |
|
| 87 |
IPAddr.new(host) |
|
| 88 |
true |
|
| 89 |
rescue IPAddr::Error |
|
| 90 |
false |
|
| 91 |
end |
|
| 92 | ||
| 93 |
def blocked_targets |
|
| 94 |
@blocked_targets ||= begin |
|
| 95 |
ips = [] |
|
| 96 |
host_patterns = [] |
|
| 97 |
Array(Redmine::Configuration['webhook_blocklist']).each do |entry| |
|
| 98 |
entry = entry.to_s.strip |
|
| 99 |
next if entry.empty? |
|
| 100 | ||
| 101 |
begin |
|
| 102 |
ips << IPAddr.new(entry) |
|
| 103 |
rescue IPAddr::Error |
|
| 104 |
host_patterns << entry |
|
| 105 |
end |
|
| 106 |
end |
|
| 107 |
{
|
|
| 108 |
:ips => ips.freeze, |
|
| 109 |
:host_pattern => build_host_pattern(host_patterns) |
|
| 110 |
} |
|
| 111 |
end |
|
| 112 |
end |
|
| 113 | ||
| 114 |
def allowed_targets |
|
| 115 |
@allowed_targets ||= begin |
|
| 116 |
ips = [] |
|
| 117 |
host_patterns = [] |
|
| 118 |
Array(Redmine::Configuration['webhook_allowlist']).each do |entry| |
|
| 119 |
entry = entry.to_s.strip |
|
| 120 |
next if entry.empty? |
|
| 121 | ||
| 122 |
begin |
|
| 123 |
ips << IPAddr.new(entry) |
|
| 124 |
rescue IPAddr::Error |
|
| 125 |
host_patterns << entry |
|
| 126 |
end |
|
| 127 |
end |
|
| 128 |
{
|
|
| 129 |
:ips => ips.freeze, |
|
| 130 |
:host_pattern => build_host_pattern(host_patterns) |
|
| 131 |
} |
|
| 132 |
end |
|
| 133 |
end |
|
| 134 | ||
| 135 |
def build_host_pattern(patterns) |
|
| 136 |
return if patterns.empty? |
|
| 137 | ||
| 138 |
sources = patterns.map do |value| |
|
| 139 |
if value.start_with?('*.')
|
|
| 140 |
"(?:.*\\.)?#{Regexp.escape(value.delete_prefix('*.'))}"
|
|
| 141 |
else |
|
| 142 |
Regexp.escape(value) |
|
| 143 |
end |
|
| 144 |
end |
|
| 145 | ||
| 146 |
Regexp.new("\\A(?:#{sources.join('|')})\\z", Regexp::IGNORECASE)
|
|
| 147 |
end |
|
| 148 | ||
| 149 |
def host_blocked?(host, blocked) |
|
| 150 |
blocked[:host_pattern]&.match?(host) |
|
| 151 |
end |
|
| 152 | ||
| 153 |
def host_allowed_by_allowlist?(host, allowed) |
|
| 154 |
return true if allowlist_empty?(allowed) |
|
| 155 | ||
| 156 |
allowed[:host_pattern]&.match?(host) || allowlist_ip_allowed_for_host?(host, allowed) |
|
| 157 |
end |
|
| 158 | ||
| 159 |
def allowlist_empty?(allowed) |
|
| 160 |
allowed[:ips].empty? && allowed[:host_pattern].nil? |
|
| 161 |
end |
|
| 162 | ||
| 163 |
def allowlist_ip_allowed_for_host?(host, allowed) |
|
| 164 |
addresses = ip_literal?(host) ? [host] : resolve_addresses(host) |
|
| 165 |
return false if addresses.empty? |
|
| 166 | ||
| 167 |
addresses.any? do |ip| |
|
| 168 |
allowlist_ip_allowed?(IPAddr.new(ip), allowed) |
|
| 169 |
rescue IPAddr::Error |
|
| 170 |
false |
|
| 171 |
end |
|
| 172 |
end |
|
| 173 | ||
| 174 |
def allowlist_ip_allowed?(ipaddr, allowed) |
|
| 175 |
allowed[:ips].any? {|entry| entry.include?(ipaddr)}
|
|
| 176 |
end |
|
| 177 |
end |
|
| 178 |
end |
|
| app/views/my/_sidebar.html.erb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/views/my/_sidebar.html.erb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 31 | 31 |
<% else %> |
| 32 | 32 |
<%= l(:label_missing_api_access_key) %> |
| 33 | 33 |
<% end %> |
| 34 | ||
| 35 |
<% if Setting.webhooks_enabled? && User.current.allowed_to?(:use_webhooks, nil, :global => true) %> |
|
| 36 |
<h4><%= l(:label_integrations) %></h4> |
|
| 37 |
<p><%= link_to l(:label_webhook_plural), webhooks_path, :class => 'icon icon-link' %></p> |
|
| 38 |
<% end %> |
|
| 34 | 39 |
(<%= link_to l(:button_reset), my_api_key_path, :method => :post %>) |
| 35 | 40 |
</p> |
| 36 | 41 |
<% end %> |
| app/views/settings/_api.html.erb (revision f9673061812c2f08e3775f61597c44826a430ea3) → app/views/settings/_api.html.erb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 4 | 4 |
<p><%= setting_check_box :rest_api_enabled %></p> |
| 5 | 5 | |
| 6 | 6 |
<p><%= setting_check_box :jsonp_enabled %></p> |
| 7 | ||
| 8 |
<p><%= setting_check_box :webhooks_enabled %></p> |
|
| 7 | 9 |
</div> |
| 8 | 10 | |
| 9 | 11 |
<%= submit_tag l(:button_save) %> |
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/views/webhooks/_form.html.erb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
<%= error_messages_for @webhook %> |
|
| 2 |
<div class="splitcontent"> |
|
| 3 |
<div class="splitcontentleft"> |
|
| 4 |
<div class="box tabular"> |
|
| 5 |
<p> |
|
| 6 |
<%= f.text_field :url, :required => true, :size => 60 %> |
|
| 7 |
<em class="info"><%= l(:webhook_url_info) %></em> |
|
| 8 |
</p> |
|
| 9 |
<p> |
|
| 10 |
<%= f.text_field :secret %> |
|
| 11 |
<em class="info"><%= raw l(:webhook_secret_info_html) %></em> |
|
| 12 |
</p> |
|
| 13 |
<p> |
|
| 14 |
<%= f.check_box :active %> <%= l(:field_active) %> |
|
| 15 |
</p> |
|
| 16 |
</div> |
|
| 17 | ||
| 18 |
<h3><%= l(:label_webhook_events) %></h3> |
|
| 19 |
<div class="box tabular" id="events"> |
|
| 20 |
<% @webhook.setable_events.keys.sort.each do |type| %> |
|
| 21 |
<fieldset id="<%= type %>_events"> |
|
| 22 |
<legend> |
|
| 23 |
<%= toggle_checkboxes_link("##{type}_events input") %>
|
|
| 24 |
<%= l_or_humanize(type, :prefix => 'webhook_events_') %> |
|
| 25 |
</legend> |
|
| 26 |
<% @webhook.setable_events[type].each do |action| %> |
|
| 27 |
<% name = "#{type}.#{action}" %>
|
|
| 28 |
<label class="floating"> |
|
| 29 |
<%= check_box_tag 'webhook[events][]', name, @webhook.events.include?(name), :id => "webhook_events_#{name.tr('.', '_')}" %>
|
|
| 30 |
<%= l_or_humanize("#{type}_#{action}", :prefix => 'webhook_events_') %>
|
|
| 31 |
</label> |
|
| 32 |
<% end %> |
|
| 33 |
</fieldset> |
|
| 34 |
<% end %> |
|
| 35 |
<br /><%= check_all_links 'events' %> |
|
| 36 |
<%= hidden_field_tag 'webhook[events][]', '' %> |
|
| 37 |
</div> |
|
| 38 |
</div> |
|
| 39 | ||
| 40 |
<div class="splitcontentright"> |
|
| 41 |
<fieldset class="box" id="webhook_project_ids"> |
|
| 42 |
<legend> |
|
| 43 |
<%= toggle_checkboxes_link("#webhook_project_ids input[type=checkbox]") %>
|
|
| 44 |
<%= l(:label_project_plural) %> |
|
| 45 |
</legend> |
|
| 46 |
<% project_ids = @webhook.project_ids || [] %> |
|
| 47 |
<%= render_project_nested_lists(@webhook.setable_projects) do |p| %> |
|
| 48 |
<%= content_tag('label', check_box_tag('webhook[project_ids][]', p.id, project_ids.include?(p.id)) + ' ' + h(p)) %>
|
|
| 49 |
<% end %> |
|
| 50 |
<%= hidden_field_tag 'webhook[project_ids][]', '' %> |
|
| 51 |
</fieldset> |
|
| 52 | ||
| 53 |
<fieldset class="box" id="webhook_tracker_ids"> |
|
| 54 |
<legend> |
|
| 55 |
<%= toggle_checkboxes_link("#webhook_tracker_ids input[type=checkbox]") %>
|
|
| 56 |
<%= l(:label_tracker_plural) %> |
|
| 57 |
</legend> |
|
| 58 |
<% tracker_ids = @webhook.tracker_ids || [] %> |
|
| 59 |
<% @webhook.setable_trackers.each do |tracker| %> |
|
| 60 |
<label class="floating"> |
|
| 61 |
<%= check_box_tag('webhook[tracker_ids][]', tracker.id, tracker_ids.include?(tracker.id)) %>
|
|
| 62 |
<%= h(tracker.name) %> |
|
| 63 |
</label> |
|
| 64 |
<% end %> |
|
| 65 |
<%= hidden_field_tag 'webhook[tracker_ids][]', '' %> |
|
| 66 |
</fieldset> |
|
| 67 |
</div> |
|
| 68 |
</div> |
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/views/webhooks/edit.html.erb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
<%= labelled_form_for @webhook, :url => webhook_path(@webhook), :html => {:method => :patch} do |f| %>
|
|
| 2 |
<%= render :partial => 'form', :locals => {:f => f} %>
|
|
| 3 |
<p><%= submit_tag l(:button_save) %></p> |
|
| 4 |
<% end %> |
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/views/webhooks/index.html.erb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
<div class="contextual"> |
|
| 2 |
<%= link_to l(:label_webhook_new), new_webhook_path, :class => 'icon icon-add' %> |
|
| 3 |
</div> |
|
| 4 | ||
| 5 |
<%= title l(:label_webhook_plural) %> |
|
| 6 | ||
| 7 |
<% if @webhooks.any? %> |
|
| 8 |
<div class="autoscroll"> |
|
| 9 |
<table class="list"> |
|
| 10 |
<thead> |
|
| 11 |
<tr> |
|
| 12 |
<% if User.current.admin? %> |
|
| 13 |
<th><%= l(:label_user) %></th> |
|
| 14 |
<% end %> |
|
| 15 |
<th><%= l(:field_active) %></th> |
|
| 16 |
<th><%= l(:field_url) %></th> |
|
| 17 |
<th><%= l(:label_webhook_events) %></th> |
|
| 18 |
<th><%= l(:label_project_plural) %></th> |
|
| 19 |
<th></th> |
|
| 20 |
</tr> |
|
| 21 |
</thead> |
|
| 22 |
<tbody> |
|
| 23 |
<% @webhooks.each do |hook| %> |
|
| 24 |
<tr id="webhook_<%= hook.id %>" class="<%= cycle('odd', 'even') %>">
|
|
| 25 |
<% if User.current.admin? %> |
|
| 26 |
<td class="username"><%= h hook.user %></td> |
|
| 27 |
<% end %> |
|
| 28 |
<td><%= hook.active? ? l(:general_text_Yes) : l(:general_text_No) %></td> |
|
| 29 |
<td><%= truncate(hook.url, :length => 50) %></td> |
|
| 30 |
<td><%= safe_join(hook.events.map {|e| content_tag(:code, e)}, ', ') %></td>
|
|
| 31 |
<td><%= safe_join(hook.projects.visible.map {|p| link_to_project(p)}, ', ') %></td>
|
|
| 32 |
<td class="buttons"> |
|
| 33 |
<%= link_to l(:button_edit), edit_webhook_path(hook), :class => 'icon icon-edit' %> |
|
| 34 |
<%= link_to l(:button_delete), webhook_path(hook), :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del' %>
|
|
| 35 |
</td> |
|
| 36 |
</tr> |
|
| 37 |
<% end %> |
|
| 38 |
</tbody> |
|
| 39 |
</table> |
|
| 40 |
</div> |
|
| 41 |
<% else %> |
|
| 42 |
<p class="nodata"><%= l(:label_no_data) %></p> |
|
| 43 |
<% end %> |
|
| /dev/null (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) → app/views/webhooks/new.html.erb (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1 |
<%= labelled_form_for @webhook, :url => webhooks_path do |f| %> |
|
| 2 |
<%= render :partial => 'form', :locals => {:f => f} %>
|
|
| 3 |
<p><%= submit_tag l(:button_create) %></p> |
|
| 4 |
<% end %> |
|
| config/locales/ar.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/ar.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1487 | 1487 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1488 | 1488 |
only for dev/test changed |
| 1489 | 1489 |
twofa_already_setup: Two-factor authentication already set up |
| 1490 | ||
| 1491 |
label_webhook_plural: "Webhooks" |
|
| 1492 |
label_webhook_new: "New webhook" |
|
| 1493 |
label_webhook_edit: "Edit webhook" |
|
| 1494 |
label_webhook_events: "Events" |
|
| 1495 |
webhook_events_issue: "Issues" |
|
| 1496 |
webhook_events_issue_created: "Issue created" |
|
| 1497 |
webhook_events_issue_updated: "Issue updated" |
|
| 1498 |
webhook_events_issue_closed: "Issue closed" |
|
| 1499 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1500 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1501 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1502 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1503 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1504 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1505 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1506 |
permission_use_webhooks: "Use webhooks" |
|
| 1507 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1508 |
label_integrations: "Integrations" |
|
| config/locales/az.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/az.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1579 | 1579 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1580 | 1580 |
only for dev/test changed |
| 1581 | 1581 |
twofa_already_setup: Two-factor authentication already set up |
| 1582 | ||
| 1583 |
label_webhook_plural: "Webhooks" |
|
| 1584 |
label_webhook_new: "New webhook" |
|
| 1585 |
label_webhook_edit: "Edit webhook" |
|
| 1586 |
label_webhook_events: "Events" |
|
| 1587 |
webhook_events_issue: "Issues" |
|
| 1588 |
webhook_events_issue_created: "Issue created" |
|
| 1589 |
webhook_events_issue_updated: "Issue updated" |
|
| 1590 |
webhook_events_issue_closed: "Issue closed" |
|
| 1591 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1592 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1593 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1594 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1595 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1596 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1597 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1598 |
permission_use_webhooks: "Use webhooks" |
|
| 1599 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1600 |
label_integrations: "Integrations" |
|
| config/locales/bg.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/bg.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1432 | 1432 |
text_project_destroy_enter_identifier: За да потвърдите действието, въведете идентификатора на проекта (%{identifier}) по-долу.
|
| 1433 | 1433 |
field_name_or_email_or_login: Име, e-mail или login име |
| 1434 | 1434 |
twofa_already_setup: Two-factor authentication already set up |
| 1435 | ||
| 1436 |
label_webhook_plural: "Webhooks" |
|
| 1437 |
label_webhook_new: "New webhook" |
|
| 1438 |
label_webhook_edit: "Edit webhook" |
|
| 1439 |
label_webhook_events: "Events" |
|
| 1440 |
webhook_events_issue: "Issues" |
|
| 1441 |
webhook_events_issue_created: "Issue created" |
|
| 1442 |
webhook_events_issue_updated: "Issue updated" |
|
| 1443 |
webhook_events_issue_closed: "Issue closed" |
|
| 1444 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1445 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1446 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1447 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1448 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1449 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1450 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1451 |
permission_use_webhooks: "Use webhooks" |
|
| 1452 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1453 |
label_integrations: "Integrations" |
|
| config/locales/bs.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/bs.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1474 | 1474 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1475 | 1475 |
only for dev/test changed |
| 1476 | 1476 |
twofa_already_setup: Two-factor authentication already set up |
| 1477 | ||
| 1478 |
label_webhook_plural: "Webhooks" |
|
| 1479 |
label_webhook_new: "New webhook" |
|
| 1480 |
label_webhook_edit: "Edit webhook" |
|
| 1481 |
label_webhook_events: "Events" |
|
| 1482 |
webhook_events_issue: "Issues" |
|
| 1483 |
webhook_events_issue_created: "Issue created" |
|
| 1484 |
webhook_events_issue_updated: "Issue updated" |
|
| 1485 |
webhook_events_issue_closed: "Issue closed" |
|
| 1486 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1487 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1488 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1489 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1490 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1491 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1492 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1493 |
permission_use_webhooks: "Use webhooks" |
|
| 1494 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1495 |
label_integrations: "Integrations" |
|
| config/locales/ca.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/ca.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1475 | 1475 |
text_select_apply_issue_status: Marca l'estat de la incidència |
| 1476 | 1476 |
field_name_or_email_or_login: Nom, correu o login |
| 1477 | 1477 |
twofa_already_setup: Two-factor authentication already set up |
| 1478 | ||
| 1479 |
label_webhook_plural: "Webhooks" |
|
| 1480 |
label_webhook_new: "New webhook" |
|
| 1481 |
label_webhook_edit: "Edit webhook" |
|
| 1482 |
label_webhook_events: "Events" |
|
| 1483 |
webhook_events_issue: "Issues" |
|
| 1484 |
webhook_events_issue_created: "Issue created" |
|
| 1485 |
webhook_events_issue_updated: "Issue updated" |
|
| 1486 |
webhook_events_issue_closed: "Issue closed" |
|
| 1487 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1488 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1489 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1490 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1491 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1492 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1493 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1494 |
permission_use_webhooks: "Use webhooks" |
|
| 1495 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1496 |
label_integrations: "Integrations" |
|
| config/locales/cs.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/cs.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1469 | 1469 |
text_default_active_job_queue_changed: Výchozí adaptér fronty, vhodný pouze pro |
| 1470 | 1470 |
vývoj/testování, byl změněn |
| 1471 | 1471 |
twofa_already_setup: Two-factor authentication already set up |
| 1472 | ||
| 1473 |
label_webhook_plural: "Webhooks" |
|
| 1474 |
label_webhook_new: "New webhook" |
|
| 1475 |
label_webhook_edit: "Edit webhook" |
|
| 1476 |
label_webhook_events: "Events" |
|
| 1477 |
webhook_events_issue: "Issues" |
|
| 1478 |
webhook_events_issue_created: "Issue created" |
|
| 1479 |
webhook_events_issue_updated: "Issue updated" |
|
| 1480 |
webhook_events_issue_closed: "Issue closed" |
|
| 1481 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1482 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1483 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1484 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1485 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1486 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1487 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1488 |
permission_use_webhooks: "Use webhooks" |
|
| 1489 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1490 |
label_integrations: "Integrations" |
|
| config/locales/da.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/da.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1504 | 1504 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1505 | 1505 |
only for dev/test changed |
| 1506 | 1506 |
twofa_already_setup: Two-factor authentication already set up |
| 1507 | ||
| 1508 |
label_webhook_plural: "Webhooks" |
|
| 1509 |
label_webhook_new: "New webhook" |
|
| 1510 |
label_webhook_edit: "Edit webhook" |
|
| 1511 |
label_webhook_events: "Events" |
|
| 1512 |
webhook_events_issue: "Issues" |
|
| 1513 |
webhook_events_issue_created: "Issue created" |
|
| 1514 |
webhook_events_issue_updated: "Issue updated" |
|
| 1515 |
webhook_events_issue_closed: "Issue closed" |
|
| 1516 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1517 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1518 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1519 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1520 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1521 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1522 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1523 |
permission_use_webhooks: "Use webhooks" |
|
| 1524 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1525 |
label_integrations: "Integrations" |
|
| config/locales/de.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/de.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1455 | 1455 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1456 | 1456 |
only for dev/test changed |
| 1457 | 1457 |
twofa_already_setup: Two-factor authentication already set up |
| 1458 | ||
| 1459 |
label_webhook_plural: "Webhooks" |
|
| 1460 |
label_webhook_new: "New webhook" |
|
| 1461 |
label_webhook_edit: "Edit webhook" |
|
| 1462 |
label_webhook_events: "Events" |
|
| 1463 |
webhook_events_issue: "Issues" |
|
| 1464 |
webhook_events_issue_created: "Issue created" |
|
| 1465 |
webhook_events_issue_updated: "Issue updated" |
|
| 1466 |
webhook_events_issue_closed: "Issue closed" |
|
| 1467 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1468 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1469 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1470 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1471 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1472 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1473 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1474 |
permission_use_webhooks: "Use webhooks" |
|
| 1475 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1476 |
label_integrations: "Integrations" |
|
| config/locales/el.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/el.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1487 | 1487 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1488 | 1488 |
only for dev/test changed |
| 1489 | 1489 |
twofa_already_setup: Two-factor authentication already set up |
| 1490 | ||
| 1491 |
label_webhook_plural: "Webhooks" |
|
| 1492 |
label_webhook_new: "New webhook" |
|
| 1493 |
label_webhook_edit: "Edit webhook" |
|
| 1494 |
label_webhook_events: "Events" |
|
| 1495 |
webhook_events_issue: "Issues" |
|
| 1496 |
webhook_events_issue_created: "Issue created" |
|
| 1497 |
webhook_events_issue_updated: "Issue updated" |
|
| 1498 |
webhook_events_issue_closed: "Issue closed" |
|
| 1499 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1500 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1501 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1502 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1503 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1504 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1505 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1506 |
permission_use_webhooks: "Use webhooks" |
|
| 1507 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1508 |
label_integrations: "Integrations" |
|
| config/locales/en-GB.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/en-GB.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1488 | 1488 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1489 | 1489 |
only for dev/test changed |
| 1490 | 1490 |
twofa_already_setup: Two-factor authentication already set up |
| 1491 | ||
| 1492 |
label_webhook_plural: "Webhooks" |
|
| 1493 |
label_webhook_new: "New webhook" |
|
| 1494 |
label_webhook_edit: "Edit webhook" |
|
| 1495 |
label_webhook_events: "Events" |
|
| 1496 |
webhook_events_issue: "Issues" |
|
| 1497 |
webhook_events_issue_created: "Issue created" |
|
| 1498 |
webhook_events_issue_updated: "Issue updated" |
|
| 1499 |
webhook_events_issue_closed: "Issue closed" |
|
| 1500 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1501 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1502 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1503 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1504 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1505 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1506 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1507 |
permission_use_webhooks: "Use webhooks" |
|
| 1508 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1509 |
label_integrations: "Integrations" |
|
| config/locales/en.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/en.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 922 | 922 |
label_year: Year |
| 923 | 923 |
label_month: Month |
| 924 | 924 |
label_week: Week |
| 925 |
label_webhook_plural: "Webhooks" |
|
| 926 |
label_webhook_new: "New webhook" |
|
| 927 |
label_webhook_edit: "Edit webhook" |
|
| 928 |
label_webhook_events: "Events" |
|
| 929 |
webhook_events_issue: "Issues" |
|
| 930 |
webhook_events_issue_created: "Issue created" |
|
| 931 |
webhook_events_issue_updated: "Issue updated" |
|
| 932 |
webhook_events_issue_closed: "Issue closed" |
|
| 933 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 934 |
webhook_events_wiki_page: "Wiki pages" |
|
| 935 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 936 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 937 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 938 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 939 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 940 |
permission_use_webhooks: "Use webhooks" |
|
| 941 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 942 |
label_integrations: "Integrations" |
|
| 925 | 943 |
label_date_from: From |
| 926 | 944 |
label_date_to: To |
| 927 | 945 |
label_language_based: Based on user's language |
| config/locales/es-PA.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/es-PA.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1517 | 1517 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1518 | 1518 |
only for dev/test changed |
| 1519 | 1519 |
twofa_already_setup: Two-factor authentication already set up |
| 1520 | ||
| 1521 |
label_webhook_plural: "Webhooks" |
|
| 1522 |
label_webhook_new: "New webhook" |
|
| 1523 |
label_webhook_edit: "Edit webhook" |
|
| 1524 |
label_webhook_events: "Events" |
|
| 1525 |
webhook_events_issue: "Issues" |
|
| 1526 |
webhook_events_issue_created: "Issue created" |
|
| 1527 |
webhook_events_issue_updated: "Issue updated" |
|
| 1528 |
webhook_events_issue_closed: "Issue closed" |
|
| 1529 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1530 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1531 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1532 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1533 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1534 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1535 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1536 |
permission_use_webhooks: "Use webhooks" |
|
| 1537 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1538 |
label_integrations: "Integrations" |
|
| config/locales/es.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/es.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1553 | 1553 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1554 | 1554 |
only for dev/test changed |
| 1555 | 1555 |
twofa_already_setup: Two-factor authentication already set up |
| 1556 | ||
| 1557 |
label_webhook_plural: "Webhooks" |
|
| 1558 |
label_webhook_new: "New webhook" |
|
| 1559 |
label_webhook_edit: "Edit webhook" |
|
| 1560 |
label_webhook_events: "Events" |
|
| 1561 |
webhook_events_issue: "Issues" |
|
| 1562 |
webhook_events_issue_created: "Issue created" |
|
| 1563 |
webhook_events_issue_updated: "Issue updated" |
|
| 1564 |
webhook_events_issue_closed: "Issue closed" |
|
| 1565 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1566 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1567 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1568 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1569 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1570 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1571 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1572 |
permission_use_webhooks: "Use webhooks" |
|
| 1573 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1574 |
label_integrations: "Integrations" |
|
| config/locales/et.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/et.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1492 | 1492 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1493 | 1493 |
only for dev/test changed |
| 1494 | 1494 |
twofa_already_setup: Two-factor authentication already set up |
| 1495 | ||
| 1496 |
label_webhook_plural: "Webhooks" |
|
| 1497 |
label_webhook_new: "New webhook" |
|
| 1498 |
label_webhook_edit: "Edit webhook" |
|
| 1499 |
label_webhook_events: "Events" |
|
| 1500 |
webhook_events_issue: "Issues" |
|
| 1501 |
webhook_events_issue_created: "Issue created" |
|
| 1502 |
webhook_events_issue_updated: "Issue updated" |
|
| 1503 |
webhook_events_issue_closed: "Issue closed" |
|
| 1504 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1505 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1506 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1507 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1508 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1509 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1510 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1511 |
permission_use_webhooks: "Use webhooks" |
|
| 1512 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1513 |
label_integrations: "Integrations" |
|
| config/locales/eu.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/eu.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1488 | 1488 |
text_default_active_job_queue_changed: Default queue adapter which is well suited |
| 1489 | 1489 |
only for dev/test changed |
| 1490 | 1490 |
twofa_already_setup: Two-factor authentication already set up |
| 1491 | ||
| 1492 |
label_webhook_plural: "Webhooks" |
|
| 1493 |
label_webhook_new: "New webhook" |
|
| 1494 |
label_webhook_edit: "Edit webhook" |
|
| 1495 |
label_webhook_events: "Events" |
|
| 1496 |
webhook_events_issue: "Issues" |
|
| 1497 |
webhook_events_issue_created: "Issue created" |
|
| 1498 |
webhook_events_issue_updated: "Issue updated" |
|
| 1499 |
webhook_events_issue_closed: "Issue closed" |
|
| 1500 |
webhook_events_issue_deleted: "Issue deleted" |
|
| 1501 |
webhook_events_wiki_page: "Wiki pages" |
|
| 1502 |
webhook_events_wiki_page_created: "Wiki page created" |
|
| 1503 |
webhook_events_wiki_page_updated: "Wiki page updated" |
|
| 1504 |
webhook_events_wiki_page_deleted: "Wiki page deleted" |
|
| 1505 |
webhook_url_info: "Redmine will send a POST request to this URL when one of the selected events in one of the selected projects occurs." |
|
| 1506 |
webhook_secret_info_html: "If set, Redmine will include an HMAC signature in the <code>X-Redmine-Signature-256</code> header." |
|
| 1507 |
permission_use_webhooks: "Use webhooks" |
|
| 1508 |
setting_webhooks_enabled: "Enable webhooks" |
|
| 1509 |
label_integrations: "Integrations" |
|
| config/locales/fa.yml (revision f9673061812c2f08e3775f61597c44826a430ea3) → config/locales/fa.yml (revision 833d7e3d85eb74ec9bc1d45a64a445f6c577a7da) | ||
|---|---|---|
| 1421 | 1421 |
field_name_or_email_or_login: نام، رایانامه یا شناسه کاربری |
| 1422 | 1422 |
text_default_active_job_queue_changed: آداپتور پیشفرض صف که به شکل مناسبی کار میکند تنها برای توسعه/آزمون تغییر کرد |
| 1423 | 1423 |
twofa_already_setup: Two-factor authentication already set up |
| 1424 | ||
| 1425 |
label_webhook_plural: "Webhooks" |
|
| 1426 |
label_webhook_new: "New webhook" |
|
| 1427 |
label_webhook_edit: "Edit webhook" |
|