commit
10bc8cdbc8
@ -0,0 +1,9 @@ |
||||
# Used by travis to bundle this plugin with the OpenProject core. |
||||
# The tested plugin will be moved to the path `./plugins/this` |
||||
# whereas OpenProject will be checked out to `.`. |
||||
|
||||
group :opf_plugins do |
||||
gem 'openproject-webhooks', path: 'plugins/this' |
||||
end |
||||
|
||||
# If the plugin has any dependencies declare them here: |
@ -0,0 +1,64 @@ |
||||
# OpenProject Webhooks Plugin |
||||
|
||||
`openproject-webhooks` is an OpenProject plugin, which adds a webhook API to OpenProject. Other plugins may build upon this plugin to implement their functionality. |
||||
|
||||
External services like [GitHub](https://github.com/finnlabs/openproject-github_integration) or Travis could be integrated with the help of this plugin. |
||||
|
||||
**Note:** This is an infrastructure-only plugin. With this plugin alone, you will not notice any difference in your OpenProject installation. |
||||
|
||||
## Installation |
||||
|
||||
This is an OpenProject plugin, thus we follow the usual OpenProject plugin installation mechanism. |
||||
|
||||
### Requirements |
||||
|
||||
* OpenProject version **3.1.0 or higher** ( or a current installation from the `dev` branch) |
||||
|
||||
### Plugin Installation |
||||
|
||||
Edit the `Gemfile.plugins` file in your openproject-installation directory to contain the following lines: |
||||
|
||||
<pre> |
||||
gem "openproject-webhooks", :git => 'https://github.com/finnlabs/openproject-webhooks.git', :branch => 'stable' |
||||
</pre> |
||||
|
||||
Then update your bundle with: |
||||
|
||||
<pre> |
||||
bundle install |
||||
</pre> |
||||
|
||||
and restart the OpenProject server. |
||||
|
||||
## Contact |
||||
|
||||
OpenProject is supported by its community members, both companies and individuals. |
||||
|
||||
Please find ways to contact us on the OpenProject [support page](https://www.openproject.org/support). |
||||
|
||||
## Contributing |
||||
|
||||
This OpenProject plugin is an open source project and we encourage you to help us out. We'd be happy if you do one of these things: |
||||
|
||||
* Create a new [work package in the Webhooks plugin project on openproject.org](https://www.openproject.org/projects/webhooks/work_packages) if you find a bug or need a feature |
||||
* Help out other people on our [forums](https://www.openproject.org/projects/openproject/boards) |
||||
* Help us [translate this plugin to more languages](https://www.openproject.org/projects/openproject/wiki/Translations) |
||||
* Contribute code via GitHub Pull Requests, see our [contribution page](https://www.openproject.org/projects/openproject/wiki/Contribution) for more information |
||||
|
||||
## Community |
||||
|
||||
OpenProject is driven by an active group of open source enthusiasts: software engineers, project managers, creatives, and consultants. OpenProject is supported by companies as well as individuals. We share the vision to build great open source project collaboration software. |
||||
The [OpenProject Foundation (OPF)](https://www.openproject.org/projects/openproject/wiki/OpenProject_Foundation) will give official guidance to the project and the community and oversees contributions and decisions. |
||||
|
||||
## Repository |
||||
|
||||
This repository contains two main branches: |
||||
|
||||
* `dev`: The main development branch. We try to keep it stable in the sense of all tests are passing, but we don't recommend it for production systems. |
||||
* `stable`: Contains the latest stable release that we recommend for production use. Use this if you always want the latest version of this plugin. |
||||
|
||||
## License |
||||
|
||||
Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
|
||||
This plugin is licensed under the GNU GPL v3. See [doc/COPYRIGHT.md](doc/COPYRIGHT.md) for details. |
@ -0,0 +1,12 @@ |
||||
jQuery(function ($) { |
||||
|
||||
// Toggle selector for new/edit webhooks projects
|
||||
$('input[name="webhook[project_ids]"]').change(function(){ |
||||
$('.webhooks--selected-project-ids').prop('disabled', $(this).val() === 'all'); |
||||
}); |
||||
|
||||
$('input[name="webhook[type_ids]"]').change(function(){ |
||||
$('.webhooks--selected-type-ids').prop('disabled', $(this).val() === 'all'); |
||||
}); |
||||
|
||||
}); |
@ -0,0 +1,18 @@ |
||||
// Add some paddings to action links |
||||
.webhooks--outgoing-webhook-row td.buttons |
||||
a:not(:last-child):after |
||||
content: "," |
||||
padding: 0 1px |
||||
|
||||
.webhooks--delivery-success |
||||
color: #019875 |
||||
|
||||
.webhooks--delivery-error |
||||
color: #c0392b |
||||
|
||||
.webhooks--response-body-modal |
||||
min-width: 25vw |
||||
|
||||
pre |
||||
background: #f1f1f1 |
||||
padding: 5px |
@ -0,0 +1,28 @@ |
||||
<section data-augmented-model-wrapper> |
||||
<a class="modal-wrapper--activation-link" title="<%= log_entry.class.human_attribute_name('response_body') %>"> |
||||
<%= op_icon('icon-info1') %> |
||||
<%= t(:button_show) %> |
||||
</a> |
||||
<div style="display: none" class="modal-wrapper--content ngdialog-theme-openproject"> |
||||
<div class="webhooks--response-body-modal"> |
||||
<div class="modal--header"> |
||||
<h2><%= log_entry.class.human_attribute_name('response_body') %></h2> |
||||
</div> |
||||
<div class="ngdialog-body modal--main -inner-scroll"> |
||||
<h2>Headers</h2> |
||||
<pre class="webhooks--response-headers" ><%- log_entry.response_headers.each do |k,v| -%><strong><%= k -%></strong>: <span><%= v -%></span><br/><%- end -%></pre> |
||||
<hr/> |
||||
<h2>Response body</h2> |
||||
<pre class="webhooks--response-body"> |
||||
<%= log_entry.response_body %> |
||||
</pre> |
||||
</div> |
||||
<div class="modal--footer"> |
||||
<button class="button dynamic-content-modal--close-button button" |
||||
title="<%= t(:button_close) %>"> |
||||
<%= t(:button_close)%> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</section> |
@ -0,0 +1,22 @@ |
||||
module ::Webhooks |
||||
module Outgoing |
||||
module Deliveries |
||||
class RowCell < ::RowCell |
||||
include ::IconsHelper |
||||
|
||||
def log |
||||
model |
||||
end |
||||
|
||||
def time |
||||
model.updated_at.to_s # Force ISO8601 |
||||
end |
||||
|
||||
def response_body |
||||
render(locals: { log_entry: log }, |
||||
prefixes: ["#{::OpenProject::Webhooks::Engine.root}/app/cells/views"]).html_safe |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,27 @@ |
||||
module ::Webhooks |
||||
module Outgoing |
||||
module Deliveries |
||||
class TableCell < ::TableCell |
||||
columns :id, :event_name, :time, :response_code, :response_body |
||||
|
||||
def sortable? |
||||
false |
||||
end |
||||
|
||||
def empty_row_message |
||||
I18n.t 'webhooks.outgoing.deliveries.no_results_table' |
||||
end |
||||
|
||||
def headers |
||||
[ |
||||
['id', caption: I18n.t('attributes.id')], |
||||
['event_name', caption: ::Webhooks::Log.human_attribute_name('event_name')], |
||||
['time', caption: I18n.t('webhooks.outgoing.deliveries.time')], |
||||
['response_code', caption: ::Webhooks::Log.human_attribute_name('response_code')], |
||||
['response_body', caption: ::Webhooks::Log.human_attribute_name('response_body')], |
||||
] |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,83 @@ |
||||
module ::Webhooks |
||||
module Outgoing |
||||
module Webhooks |
||||
class RowCell < ::RowCell |
||||
include ::IconsHelper |
||||
|
||||
def webhook |
||||
model |
||||
end |
||||
|
||||
def name |
||||
link_to webhook.name, |
||||
{ controller: table.target_controller, action: :show, webhook_id: webhook.id } |
||||
end |
||||
|
||||
def enabled |
||||
if webhook.enabled? |
||||
op_icon 'icon-yes' |
||||
end |
||||
end |
||||
|
||||
def events |
||||
selected_events = |
||||
webhook |
||||
.events |
||||
.pluck(:name) |
||||
.map(&method(:lookup_event_name)) |
||||
.compact |
||||
.uniq |
||||
|
||||
count = selected_events.count |
||||
if count <= 3 |
||||
selected_events.join(', ') |
||||
else |
||||
content_tag('span', count, class: 'badge -border-only') |
||||
end |
||||
end |
||||
|
||||
def lookup_event_name(name) |
||||
OpenProject::Webhooks::EventResources.lookup_resource_name(name) |
||||
end |
||||
|
||||
def selected_projects |
||||
selected = webhook.projects.map(&:name) |
||||
|
||||
if selected.empty? |
||||
"(#{I18n.t(:label_all)})" |
||||
elsif selected.size <= 3 |
||||
webhook.projects.pluck(:name).join(', ') |
||||
else |
||||
content_tag('span', selected, class: 'badge -border-only') |
||||
end |
||||
end |
||||
|
||||
def row_css_class |
||||
[ |
||||
'webhooks--outgoing-webhook-row', |
||||
"webhooks--outgoing-webhook-row-#{model.id}" |
||||
].join(' ') |
||||
end |
||||
|
||||
### |
||||
|
||||
def button_links |
||||
[edit_link, delete_link] |
||||
end |
||||
|
||||
def edit_link |
||||
link_to I18n.t(:button_edit), |
||||
{ controller: table.target_controller, action: :edit, webhook_id: webhook.id }, |
||||
class: 'button--link' |
||||
end |
||||
|
||||
def delete_link |
||||
link_to I18n.t(:button_delete), |
||||
{ controller: table.target_controller, action: :destroy, webhook_id: webhook.id }, |
||||
data: { method: 'delete', confirm: I18n.t(:text_are_you_sure) }, |
||||
class: 'button--link' |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,43 @@ |
||||
module ::Webhooks |
||||
module Outgoing |
||||
module Webhooks |
||||
class TableCell < ::TableCell |
||||
columns :name, :enabled, :selected_projects, :events, :description |
||||
|
||||
def initial_sort |
||||
[:id, :asc] |
||||
end |
||||
|
||||
def target_controller |
||||
'webhooks/outgoing/admin' |
||||
end |
||||
|
||||
def sortable? |
||||
false |
||||
end |
||||
|
||||
def inline_create_link |
||||
link_to({ controller: target_controller, action: :new }, |
||||
class: 'webhooks--add-row wp-inline-create--add-link', |
||||
title: I18n.t('webhooks.outgoing.label_add_new')) do |
||||
op_icon('icon icon-add') |
||||
end |
||||
end |
||||
|
||||
def empty_row_message |
||||
I18n.t 'webhooks.outgoing.no_results_table' |
||||
end |
||||
|
||||
def headers |
||||
[ |
||||
['name', caption: I18n.t('attributes.name')], |
||||
['enabled', caption: I18n.t(:label_active)], |
||||
['selected_projects', caption: ::Webhooks::Webhook.human_attribute_name('projects')], |
||||
['events', caption: I18n.t('webhooks.outgoing.label_event_resources')], |
||||
['description', caption: I18n.t('attributes.description')] |
||||
] |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,51 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require 'json' |
||||
|
||||
module Webhooks |
||||
module Incoming |
||||
class HooksController < ApplicationController |
||||
accept_key_auth :handle_hook |
||||
|
||||
# Disable CSRF detection since we openly welcome POSTs here! |
||||
skip_before_action :verify_authenticity_token |
||||
|
||||
# Wrap the JSON body as 'payload' param |
||||
# making it available as params[:payload] |
||||
wrap_parameters :payload |
||||
|
||||
def api_request? |
||||
# OpenProject only allows API requests based on an Accept request header. |
||||
# Webhooks (at least GitHub) don't send an Accept header as they're not interested |
||||
# in any part of the response except the HTTP status code. |
||||
# Also handling requests with a application/json Content-Type as API requests |
||||
# should be safe regarding CSRF as browsers don't send forms as JSON. |
||||
super || request.content_type == "application/json" |
||||
end |
||||
|
||||
def handle_hook |
||||
hook = OpenProject::Webhooks.find(params.require 'hook_name') |
||||
|
||||
if hook |
||||
code = hook.handle(request, params, find_current_user) |
||||
head code.is_a?(Integer) ? code : 200 |
||||
else |
||||
head :not_found |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,84 @@ |
||||
module Webhooks |
||||
module Outgoing |
||||
class AdminController < ::ApplicationController |
||||
layout 'admin' |
||||
menu_item :plugin_webhooks |
||||
|
||||
before_action :require_admin |
||||
before_action :find_webhook, only: [:show, :edit, :update, :destroy] |
||||
|
||||
def index |
||||
@webhooks = webhook_class.all |
||||
end |
||||
|
||||
def show; end |
||||
def edit; end |
||||
|
||||
def new |
||||
@webhook = webhook_class.new_default |
||||
end |
||||
|
||||
def create |
||||
service = ::Webhooks::Outgoing::UpdateWebhookService.new(webhook_class.new_default, current_user: current_user) |
||||
action = service.call(attributes: permitted_webhooks_params) |
||||
if action.success? |
||||
flash[:notice] = I18n.t(:notice_successful_create) |
||||
redirect_to action: :index |
||||
else |
||||
@webhook = action.result |
||||
render action: :new |
||||
end |
||||
end |
||||
|
||||
def update |
||||
service = ::Webhooks::Outgoing::UpdateWebhookService.new(@webhook, current_user: current_user) |
||||
action = service.call(attributes: permitted_webhooks_params) |
||||
if action.success? |
||||
flash[:notice] = I18n.t(:notice_successful_update) |
||||
redirect_to action: :index |
||||
else |
||||
@webhook = action.result |
||||
render action: :edit |
||||
end |
||||
end |
||||
|
||||
def destroy |
||||
if @webhook.destroy |
||||
flash[:notice] = I18n.t(:notice_successful_delete) |
||||
else |
||||
flash[:error] = I18n.t(:error_failed_to_delete_entry) |
||||
end |
||||
|
||||
redirect_to action: :index |
||||
end |
||||
|
||||
private |
||||
|
||||
def find_webhook |
||||
@webhook = webhook_class.find(params[:webhook_id]) |
||||
rescue ActiveRecord::RecordNotFound |
||||
render_404 |
||||
end |
||||
|
||||
def webhook_class |
||||
::Webhooks::Webhook |
||||
end |
||||
|
||||
def permitted_webhooks_params |
||||
params |
||||
.require(:webhook) |
||||
.permit(:name, :description, :url, :secret, :enabled, |
||||
:project_ids, selected_project_ids: [], events: []) |
||||
|
||||
end |
||||
|
||||
def show_local_breadcrumb |
||||
true |
||||
end |
||||
|
||||
def default_breadcrumb |
||||
[] |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,5 @@ |
||||
module Webhooks |
||||
def self.table_name_prefix |
||||
'webhooks_' |
||||
end |
||||
end |
@ -0,0 +1,7 @@ |
||||
module Webhooks |
||||
class Event < ActiveRecord::Base |
||||
belongs_to :webhook |
||||
validates_associated :webhook |
||||
validates_presence_of :name |
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
module Webhooks |
||||
class Log < ActiveRecord::Base |
||||
belongs_to :webhook, foreign_key: :webhooks_webhook_id, class_name: '::Webhooks::Webhook', dependent: :destroy |
||||
|
||||
validates :url, presence: true |
||||
validates :event_name, presence: true |
||||
validates :response_code, presence: true |
||||
|
||||
serialize :response_headers, Hash |
||||
serialize :request_headers, Hash |
||||
|
||||
validates :request_headers, presence: true |
||||
validates :request_body, presence: true |
||||
|
||||
def self.newest(limit: 10) |
||||
order(updated_at: :desc).limit(limit) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,8 @@ |
||||
module Webhooks |
||||
class Project < ActiveRecord::Base |
||||
belongs_to :webhook |
||||
belongs_to :project, class_name: '::Project' |
||||
|
||||
validates_presence_of :project |
||||
end |
||||
end |
@ -0,0 +1,46 @@ |
||||
module Webhooks |
||||
class Webhook < ActiveRecord::Base |
||||
default_scope { order(id: :asc) } |
||||
|
||||
validates_presence_of :name |
||||
validates_presence_of :url |
||||
|
||||
validates_uniqueness_of :name |
||||
validates :url, url: true |
||||
|
||||
has_many :events, foreign_key: :webhooks_webhook_id, class_name: '::Webhooks::Event', dependent: :delete_all |
||||
has_many :webhook_projects, foreign_key: :webhooks_webhook_id, class_name: '::Webhooks::Project', dependent: :delete_all |
||||
has_many :projects, through: :webhook_projects |
||||
has_many :deliveries, foreign_key: :webhooks_webhook_id, class_name: '::Webhooks::Log', dependent: :delete_all |
||||
|
||||
def self.enabled |
||||
where(enabled: true) |
||||
end |
||||
|
||||
def self.with_event_name(event_name) |
||||
enabled |
||||
.joins(:events) |
||||
.where("#{::Webhooks::Event.table_name}.name" => event_name) |
||||
end |
||||
|
||||
def self.new_default |
||||
new all_projects: true, enabled: true |
||||
end |
||||
|
||||
def all_projects? |
||||
!!all_projects |
||||
end |
||||
|
||||
def enabled? |
||||
!!enabled |
||||
end |
||||
|
||||
def event_names |
||||
events.pluck(:name) |
||||
end |
||||
|
||||
def event_names=(names) |
||||
self.events = names.map { |name| events.build(name: name) } |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,50 @@ |
||||
module Webhooks |
||||
module Outgoing |
||||
class UpdateWebhookService |
||||
attr_reader :current_user |
||||
attr_reader :webhook |
||||
|
||||
def initialize(webhook, current_user:) |
||||
@current_user = current_user |
||||
@webhook = webhook |
||||
end |
||||
|
||||
def call(attributes: {}) |
||||
::Webhooks::Webhook.transaction do |
||||
set_attributes attributes |
||||
raise ActiveRecord::Rollback unless (webhook.errors.empty? && webhook.save) |
||||
end |
||||
|
||||
ServiceResult.new success: webhook.errors.empty? , errors: webhook.errors, result: webhook |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_attributes(params) |
||||
set_selected_projects!(params) |
||||
set_selected_events!(params) |
||||
|
||||
webhook.attributes = params |
||||
end |
||||
|
||||
def set_selected_events!(params) |
||||
webhook.event_names = params.delete(:events).select(&:present?) |
||||
end |
||||
|
||||
def set_selected_projects!(params) |
||||
option = params.delete :project_ids |
||||
selected = params.delete :selected_project_ids |
||||
|
||||
if option == 'all' |
||||
webhook.all_projects = true |
||||
else |
||||
webhook.all_projects = false |
||||
webhook.project_ids = selected |
||||
end |
||||
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e |
||||
Rails.logger.error "Failed to set project association on webhook: #{e}" |
||||
webhook.errors.add :project_ids, :invalid |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,98 @@ |
||||
<fieldset class="form--fieldset"> |
||||
<p> |
||||
<%= t 'webhooks.outgoing.form.introduction' %> |
||||
<br/> |
||||
<%= link_to t('webhooks.outgoing.form.apiv3_doc_url'), OpenProject::Static::Links.links[:api_docs][:href] %> |
||||
</p> |
||||
|
||||
<div class="form--field"> |
||||
<%= f.text_field :name, required: true, container_class: '-middle' %> |
||||
</div> |
||||
|
||||
<div class="form--field"> |
||||
<%= f.url_field :url, required: true, container_class: '-wide' %> |
||||
</div> |
||||
|
||||
<div class="form--field"> |
||||
<%= f.text_area :description, placeholder: t('webhooks.outgoing.form.description.placeholder'), container_class: '-wide' %> |
||||
</div> |
||||
|
||||
<div class="form--field"> |
||||
<%= f.text_field :secret, container_class: '-wide' %> |
||||
<div class="form--field-instructions"> |
||||
<%= t('webhooks.outgoing.form.secret.description') %> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="form--field"> |
||||
<%= f.check_box :enabled %> |
||||
<div class="form--field-instructions"> |
||||
<%= t('webhooks.outgoing.form.enabled.description') %> |
||||
</div> |
||||
</div> |
||||
</fieldset> |
||||
|
||||
<fieldset class="form--fieldset" id="webhooks-selected-events"> |
||||
<legend class="form--fieldset-legend" title="<%= t 'webhooks.outgoing.form.events.title' %>"> |
||||
<%= t 'webhooks.outgoing.form.events.title' %> |
||||
</legend> |
||||
<div class="form--fieldset-control"> |
||||
<span class="form--fieldset-control-container"> |
||||
(<%= check_all_links 'webhooks-selected-events' %>) |
||||
</span> |
||||
</div> |
||||
|
||||
<% event_names = @webhook.event_names %> |
||||
<% OpenProject::Webhooks::EventResources.available_events_map.each do |resource_label, events| %> |
||||
<div class="form--field"> |
||||
<label class="form--label"><%= resource_label %></label> |
||||
<div class="form--field-container -vertical"> |
||||
<% events.each do |key, label| %> |
||||
<label class="form--label-with-check-box"> |
||||
<%= styled_check_box_tag 'webhook[events][]', |
||||
key, |
||||
event_names.include?(key) -%> |
||||
<%= label %> |
||||
</label> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
<% end %> |
||||
</fieldset> |
||||
|
||||
<fieldset class="form--fieldset"> |
||||
<legend class="form--fieldset-legend" title="<%= 'webhooks.outgoing.form.project_ids.title' %>"> |
||||
<%= t 'webhooks.outgoing.form.project_ids.title' %> |
||||
</legend> |
||||
<p><%= t('webhooks.outgoing.form.project_ids.description') %></p> |
||||
<div class="form--field"> |
||||
<%= f.radio_button :project_ids, |
||||
'all', |
||||
checked: @webhook.all_projects?, |
||||
label: t('webhooks.outgoing.form.project_ids.all'), |
||||
container_class: '-wide' %> |
||||
</div> |
||||
<div class="form--field"> |
||||
<%= f.radio_button :project_ids, |
||||
'selection', |
||||
checked: !@webhook.all_projects?, |
||||
label: t('webhooks.outgoing.form.project_ids.selected'), |
||||
container_class: '-wide' %> |
||||
</div> |
||||
|
||||
<div class="form--field"> |
||||
<label class="form--label"><%= t 'webhooks.outgoing.form.selected_project_ids.title' %></label> |
||||
<div class="form--field-container -vertical"> |
||||
<% Project.pluck(:id, :name).each do |id, name| %> |
||||
<label class="form--label-with-check-box"> |
||||
<%= styled_check_box_tag 'webhook[selected_project_ids][]', |
||||
id, |
||||
!@webhook.all_projects? && @webhook.project_ids.include?(id), |
||||
disabled: @webhook.all_projects?, |
||||
class: 'webhooks--selected-project-ids' -%> |
||||
<%= name %> |
||||
</label> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
</fieldset> |
@ -0,0 +1,4 @@ |
||||
<% content_for :header_tags do %> |
||||
<%= stylesheet_link_tag('webhooks/webhooks.css') %> |
||||
<%= javascript_include_tag('webhooks/webhooks.js') %> |
||||
<% end %> |
@ -0,0 +1,23 @@ |
||||
|
||||
<%= render partial: 'header_tags' %> |
||||
<% html_title(t(:label_administration), t('webhooks.outgoing.label_edit')) -%> |
||||
<% local_assigns[:additional_breadcrumb] = [ |
||||
link_to(t('webhooks.plural'), admin_outgoing_webhooks_path), |
||||
t('webhooks.outgoing.label_edit') |
||||
] |
||||
%> |
||||
|
||||
<%= toolbar title: t('webhooks.outgoing.label_edit') %> |
||||
|
||||
<%= error_messages_for @webhook %> |
||||
|
||||
<%= labelled_tabular_form_for @webhook, |
||||
url: { action: :update }, |
||||
as: 'webhook', |
||||
html: { class: 'form', autocomplete: 'off' } do |f| %> |
||||
<%= render partial: "form", locals: { f: f, webhook: @webhook } %> |
||||
<p> |
||||
<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %> |
||||
<%= link_to t(:button_cancel), { action: :index }, class: 'button' %> |
||||
</p> |
||||
<% end %> |
@ -0,0 +1,19 @@ |
||||
<%= render partial: 'header_tags' %> |
||||
|
||||
<% html_title(t(:label_administration), t('webhooks.plural')) -%> |
||||
<% local_assigns[:additional_breadcrumb] = t('webhooks.plural') %> |
||||
|
||||
<%= toolbar title: t('webhooks.plural') do %> |
||||
<li class="toolbar-item"> |
||||
<%= link_to new_admin_outgoing_webhook_path, |
||||
{ class: 'button -alt-highlight', |
||||
aria: {label: t('webhooks.outgoing.label_add_new')}, |
||||
title: t('webhooks.outgoing.label_add_new')} do %> |
||||
<%= op_icon('button--icon icon-add') %> |
||||
<span class="button--text"><%= t('webhooks.singular') %></span> |
||||
<% end %> |
||||
</li> |
||||
<% end %> |
||||
|
||||
<%= cell ::Webhooks::Outgoing::Webhooks::TableCell, @webhooks %> |
||||
|
@ -0,0 +1,23 @@ |
||||
|
||||
<%= render partial: 'header_tags' %> |
||||
<% html_title(t(:label_administration), t('webhooks.outgoing.label_add_new')) -%> |
||||
<% local_assigns[:additional_breadcrumb] = [ |
||||
link_to(t('webhooks.plural'), admin_outgoing_webhooks_path), |
||||
t('webhooks.outgoing.label_add_new') |
||||
] |
||||
%> |
||||
|
||||
<%= toolbar title: t('webhooks.outgoing.label_add_new') %> |
||||
|
||||
<%= error_messages_for @webhook %> |
||||
|
||||
<%= labelled_tabular_form_for @webhook, |
||||
url: { action: :create }, |
||||
as: 'webhook', |
||||
html: { class: 'form', autocomplete: 'off' } do |f| %> |
||||
<%= render partial: "form", locals: { f: f, webhook: @webhook } %> |
||||
<p> |
||||
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-checkmark' %> |
||||
<%= link_to t(:button_cancel), { action: :index }, class: 'button' %> |
||||
</p> |
||||
<% end %> |
@ -0,0 +1,74 @@ |
||||
|
||||
<%= render partial: 'header_tags' %> |
||||
<% html_title(t(:label_administration), t('webhooks.singular'), @webhook.name) -%> |
||||
<% local_assigns[:additional_breadcrumb] = [ |
||||
link_to(t('webhooks.plural'), admin_outgoing_webhooks_path), |
||||
@webhook.name |
||||
] |
||||
%> |
||||
|
||||
<%= toolbar title: "#{t('webhooks.singular')} - #{@webhook.name}" do %> |
||||
<li class="toolbar-item"> |
||||
<%= link_to edit_admin_outgoing_webhook_path(@webhook), |
||||
{ class: 'button', |
||||
aria: {label: t(:label_edit)}, |
||||
title: t(:label_edit)} do %> |
||||
<%= op_icon('button--icon icon-edit') %> |
||||
<span class="button--text"><%= t(:label_edit) %></span> |
||||
<% end %> |
||||
</li> |
||||
<li class="toolbar-item"> |
||||
<%= link_to admin_outgoing_webhook_path(@webhook), |
||||
class: 'button -danger', |
||||
data: { method: 'delete', confirm: I18n.t(:text_are_you_sure) } do %> |
||||
<%= op_icon('button--icon icon-delete') %> |
||||
<span class="button--text"><%= t(:button_delete) %></span> |
||||
<% end %> |
||||
</li> |
||||
<% end %> |
||||
|
||||
<%= cell ::Components::OnOffStatusCell, |
||||
is_on: @webhook.enabled?, |
||||
on_text: t('webhooks.outgoing.status.enabled'), |
||||
on_description: t('webhooks.outgoing.status.enabled_text'), |
||||
off_text: t('webhooks.outgoing.status.disabled'), |
||||
off_description: t('webhooks.outgoing.status.disabled_text') %> |
||||
|
||||
<div class="account--section"> |
||||
<div class="attributes-group"> |
||||
<div class="attributes-key-value"> |
||||
|
||||
<div class="attributes-key-value--key"><%= ::Webhooks::Webhook.human_attribute_name('events') %></div> |
||||
<div class="attributes-key-value--value-container"> |
||||
<div class="attributes-key-value--value -text"> |
||||
<span><%= @webhook.event_names.join(', ') %></span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="attributes-key-value--key"><%= ::Webhooks::Webhook.human_attribute_name('projects') %></div> |
||||
<div class="attributes-key-value--value-container"> |
||||
<div class="attributes-key-value--value -text"> |
||||
<span> |
||||
<% if @webhook.all_projects? %> |
||||
(<%= t(:label_all) %>) |
||||
<% else %> |
||||
<%= @webhook.projects.join(', ') %> |
||||
<% end %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="attributes-key-value--key"><%= ::Webhooks::Webhook.human_attribute_name('description') %></div> |
||||
<div class="attributes-key-value--value-container"> |
||||
<div class="attributes-key-value--value -text"> |
||||
<span><%= @webhook.description %></span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="account--section"> |
||||
<h2><%= t 'webhooks.outgoing.deliveries.title' %></h2> |
||||
<%= cell ::Webhooks::Outgoing::Deliveries::TableCell, @webhook.deliveries.newest %> |
||||
</div> |
@ -0,0 +1,34 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class WebhookJob < ApplicationJob |
||||
def perform |
||||
|
||||
end |
||||
end |
@ -0,0 +1,108 @@ |
||||
require 'rest-client' |
||||
|
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
class WorkPackageWebhookJob < WebhookJob |
||||
attr_reader :webhook_id |
||||
attr_reader :journal_id |
||||
attr_reader :event_name |
||||
|
||||
def initialize(webhook_id, journal_id, event_name) |
||||
@webhook_id = webhook_id |
||||
@journal_id = journal_id |
||||
@event_name = event_name |
||||
end |
||||
|
||||
def perform |
||||
body = request_body |
||||
headers = request_headers |
||||
exception = nil |
||||
|
||||
if signature = request_signature(body) |
||||
headers['X-OP-Signature'] = signature |
||||
end |
||||
|
||||
response = RestClient.post webhook.url, request_body, headers |
||||
rescue RestClient::Exception => e |
||||
response = e.response |
||||
|
||||
raise e |
||||
rescue => e |
||||
exception = e |
||||
|
||||
raise e |
||||
ensure |
||||
::Webhooks::Log.create( |
||||
webhook: webhook, |
||||
event_name: event_name, |
||||
url: webhook.url, |
||||
request_headers: headers, |
||||
request_body: body, |
||||
response_code: response.try(:code).to_i, |
||||
response_headers: response.try(:headers), |
||||
response_body: response.try(:to_s) || exception.try(:message) |
||||
) |
||||
end |
||||
|
||||
def request_signature(request_body) |
||||
if secret = webhook.secret.presence |
||||
'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), secret, request_body) |
||||
end |
||||
end |
||||
|
||||
def request_headers |
||||
{ |
||||
content_type: "application/json", |
||||
accept: "application/json" |
||||
} |
||||
end |
||||
|
||||
def request_body |
||||
'{"action":"' + event_name + '","work_package":' + work_package_json + '}' |
||||
end |
||||
|
||||
def work_package_json |
||||
::API::V3::WorkPackages::WorkPackageRepresenter |
||||
.create(work_package, current_user: User.admin.first, embed_links: true) |
||||
.to_json |
||||
end |
||||
|
||||
def work_package |
||||
journal.journable |
||||
end |
||||
|
||||
def journal |
||||
@journal ||= Journal.find(journal_id) |
||||
end |
||||
|
||||
def webhook |
||||
@webhook ||= Webhooks::Webhook.find(webhook_id) |
||||
end |
||||
end |
@ -0,0 +1,57 @@ |
||||
en: |
||||
activerecord: |
||||
attributes: |
||||
webhooks/webhook: |
||||
url: 'Payload URL' |
||||
secret: 'Signature secret' |
||||
events: 'Events' |
||||
projects: 'Enabled projects' |
||||
webhooks/log: |
||||
event_name: 'Event name' |
||||
url: 'Payload URL' |
||||
response_code: 'Response code' |
||||
response_body: 'Response' |
||||
models: |
||||
webhooks/outgoing_webhook: "Outgoing webhook" |
||||
webhooks: |
||||
singular: Webhook |
||||
plural: Webhooks |
||||
outgoing: |
||||
no_results_table: No webhooks have been defined yet. |
||||
label_add_new: Add new webhook |
||||
label_edit: Edit webhook |
||||
label_event_resources: Event resources |
||||
events: |
||||
created: "Created" |
||||
updated: "Updated" |
||||
status: |
||||
enabled: 'Webhook is enabled' |
||||
disabled: 'Webhook is disabled' |
||||
enabled_text: 'The webhook will emit payloads for the defined events below.' |
||||
disabled_text: 'Click the edit button to activate the webhook.' |
||||
deliveries: |
||||
no_results_table: No deliveries have been made for this webhook. |
||||
title: 'Recent deliveries' |
||||
time: 'Delivery time' |
||||
form: |
||||
introduction: > |
||||
Send a POST request to the payload URL below for any event in the project your subscribe. |
||||
Payload will correspond to the APIv3 representation of the object being modified. |
||||
apiv3_doc_url: For more information, visit the API documentation |
||||
description: |
||||
placeholder: 'Optional description for the webhook.' |
||||
enabled: |
||||
description: > |
||||
When checked, the webhook will trigger on the selected events. Uncheck to disable the webhook. |
||||
events: |
||||
title: 'Enabled events' |
||||
project_ids: |
||||
title: 'Enabled projects' |
||||
description: 'Select for which projects this webhook should be executed for.' |
||||
all: 'All projects' |
||||
selected: 'Selected projects only' |
||||
selected_project_ids: |
||||
title: 'Selected projects' |
||||
secret: |
||||
description: > |
||||
If set, this secret value is used by OpenProject to sign the webhook payload. |
@ -0,0 +1,26 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
OpenProject::Application.routes.draw do |
||||
namespace 'webhooks' do |
||||
match ":hook_name", to: 'incoming/hooks#handle_hook', via: %i(get post) |
||||
end |
||||
|
||||
scope 'admin' do |
||||
resources :webhooks, |
||||
param: :webhook_id, |
||||
controller: 'webhooks/outgoing/admin', |
||||
as: 'admin_outgoing_webhooks' |
||||
end |
||||
end |
@ -0,0 +1,24 @@ |
||||
class AddWebhooks < ActiveRecord::Migration[5.0] |
||||
def change |
||||
create_table :webhooks_webhooks do |t| |
||||
t.string :name |
||||
t.text :url |
||||
t.text :description, null: false |
||||
t.string :secret, null: true |
||||
t.boolean :enabled, null: false |
||||
t.boolean :all_projects, null: false |
||||
|
||||
t.timestamps |
||||
end |
||||
|
||||
create_table :webhooks_events do |t| |
||||
t.string :name |
||||
t.references :webhooks_webhook, index: true, foreign_key: true |
||||
end |
||||
|
||||
create_table :webhooks_projects do |t| |
||||
t.references :project, index: true, foreign_key: true |
||||
t.references :webhooks_webhook, index: true, foreign_key: true |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
class CreateWebhookLogs < ActiveRecord::Migration[5.0] |
||||
def change |
||||
create_table :webhooks_logs do |t| |
||||
t.references :webhooks_webhook, foreign_key: { on_delete: :cascade } |
||||
|
||||
t.string :event_name |
||||
t.string :url |
||||
|
||||
t.text :request_headers |
||||
t.text :request_body |
||||
|
||||
t.integer :response_code |
||||
t.text :response_headers |
||||
t.text :response_body |
||||
|
||||
t.timestamps |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,18 @@ |
||||
<!---- copyright |
||||
OpenProject is a project management system. |
||||
Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License version 3. |
||||
|
||||
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. |
||||
|
||||
See doc/COPYRIGHT.md for more details. |
||||
|
||||
++--> |
||||
|
||||
# Changelog |
||||
|
||||
* `#<ticket number>` Create plugin |
@ -0,0 +1,16 @@ |
||||
OpenProject is a project management system. |
||||
|
||||
Copyright (C) 2013 the OpenProject Foundation (OPF) |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License |
||||
version 3. |
||||
|
||||
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. |
@ -0,0 +1,11 @@ |
||||
OpenProject is a project management system. |
||||
Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
|
||||
This program is free software; you can redistribute it and/or |
||||
modify it under the terms of the GNU General Public License version 3. |
||||
|
||||
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. |
||||
|
||||
See doc/COPYRIGHT.md for more details. |
@ -0,0 +1,674 @@ |
||||
GNU GENERAL PUBLIC LICENSE |
||||
Version 3, 29 June 2007 |
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |
||||
Everyone is permitted to copy and distribute verbatim copies |
||||
of this license document, but changing it is not allowed. |
||||
|
||||
Preamble |
||||
|
||||
The GNU General Public License is a free, copyleft license for |
||||
software and other kinds of works. |
||||
|
||||
The licenses for most software and other practical works are designed |
||||
to take away your freedom to share and change the works. By contrast, |
||||
the GNU General Public License is intended to guarantee your freedom to |
||||
share and change all versions of a program--to make sure it remains free |
||||
software for all its users. We, the Free Software Foundation, use the |
||||
GNU General Public License for most of our software; it applies also to |
||||
any other work released this way by its authors. You can apply it to |
||||
your programs, too. |
||||
|
||||
When we speak of free software, we are referring to freedom, not |
||||
price. Our General Public Licenses are designed to make sure that you |
||||
have the freedom to distribute copies of free software (and charge for |
||||
them if you wish), that you receive source code or can get it if you |
||||
want it, that you can change the software or use pieces of it in new |
||||
free programs, and that you know you can do these things. |
||||
|
||||
To protect your rights, we need to prevent others from denying you |
||||
these rights or asking you to surrender the rights. Therefore, you have |
||||
certain responsibilities if you distribute copies of the software, or if |
||||
you modify it: responsibilities to respect the freedom of others. |
||||
|
||||
For example, if you distribute copies of such a program, whether |
||||
gratis or for a fee, you must pass on to the recipients the same |
||||
freedoms that you received. You must make sure that they, too, receive |
||||
or can get the source code. And you must show them these terms so they |
||||
know their rights. |
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps: |
||||
(1) assert copyright on the software, and (2) offer you this License |
||||
giving you legal permission to copy, distribute and/or modify it. |
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains |
||||
that there is no warranty for this free software. For both users' and |
||||
authors' sake, the GPL requires that modified versions be marked as |
||||
changed, so that their problems will not be attributed erroneously to |
||||
authors of previous versions. |
||||
|
||||
Some devices are designed to deny users access to install or run |
||||
modified versions of the software inside them, although the manufacturer |
||||
can do so. This is fundamentally incompatible with the aim of |
||||
protecting users' freedom to change the software. The systematic |
||||
pattern of such abuse occurs in the area of products for individuals to |
||||
use, which is precisely where it is most unacceptable. Therefore, we |
||||
have designed this version of the GPL to prohibit the practice for those |
||||
products. If such problems arise substantially in other domains, we |
||||
stand ready to extend this provision to those domains in future versions |
||||
of the GPL, as needed to protect the freedom of users. |
||||
|
||||
Finally, every program is threatened constantly by software patents. |
||||
States should not allow patents to restrict development and use of |
||||
software on general-purpose computers, but in those that do, we wish to |
||||
avoid the special danger that patents applied to a free program could |
||||
make it effectively proprietary. To prevent this, the GPL assures that |
||||
patents cannot be used to render the program non-free. |
||||
|
||||
The precise terms and conditions for copying, distribution and |
||||
modification follow. |
||||
|
||||
TERMS AND CONDITIONS |
||||
|
||||
0. Definitions. |
||||
|
||||
"This License" refers to version 3 of the GNU General Public License. |
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of |
||||
works, such as semiconductor masks. |
||||
|
||||
"The Program" refers to any copyrightable work licensed under this |
||||
License. Each licensee is addressed as "you". "Licensees" and |
||||
"recipients" may be individuals or organizations. |
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work |
||||
in a fashion requiring copyright permission, other than the making of an |
||||
exact copy. The resulting work is called a "modified version" of the |
||||
earlier work or a work "based on" the earlier work. |
||||
|
||||
A "covered work" means either the unmodified Program or a work based |
||||
on the Program. |
||||
|
||||
To "propagate" a work means to do anything with it that, without |
||||
permission, would make you directly or secondarily liable for |
||||
infringement under applicable copyright law, except executing it on a |
||||
computer or modifying a private copy. Propagation includes copying, |
||||
distribution (with or without modification), making available to the |
||||
public, and in some countries other activities as well. |
||||
|
||||
To "convey" a work means any kind of propagation that enables other |
||||
parties to make or receive copies. Mere interaction with a user through |
||||
a computer network, with no transfer of a copy, is not conveying. |
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices" |
||||
to the extent that it includes a convenient and prominently visible |
||||
feature that (1) displays an appropriate copyright notice, and (2) |
||||
tells the user that there is no warranty for the work (except to the |
||||
extent that warranties are provided), that licensees may convey the |
||||
work under this License, and how to view a copy of this License. If |
||||
the interface presents a list of user commands or options, such as a |
||||
menu, a prominent item in the list meets this criterion. |
||||
|
||||
1. Source Code. |
||||
|
||||
The "source code" for a work means the preferred form of the work |
||||
for making modifications to it. "Object code" means any non-source |
||||
form of a work. |
||||
|
||||
A "Standard Interface" means an interface that either is an official |
||||
standard defined by a recognized standards body, or, in the case of |
||||
interfaces specified for a particular programming language, one that |
||||
is widely used among developers working in that language. |
||||
|
||||
The "System Libraries" of an executable work include anything, other |
||||
than the work as a whole, that (a) is included in the normal form of |
||||
packaging a Major Component, but which is not part of that Major |
||||
Component, and (b) serves only to enable use of the work with that |
||||
Major Component, or to implement a Standard Interface for which an |
||||
implementation is available to the public in source code form. A |
||||
"Major Component", in this context, means a major essential component |
||||
(kernel, window system, and so on) of the specific operating system |
||||
(if any) on which the executable work runs, or a compiler used to |
||||
produce the work, or an object code interpreter used to run it. |
||||
|
||||
The "Corresponding Source" for a work in object code form means all |
||||
the source code needed to generate, install, and (for an executable |
||||
work) run the object code and to modify the work, including scripts to |
||||
control those activities. However, it does not include the work's |
||||
System Libraries, or general-purpose tools or generally available free |
||||
programs which are used unmodified in performing those activities but |
||||
which are not part of the work. For example, Corresponding Source |
||||
includes interface definition files associated with source files for |
||||
the work, and the source code for shared libraries and dynamically |
||||
linked subprograms that the work is specifically designed to require, |
||||
such as by intimate data communication or control flow between those |
||||
subprograms and other parts of the work. |
||||
|
||||
The Corresponding Source need not include anything that users |
||||
can regenerate automatically from other parts of the Corresponding |
||||
Source. |
||||
|
||||
The Corresponding Source for a work in source code form is that |
||||
same work. |
||||
|
||||
2. Basic Permissions. |
||||
|
||||
All rights granted under this License are granted for the term of |
||||
copyright on the Program, and are irrevocable provided the stated |
||||
conditions are met. This License explicitly affirms your unlimited |
||||
permission to run the unmodified Program. The output from running a |
||||
covered work is covered by this License only if the output, given its |
||||
content, constitutes a covered work. This License acknowledges your |
||||
rights of fair use or other equivalent, as provided by copyright law. |
||||
|
||||
You may make, run and propagate covered works that you do not |
||||
convey, without conditions so long as your license otherwise remains |
||||
in force. You may convey covered works to others for the sole purpose |
||||
of having them make modifications exclusively for you, or provide you |
||||
with facilities for running those works, provided that you comply with |
||||
the terms of this License in conveying all material for which you do |
||||
not control copyright. Those thus making or running the covered works |
||||
for you must do so exclusively on your behalf, under your direction |
||||
and control, on terms that prohibit them from making any copies of |
||||
your copyrighted material outside their relationship with you. |
||||
|
||||
Conveying under any other circumstances is permitted solely under |
||||
the conditions stated below. Sublicensing is not allowed; section 10 |
||||
makes it unnecessary. |
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
||||
|
||||
No covered work shall be deemed part of an effective technological |
||||
measure under any applicable law fulfilling obligations under article |
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
||||
similar laws prohibiting or restricting circumvention of such |
||||
measures. |
||||
|
||||
When you convey a covered work, you waive any legal power to forbid |
||||
circumvention of technological measures to the extent such circumvention |
||||
is effected by exercising rights under this License with respect to |
||||
the covered work, and you disclaim any intention to limit operation or |
||||
modification of the work as a means of enforcing, against the work's |
||||
users, your or third parties' legal rights to forbid circumvention of |
||||
technological measures. |
||||
|
||||
4. Conveying Verbatim Copies. |
||||
|
||||
You may convey verbatim copies of the Program's source code as you |
||||
receive it, in any medium, provided that you conspicuously and |
||||
appropriately publish on each copy an appropriate copyright notice; |
||||
keep intact all notices stating that this License and any |
||||
non-permissive terms added in accord with section 7 apply to the code; |
||||
keep intact all notices of the absence of any warranty; and give all |
||||
recipients a copy of this License along with the Program. |
||||
|
||||
You may charge any price or no price for each copy that you convey, |
||||
and you may offer support or warranty protection for a fee. |
||||
|
||||
5. Conveying Modified Source Versions. |
||||
|
||||
You may convey a work based on the Program, or the modifications to |
||||
produce it from the Program, in the form of source code under the |
||||
terms of section 4, provided that you also meet all of these conditions: |
||||
|
||||
a) The work must carry prominent notices stating that you modified |
||||
it, and giving a relevant date. |
||||
|
||||
b) The work must carry prominent notices stating that it is |
||||
released under this License and any conditions added under section |
||||
7. This requirement modifies the requirement in section 4 to |
||||
"keep intact all notices". |
||||
|
||||
c) You must license the entire work, as a whole, under this |
||||
License to anyone who comes into possession of a copy. This |
||||
License will therefore apply, along with any applicable section 7 |
||||
additional terms, to the whole of the work, and all its parts, |
||||
regardless of how they are packaged. This License gives no |
||||
permission to license the work in any other way, but it does not |
||||
invalidate such permission if you have separately received it. |
||||
|
||||
d) If the work has interactive user interfaces, each must display |
||||
Appropriate Legal Notices; however, if the Program has interactive |
||||
interfaces that do not display Appropriate Legal Notices, your |
||||
work need not make them do so. |
||||
|
||||
A compilation of a covered work with other separate and independent |
||||
works, which are not by their nature extensions of the covered work, |
||||
and which are not combined with it such as to form a larger program, |
||||
in or on a volume of a storage or distribution medium, is called an |
||||
"aggregate" if the compilation and its resulting copyright are not |
||||
used to limit the access or legal rights of the compilation's users |
||||
beyond what the individual works permit. Inclusion of a covered work |
||||
in an aggregate does not cause this License to apply to the other |
||||
parts of the aggregate. |
||||
|
||||
6. Conveying Non-Source Forms. |
||||
|
||||
You may convey a covered work in object code form under the terms |
||||
of sections 4 and 5, provided that you also convey the |
||||
machine-readable Corresponding Source under the terms of this License, |
||||
in one of these ways: |
||||
|
||||
a) Convey the object code in, or embodied in, a physical product |
||||
(including a physical distribution medium), accompanied by the |
||||
Corresponding Source fixed on a durable physical medium |
||||
customarily used for software interchange. |
||||
|
||||
b) Convey the object code in, or embodied in, a physical product |
||||
(including a physical distribution medium), accompanied by a |
||||
written offer, valid for at least three years and valid for as |
||||
long as you offer spare parts or customer support for that product |
||||
model, to give anyone who possesses the object code either (1) a |
||||
copy of the Corresponding Source for all the software in the |
||||
product that is covered by this License, on a durable physical |
||||
medium customarily used for software interchange, for a price no |
||||
more than your reasonable cost of physically performing this |
||||
conveying of source, or (2) access to copy the |
||||
Corresponding Source from a network server at no charge. |
||||
|
||||
c) Convey individual copies of the object code with a copy of the |
||||
written offer to provide the Corresponding Source. This |
||||
alternative is allowed only occasionally and noncommercially, and |
||||
only if you received the object code with such an offer, in accord |
||||
with subsection 6b. |
||||
|
||||
d) Convey the object code by offering access from a designated |
||||
place (gratis or for a charge), and offer equivalent access to the |
||||
Corresponding Source in the same way through the same place at no |
||||
further charge. You need not require recipients to copy the |
||||
Corresponding Source along with the object code. If the place to |
||||
copy the object code is a network server, the Corresponding Source |
||||
may be on a different server (operated by you or a third party) |
||||
that supports equivalent copying facilities, provided you maintain |
||||
clear directions next to the object code saying where to find the |
||||
Corresponding Source. Regardless of what server hosts the |
||||
Corresponding Source, you remain obligated to ensure that it is |
||||
available for as long as needed to satisfy these requirements. |
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided |
||||
you inform other peers where the object code and Corresponding |
||||
Source of the work are being offered to the general public at no |
||||
charge under subsection 6d. |
||||
|
||||
A separable portion of the object code, whose source code is excluded |
||||
from the Corresponding Source as a System Library, need not be |
||||
included in conveying the object code work. |
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any |
||||
tangible personal property which is normally used for personal, family, |
||||
or household purposes, or (2) anything designed or sold for incorporation |
||||
into a dwelling. In determining whether a product is a consumer product, |
||||
doubtful cases shall be resolved in favor of coverage. For a particular |
||||
product received by a particular user, "normally used" refers to a |
||||
typical or common use of that class of product, regardless of the status |
||||
of the particular user or of the way in which the particular user |
||||
actually uses, or expects or is expected to use, the product. A product |
||||
is a consumer product regardless of whether the product has substantial |
||||
commercial, industrial or non-consumer uses, unless such uses represent |
||||
the only significant mode of use of the product. |
||||
|
||||
"Installation Information" for a User Product means any methods, |
||||
procedures, authorization keys, or other information required to install |
||||
and execute modified versions of a covered work in that User Product from |
||||
a modified version of its Corresponding Source. The information must |
||||
suffice to ensure that the continued functioning of the modified object |
||||
code is in no case prevented or interfered with solely because |
||||
modification has been made. |
||||
|
||||
If you convey an object code work under this section in, or with, or |
||||
specifically for use in, a User Product, and the conveying occurs as |
||||
part of a transaction in which the right of possession and use of the |
||||
User Product is transferred to the recipient in perpetuity or for a |
||||
fixed term (regardless of how the transaction is characterized), the |
||||
Corresponding Source conveyed under this section must be accompanied |
||||
by the Installation Information. But this requirement does not apply |
||||
if neither you nor any third party retains the ability to install |
||||
modified object code on the User Product (for example, the work has |
||||
been installed in ROM). |
||||
|
||||
The requirement to provide Installation Information does not include a |
||||
requirement to continue to provide support service, warranty, or updates |
||||
for a work that has been modified or installed by the recipient, or for |
||||
the User Product in which it has been modified or installed. Access to a |
||||
network may be denied when the modification itself materially and |
||||
adversely affects the operation of the network or violates the rules and |
||||
protocols for communication across the network. |
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, |
||||
in accord with this section must be in a format that is publicly |
||||
documented (and with an implementation available to the public in |
||||
source code form), and must require no special password or key for |
||||
unpacking, reading or copying. |
||||
|
||||
7. Additional Terms. |
||||
|
||||
"Additional permissions" are terms that supplement the terms of this |
||||
License by making exceptions from one or more of its conditions. |
||||
Additional permissions that are applicable to the entire Program shall |
||||
be treated as though they were included in this License, to the extent |
||||
that they are valid under applicable law. If additional permissions |
||||
apply only to part of the Program, that part may be used separately |
||||
under those permissions, but the entire Program remains governed by |
||||
this License without regard to the additional permissions. |
||||
|
||||
When you convey a copy of a covered work, you may at your option |
||||
remove any additional permissions from that copy, or from any part of |
||||
it. (Additional permissions may be written to require their own |
||||
removal in certain cases when you modify the work.) You may place |
||||
additional permissions on material, added by you to a covered work, |
||||
for which you have or can give appropriate copyright permission. |
||||
|
||||
Notwithstanding any other provision of this License, for material you |
||||
add to a covered work, you may (if authorized by the copyright holders of |
||||
that material) supplement the terms of this License with terms: |
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the |
||||
terms of sections 15 and 16 of this License; or |
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or |
||||
author attributions in that material or in the Appropriate Legal |
||||
Notices displayed by works containing it; or |
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or |
||||
requiring that modified versions of such material be marked in |
||||
reasonable ways as different from the original version; or |
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or |
||||
authors of the material; or |
||||
|
||||
e) Declining to grant rights under trademark law for use of some |
||||
trade names, trademarks, or service marks; or |
||||
|
||||
f) Requiring indemnification of licensors and authors of that |
||||
material by anyone who conveys the material (or modified versions of |
||||
it) with contractual assumptions of liability to the recipient, for |
||||
any liability that these contractual assumptions directly impose on |
||||
those licensors and authors. |
||||
|
||||
All other non-permissive additional terms are considered "further |
||||
restrictions" within the meaning of section 10. If the Program as you |
||||
received it, or any part of it, contains a notice stating that it is |
||||
governed by this License along with a term that is a further |
||||
restriction, you may remove that term. If a license document contains |
||||
a further restriction but permits relicensing or conveying under this |
||||
License, you may add to a covered work material governed by the terms |
||||
of that license document, provided that the further restriction does |
||||
not survive such relicensing or conveying. |
||||
|
||||
If you add terms to a covered work in accord with this section, you |
||||
must place, in the relevant source files, a statement of the |
||||
additional terms that apply to those files, or a notice indicating |
||||
where to find the applicable terms. |
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the |
||||
form of a separately written license, or stated as exceptions; |
||||
the above requirements apply either way. |
||||
|
||||
8. Termination. |
||||
|
||||
You may not propagate or modify a covered work except as expressly |
||||
provided under this License. Any attempt otherwise to propagate or |
||||
modify it is void, and will automatically terminate your rights under |
||||
this License (including any patent licenses granted under the third |
||||
paragraph of section 11). |
||||
|
||||
However, if you cease all violation of this License, then your |
||||
license from a particular copyright holder is reinstated (a) |
||||
provisionally, unless and until the copyright holder explicitly and |
||||
finally terminates your license, and (b) permanently, if the copyright |
||||
holder fails to notify you of the violation by some reasonable means |
||||
prior to 60 days after the cessation. |
||||
|
||||
Moreover, your license from a particular copyright holder is |
||||
reinstated permanently if the copyright holder notifies you of the |
||||
violation by some reasonable means, this is the first time you have |
||||
received notice of violation of this License (for any work) from that |
||||
copyright holder, and you cure the violation prior to 30 days after |
||||
your receipt of the notice. |
||||
|
||||
Termination of your rights under this section does not terminate the |
||||
licenses of parties who have received copies or rights from you under |
||||
this License. If your rights have been terminated and not permanently |
||||
reinstated, you do not qualify to receive new licenses for the same |
||||
material under section 10. |
||||
|
||||
9. Acceptance Not Required for Having Copies. |
||||
|
||||
You are not required to accept this License in order to receive or |
||||
run a copy of the Program. Ancillary propagation of a covered work |
||||
occurring solely as a consequence of using peer-to-peer transmission |
||||
to receive a copy likewise does not require acceptance. However, |
||||
nothing other than this License grants you permission to propagate or |
||||
modify any covered work. These actions infringe copyright if you do |
||||
not accept this License. Therefore, by modifying or propagating a |
||||
covered work, you indicate your acceptance of this License to do so. |
||||
|
||||
10. Automatic Licensing of Downstream Recipients. |
||||
|
||||
Each time you convey a covered work, the recipient automatically |
||||
receives a license from the original licensors, to run, modify and |
||||
propagate that work, subject to this License. You are not responsible |
||||
for enforcing compliance by third parties with this License. |
||||
|
||||
An "entity transaction" is a transaction transferring control of an |
||||
organization, or substantially all assets of one, or subdividing an |
||||
organization, or merging organizations. If propagation of a covered |
||||
work results from an entity transaction, each party to that |
||||
transaction who receives a copy of the work also receives whatever |
||||
licenses to the work the party's predecessor in interest had or could |
||||
give under the previous paragraph, plus a right to possession of the |
||||
Corresponding Source of the work from the predecessor in interest, if |
||||
the predecessor has it or can get it with reasonable efforts. |
||||
|
||||
You may not impose any further restrictions on the exercise of the |
||||
rights granted or affirmed under this License. For example, you may |
||||
not impose a license fee, royalty, or other charge for exercise of |
||||
rights granted under this License, and you may not initiate litigation |
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that |
||||
any patent claim is infringed by making, using, selling, offering for |
||||
sale, or importing the Program or any portion of it. |
||||
|
||||
11. Patents. |
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this |
||||
License of the Program or a work on which the Program is based. The |
||||
work thus licensed is called the contributor's "contributor version". |
||||
|
||||
A contributor's "essential patent claims" are all patent claims |
||||
owned or controlled by the contributor, whether already acquired or |
||||
hereafter acquired, that would be infringed by some manner, permitted |
||||
by this License, of making, using, or selling its contributor version, |
||||
but do not include claims that would be infringed only as a |
||||
consequence of further modification of the contributor version. For |
||||
purposes of this definition, "control" includes the right to grant |
||||
patent sublicenses in a manner consistent with the requirements of |
||||
this License. |
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free |
||||
patent license under the contributor's essential patent claims, to |
||||
make, use, sell, offer for sale, import and otherwise run, modify and |
||||
propagate the contents of its contributor version. |
||||
|
||||
In the following three paragraphs, a "patent license" is any express |
||||
agreement or commitment, however denominated, not to enforce a patent |
||||
(such as an express permission to practice a patent or covenant not to |
||||
sue for patent infringement). To "grant" such a patent license to a |
||||
party means to make such an agreement or commitment not to enforce a |
||||
patent against the party. |
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, |
||||
and the Corresponding Source of the work is not available for anyone |
||||
to copy, free of charge and under the terms of this License, through a |
||||
publicly available network server or other readily accessible means, |
||||
then you must either (1) cause the Corresponding Source to be so |
||||
available, or (2) arrange to deprive yourself of the benefit of the |
||||
patent license for this particular work, or (3) arrange, in a manner |
||||
consistent with the requirements of this License, to extend the patent |
||||
license to downstream recipients. "Knowingly relying" means you have |
||||
actual knowledge that, but for the patent license, your conveying the |
||||
covered work in a country, or your recipient's use of the covered work |
||||
in a country, would infringe one or more identifiable patents in that |
||||
country that you have reason to believe are valid. |
||||
|
||||
If, pursuant to or in connection with a single transaction or |
||||
arrangement, you convey, or propagate by procuring conveyance of, a |
||||
covered work, and grant a patent license to some of the parties |
||||
receiving the covered work authorizing them to use, propagate, modify |
||||
or convey a specific copy of the covered work, then the patent license |
||||
you grant is automatically extended to all recipients of the covered |
||||
work and works based on it. |
||||
|
||||
A patent license is "discriminatory" if it does not include within |
||||
the scope of its coverage, prohibits the exercise of, or is |
||||
conditioned on the non-exercise of one or more of the rights that are |
||||
specifically granted under this License. You may not convey a covered |
||||
work if you are a party to an arrangement with a third party that is |
||||
in the business of distributing software, under which you make payment |
||||
to the third party based on the extent of your activity of conveying |
||||
the work, and under which the third party grants, to any of the |
||||
parties who would receive the covered work from you, a discriminatory |
||||
patent license (a) in connection with copies of the covered work |
||||
conveyed by you (or copies made from those copies), or (b) primarily |
||||
for and in connection with specific products or compilations that |
||||
contain the covered work, unless you entered into that arrangement, |
||||
or that patent license was granted, prior to 28 March 2007. |
||||
|
||||
Nothing in this License shall be construed as excluding or limiting |
||||
any implied license or other defenses to infringement that may |
||||
otherwise be available to you under applicable patent law. |
||||
|
||||
12. No Surrender of Others' Freedom. |
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or |
||||
otherwise) that contradict the conditions of this License, they do not |
||||
excuse you from the conditions of this License. If you cannot convey a |
||||
covered work so as to satisfy simultaneously your obligations under this |
||||
License and any other pertinent obligations, then as a consequence you may |
||||
not convey it at all. For example, if you agree to terms that obligate you |
||||
to collect a royalty for further conveying from those to whom you convey |
||||
the Program, the only way you could satisfy both those terms and this |
||||
License would be to refrain entirely from conveying the Program. |
||||
|
||||
13. Use with the GNU Affero General Public License. |
||||
|
||||
Notwithstanding any other provision of this License, you have |
||||
permission to link or combine any covered work with a work licensed |
||||
under version 3 of the GNU Affero General Public License into a single |
||||
combined work, and to convey the resulting work. The terms of this |
||||
License will continue to apply to the part which is the covered work, |
||||
but the special requirements of the GNU Affero General Public License, |
||||
section 13, concerning interaction through a network will apply to the |
||||
combination as such. |
||||
|
||||
14. Revised Versions of this License. |
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of |
||||
the GNU General Public License from time to time. Such new versions will |
||||
be similar in spirit to the present version, but may differ in detail to |
||||
address new problems or concerns. |
||||
|
||||
Each version is given a distinguishing version number. If the |
||||
Program specifies that a certain numbered version of the GNU General |
||||
Public License "or any later version" applies to it, you have the |
||||
option of following the terms and conditions either of that numbered |
||||
version or of any later version published by the Free Software |
||||
Foundation. If the Program does not specify a version number of the |
||||
GNU General Public License, you may choose any version ever published |
||||
by the Free Software Foundation. |
||||
|
||||
If the Program specifies that a proxy can decide which future |
||||
versions of the GNU General Public License can be used, that proxy's |
||||
public statement of acceptance of a version permanently authorizes you |
||||
to choose that version for the Program. |
||||
|
||||
Later license versions may give you additional or different |
||||
permissions. However, no additional obligations are imposed on any |
||||
author or copyright holder as a result of your choosing to follow a |
||||
later version. |
||||
|
||||
15. Disclaimer of Warranty. |
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
||||
|
||||
16. Limitation of Liability. |
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
||||
SUCH DAMAGES. |
||||
|
||||
17. Interpretation of Sections 15 and 16. |
||||
|
||||
If the disclaimer of warranty and limitation of liability provided |
||||
above cannot be given local legal effect according to their terms, |
||||
reviewing courts shall apply local law that most closely approximates |
||||
an absolute waiver of all civil liability in connection with the |
||||
Program, unless a warranty or assumption of liability accompanies a |
||||
copy of the Program in return for a fee. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
How to Apply These Terms to Your New Programs |
||||
|
||||
If you develop a new program, and you want it to be of the greatest |
||||
possible use to the public, the best way to achieve this is to make it |
||||
free software which everyone can redistribute and change under these terms. |
||||
|
||||
To do so, attach the following notices to the program. It is safest |
||||
to attach them to the start of each source file to most effectively |
||||
state the exclusion of warranty; and each file should have at least |
||||
the "copyright" line and a pointer to where the full notice is found. |
||||
|
||||
<one line to give the program's name and a brief idea of what it does.> |
||||
Copyright (C) <year> <name of author> |
||||
|
||||
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 3 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, see <http://www.gnu.org/licenses/>. |
||||
|
||||
Also add information on how to contact you by electronic and paper mail. |
||||
|
||||
If the program does terminal interaction, make it output a short |
||||
notice like this when it starts in an interactive mode: |
||||
|
||||
<program> Copyright (C) <year> <name of author> |
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
||||
This is free software, and you are welcome to redistribute it |
||||
under certain conditions; type `show c' for details. |
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate |
||||
parts of the General Public License. Of course, your program's commands |
||||
might be different; for a GUI interface, you would use an "about box". |
||||
|
||||
You should also get your employer (if you work as a programmer) or school, |
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. |
||||
For more information on this, and how to apply and follow the GNU GPL, see |
||||
<http://www.gnu.org/licenses/>. |
||||
|
||||
The GNU General Public License does not permit incorporating your program |
||||
into proprietary programs. If your program is a subroutine library, you |
||||
may consider it more useful to permit linking proprietary applications with |
||||
the library. If this is what you want to do, use the GNU Lesser General |
||||
Public License instead of this License. But first, please read |
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>. |
@ -0,0 +1,56 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
module OpenProject |
||||
module Webhooks |
||||
require "open_project/webhooks/engine" |
||||
require "open_project/webhooks/hook" |
||||
|
||||
@@registered_hooks = [] |
||||
|
||||
## |
||||
# Returns a list of currently active webhooks. |
||||
def self.registered_hooks |
||||
@@registered_hooks.dup |
||||
end |
||||
|
||||
## |
||||
# Registers a webhook having name and a callback. |
||||
# The name will be part of the webhook-url and may be used to unregister a webhook later. |
||||
# The callback is executed with two parameters when the webhook was called. |
||||
# The parameters are the hook object, an environment-variables hash and a params hash of the current request. |
||||
# The callback may return an Integer, which is interpreted as a http return code. |
||||
# |
||||
# Returns the newly created hook |
||||
def self.register_hook(name, &callback) |
||||
raise "A hook named '#{name}' is already registered!" if find(name) |
||||
Rails.logger.warn "hook registered" |
||||
hook = Hook.new(name, &callback) |
||||
@@registered_hooks << hook |
||||
hook |
||||
end |
||||
|
||||
# Unregisters a webhook. Might be usefull for tests only, because routes can not |
||||
# be redrawn in a running instance |
||||
def self.unregister_hook(name) |
||||
hook = find(name) |
||||
raise "A hook named '#{name}' was not registered!" unless find(name) |
||||
@@registered_hooks.delete hook |
||||
end |
||||
|
||||
def self.find(name) |
||||
@@registered_hooks.find {|h| h.name == name} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,47 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require 'open_project/plugins' |
||||
|
||||
module OpenProject::Webhooks |
||||
class Engine < ::Rails::Engine |
||||
engine_name :openproject_webhooks |
||||
|
||||
include OpenProject::Plugins::ActsAsOpEngine |
||||
|
||||
register 'openproject-webhooks', |
||||
author_url: 'https://github.com/opf/openproject-webhooks' do |
||||
menu :admin_menu, |
||||
:plugin_webhooks, |
||||
{ controller: 'webhooks/outgoing/admin', action: :index }, |
||||
after: :plugins, |
||||
caption: ->(*) { I18n.t('webhooks.plural') }, |
||||
icon: 'icon2 icon-relations' |
||||
end |
||||
|
||||
config.before_configuration do |app| |
||||
# This is required for the routes to be loaded first as the routes should |
||||
# be prepended so they take precedence over the core. |
||||
app.config.paths['config/routes.rb'].unshift File.join(File.dirname(__FILE__), "..", "..", "..", "config", "routes.rb") |
||||
end |
||||
|
||||
initializer 'webhooks.subscribe_to_notifications' do |
||||
::OpenProject::Webhooks::EventResources.subscribe! |
||||
end |
||||
|
||||
initializer 'webhooks.precompile_assets' do |app| |
||||
app.config.assets.precompile += %w(webhooks/webhooks.css webhooks/webhooks.js) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,41 @@ |
||||
module OpenProject::Webhooks |
||||
module EventResources |
||||
class << self |
||||
def subscribe! |
||||
resource_modules.each do |handler| |
||||
handler.subscribe! |
||||
end |
||||
end |
||||
|
||||
## |
||||
# Return a complete mapping of all resource modules |
||||
# in the form { label => { event1: label , event2: label } } |
||||
def available_events_map |
||||
Hash[resource_modules.map { |m| [m.resource_name, m.available_events_map] }] |
||||
end |
||||
|
||||
## |
||||
# Find a module based on the event name |
||||
def lookup_resource_name(event_name) |
||||
resource = resource_modules.detect { |m| m.available_events_map.key?(event_name) } |
||||
resource.try(:resource_name) |
||||
end |
||||
|
||||
def resource_modules |
||||
@resource_modules ||= begin |
||||
resources.map do |name| |
||||
begin |
||||
"OpenProject::Webhooks::EventResources::#{name.to_s.camelize}".constantize |
||||
rescue NameError => e |
||||
raise ArgumentError, "Failed to initialize resources module for #{name}: #{e}" |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
def resources |
||||
%i(work_package) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,77 @@ |
||||
module OpenProject::Webhooks::EventResources |
||||
class Base |
||||
class << self |
||||
|
||||
## |
||||
# Subscribe for events on this resource schedule the respective |
||||
# webhooks, if any. |
||||
def subscribe! |
||||
notification_names.each do |key| |
||||
OpenProject::Notifications.subscribe(key) do |payload| |
||||
begin |
||||
Rails.logger.debug { "[Webhooks Plugin] Handling notification for '#{key}'." } |
||||
handle_notification(payload, key) |
||||
rescue => e |
||||
Rails.logger.error { "[Webhooks Plugin] Failed notification handling for '#{key}': #{e}" } |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
## |
||||
# Return a mapping of event key to its localized name |
||||
def available_events_map |
||||
Hash[available_actions.map { |symbol| [ prefixed_event_name(symbol), localize_event_name(symbol) ] }] |
||||
end |
||||
|
||||
## |
||||
# Get the prefix key for this module |
||||
def prefix_key |
||||
name.demodulize.underscore |
||||
end |
||||
|
||||
## |
||||
# Create a prefixed event name |
||||
def prefixed_event_name(action) |
||||
"#{prefix_key}:#{action}" |
||||
end |
||||
|
||||
|
||||
def available_actions |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
## |
||||
# Localize the given event name |
||||
def localize_event_name(key) |
||||
I18n.t(key, scope: 'webhooks.outgoing.events') |
||||
end |
||||
|
||||
## |
||||
# Get the name of this resource |
||||
def resource_name |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
## |
||||
# Get the subscriptions for OP::Notifications |
||||
def notification_names |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
protected |
||||
|
||||
## |
||||
# Callback for OP::Notification |
||||
def handle_notification(payload, event_name) |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
## |
||||
# Base scope for active webhooks, helper for subclasses |
||||
def active_webhooks |
||||
::Webhooks::Webhook.where(enabled: true) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,31 @@ |
||||
require_relative 'base' |
||||
|
||||
module OpenProject::Webhooks::EventResources |
||||
class WorkPackage < Base |
||||
class << self |
||||
def notification_names |
||||
[ |
||||
OpenProject::Events::AGGREGATED_WORK_PACKAGE_JOURNAL_READY |
||||
] |
||||
end |
||||
|
||||
def available_actions |
||||
%i(updated created) |
||||
end |
||||
|
||||
def resource_name |
||||
I18n.t :label_work_package_plural |
||||
end |
||||
|
||||
protected |
||||
|
||||
def handle_notification(payload, event_name) |
||||
action = payload[:initial] ? "created" : "updated" |
||||
event_name = prefixed_event_name(action) |
||||
active_webhooks.with_event_name(event_name).pluck(:id).each do |id| |
||||
Delayed::Job.enqueue WorkPackageWebhookJob.new(id, payload[:journal_id], event_name) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,34 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
module OpenProject::Webhooks |
||||
class Hook |
||||
attr_accessor :name, :callback |
||||
|
||||
def initialize(name, &callback) |
||||
super() |
||||
@name = name |
||||
@callback = callback |
||||
end |
||||
|
||||
def relative_url |
||||
"webhooks/#{name}" |
||||
end |
||||
|
||||
def handle(request = Hash.new, params = Hash.new, user = nil) |
||||
callback.call self, request, params, user |
||||
end |
||||
|
||||
end |
||||
end |
@ -0,0 +1,19 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
module OpenProject |
||||
module Webhooks |
||||
VERSION = "8.2.0" |
||||
end |
||||
end |
@ -0,0 +1,15 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require 'open_project/webhooks' |
@ -0,0 +1,20 @@ |
||||
# encoding: UTF-8 |
||||
$:.push File.expand_path("../lib", __FILE__) |
||||
|
||||
require 'open_project/webhooks/version' |
||||
# Describe your gem and declare its dependencies: |
||||
Gem::Specification.new do |s| |
||||
s.name = "openproject-webhooks" |
||||
s.version = OpenProject::Webhooks::VERSION |
||||
s.authors = "OpenProject GmbH" |
||||
s.email = "info@openproject.com" |
||||
s.homepage = "https://community.openproject.org/projects/webhooks" |
||||
s.summary = 'OpenProject Webhooks' |
||||
s.description = 'Provides a plug-in API to support OpenProject webhooks for better 3rd party integration' |
||||
s.license = 'GPLv3' |
||||
|
||||
s.files = Dir["{app,config,db,doc,lib}/**/*"] + %w(README.md) |
||||
|
||||
s.add_dependency 'rails', '~> 5.0' |
||||
|
||||
end |
@ -0,0 +1,223 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::Webhooks::Outgoing::AdminController, type: :controller do |
||||
let(:user) { FactoryBot.build_stubbed :admin } |
||||
|
||||
before do |
||||
login_as user |
||||
end |
||||
|
||||
context 'when not admin' do |
||||
let(:user) { FactoryBot.build_stubbed :user } |
||||
|
||||
it 'renders 403' do |
||||
get :index |
||||
expect(response.status).to eq 403 |
||||
end |
||||
end |
||||
|
||||
context 'when not logged in' do |
||||
let(:user) { User.anonymous } |
||||
|
||||
it 'renders 403' do |
||||
get :index |
||||
expect(response.status).to redirect_to(signin_url(back_url: admin_outgoing_webhooks_url)) |
||||
end |
||||
end |
||||
|
||||
describe '#index' do |
||||
it 'renders the index page' do |
||||
get :index |
||||
expect(response).to be_success |
||||
expect(response).to render_template 'index' |
||||
end |
||||
end |
||||
|
||||
describe '#new' do |
||||
it 'renders the new page' do |
||||
get :new |
||||
expect(response).to be_success |
||||
expect(assigns[:webhook]).to be_new_record |
||||
expect(response).to render_template 'new' |
||||
end |
||||
end |
||||
|
||||
describe '#create' do |
||||
let(:service) { double(::Webhooks::Outgoing::UpdateWebhookService) } |
||||
let(:webhook_params) do |
||||
{ |
||||
name: 'foo', |
||||
enabled: true |
||||
} |
||||
end |
||||
|
||||
describe 'with invalid params' do |
||||
it 'renders an error' do |
||||
post :create, params: { foo: 'bar' } |
||||
expect(response).not_to be_success |
||||
end |
||||
end |
||||
|
||||
describe 'Calling the service' do |
||||
before do |
||||
expect(::Webhooks::Outgoing::UpdateWebhookService) |
||||
.to receive(:new) |
||||
.and_return service |
||||
|
||||
expect(service) |
||||
.to receive(:call) |
||||
.and_return(ServiceResult.new success: success) |
||||
|
||||
post :create, params: { webhook: webhook_params} |
||||
end |
||||
|
||||
context 'when success' do |
||||
let(:success) { true } |
||||
|
||||
it 'renders success' do |
||||
expect(flash[:notice]).to be_present |
||||
expect(response).to redirect_to(action: :index) |
||||
end |
||||
end |
||||
|
||||
context 'when not success' do |
||||
let(:success) { false } |
||||
it 'renders the form again' do |
||||
expect(flash[:notice]).not_to be_present |
||||
expect(response).to render_template 'new' |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#edit' do |
||||
context 'when found' do |
||||
before do |
||||
expect(::Webhooks::Webhook) |
||||
.to receive(:find) |
||||
.and_return(double(::Webhooks::Webhook)) |
||||
end |
||||
|
||||
it 'renders the edit page' do |
||||
get :edit, params: { webhook_id: 'mocked' } |
||||
expect(response).to be_success |
||||
expect(assigns[:webhook]).to be_present |
||||
expect(response).to render_template 'edit' |
||||
end |
||||
end |
||||
|
||||
context 'when not found' do |
||||
it 'renders 404' do |
||||
get :edit, params: { webhook_id: '1234' } |
||||
expect(response).not_to be_success |
||||
expect(response.status).to eq 404 |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#update' do |
||||
let(:service) { double(::Webhooks::Outgoing::UpdateWebhookService) } |
||||
let(:webhook_params) do |
||||
{ |
||||
name: 'foo', |
||||
enabled: true |
||||
} |
||||
end |
||||
|
||||
describe 'when not found' do |
||||
it 'renders an error' do |
||||
put :update, params: { webhook_id: 'bar' } |
||||
expect(response).not_to be_success |
||||
expect(response.status).to eq 404 |
||||
end |
||||
end |
||||
|
||||
describe 'Calling the service' do |
||||
let(:webhook) { double(::Webhooks::Webhook) } |
||||
|
||||
before do |
||||
allow(::Webhooks::Webhook) |
||||
.to receive(:find) |
||||
.and_return(webhook) |
||||
|
||||
expect(::Webhooks::Outgoing::UpdateWebhookService) |
||||
.to receive(:new) |
||||
.and_return service |
||||
|
||||
expect(service) |
||||
.to receive(:call) |
||||
.and_return(ServiceResult.new success: success) |
||||
|
||||
put :update, params: { webhook_id: '1234', webhook: webhook_params} |
||||
end |
||||
|
||||
context 'when success' do |
||||
let(:success) { true } |
||||
|
||||
it 'renders success' do |
||||
expect(flash[:notice]).to be_present |
||||
expect(response).to redirect_to(action: :index) |
||||
end |
||||
end |
||||
|
||||
context 'when not success' do |
||||
let(:success) { false } |
||||
|
||||
it 'renders the form again' do |
||||
expect(flash[:notice]).not_to be_present |
||||
expect(response).to render_template 'edit' |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#destroy' do |
||||
let(:webhook) { double(::Webhooks::Webhook) } |
||||
|
||||
context 'when found' do |
||||
before do |
||||
expect(::Webhooks::Webhook) |
||||
.to receive(:find) |
||||
.and_return(webhook) |
||||
|
||||
expect(webhook) |
||||
.to receive(:destroy) |
||||
.and_return(success) |
||||
end |
||||
|
||||
context 'when delete failed' do |
||||
let(:success) { false } |
||||
|
||||
it 'redirects to index' do |
||||
delete :destroy, params: { webhook_id: 'mocked' } |
||||
expect(response).to be_redirect |
||||
expect(flash[:notice]).not_to be_present |
||||
expect(flash[:error]).to be_present |
||||
end |
||||
end |
||||
|
||||
context 'when delete success' do |
||||
let(:success) { true } |
||||
it 'destroys the object' do |
||||
delete :destroy, params: { webhook_id: 'mocked' } |
||||
expect(response).to be_redirect |
||||
expect(flash[:notice]).to be_present |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,50 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require File.expand_path('../../spec_helper', __FILE__) |
||||
|
||||
|
||||
describe Webhooks::Incoming::HooksController, :type => :controller do |
||||
let(:hook) { double(OpenProject::Webhooks::Hook) } |
||||
let(:user) { double(User).as_null_object } |
||||
|
||||
describe '#handle_hook' do |
||||
before do |
||||
expect(OpenProject::Webhooks).to receive(:find).with('testhook').and_return(hook) |
||||
allow(controller).to receive(:find_current_user).and_return(user) |
||||
end |
||||
|
||||
after do |
||||
# ApplicationController before filter user_setup sets a user |
||||
User.current = nil |
||||
end |
||||
|
||||
it 'should be successful' do |
||||
expect(hook).to receive(:handle) |
||||
|
||||
post :handle_hook, params: { hook_name: 'testhook' } |
||||
|
||||
expect(response).to be_success |
||||
end |
||||
|
||||
it 'should call the hook with a user' do |
||||
expect(hook).to receive(:handle) { |env, params, user| |
||||
expect(user).to equal(user) |
||||
} |
||||
|
||||
post :handle_hook, params: { hook_name: 'testhook' } |
||||
end |
||||
|
||||
end |
||||
end |
@ -0,0 +1,38 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
FactoryBot.define do |
||||
factory :webhook, class: Webhooks::Webhook do |
||||
name "Example Webhook" |
||||
url "http://example.net/webhook_receiver/42" |
||||
description "This is an example webhook" |
||||
secret "42" |
||||
enabled true |
||||
all_projects true |
||||
end |
||||
end |
@ -0,0 +1,40 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
FactoryBot.define do |
||||
factory :webhook_log, class: Webhooks::Log do |
||||
webhook factory: :webhook |
||||
url "http://example.net/webhook_receiver/42" |
||||
event_name 'foobar' |
||||
response_code '200' |
||||
request_headers({ foo: :bar }) |
||||
request_body 'Request body' |
||||
response_headers({ response: :foo }) |
||||
response_body 'Response body' |
||||
end |
||||
end |
@ -0,0 +1,102 @@ |
||||
require 'spec_helper' |
||||
|
||||
describe 'Manage webhooks through UI', type: :feature, js: true do |
||||
before do |
||||
login_as user |
||||
end |
||||
|
||||
context 'as regular user' do |
||||
let(:user) { FactoryBot.create :user } |
||||
|
||||
it 'forbids accessing the webhooks management view' do |
||||
visit admin_outgoing_webhooks_path |
||||
expect(page).to have_selector('h2', text: '403') |
||||
end |
||||
end |
||||
|
||||
context 'as admin' do |
||||
let(:user) { FactoryBot.create :admin } |
||||
let!(:project) { FactoryBot.create :project } |
||||
|
||||
it 'allows the management flow' do |
||||
visit admin_outgoing_webhooks_path |
||||
expect(page).to have_selector('.generic-table--empty-row') |
||||
|
||||
# Visit inline create |
||||
find('.wp-inline-create--add-link').click |
||||
|
||||
# Fill in elements |
||||
fill_in 'webhook_name', with: 'My webhook' |
||||
fill_in 'webhook_url', with: 'http://example.org' |
||||
|
||||
# Check one event |
||||
find('.form--check-box[value="work_package:created"]').set true |
||||
|
||||
# Create |
||||
click_on 'Create' |
||||
|
||||
# |
||||
# 1st webhook created |
||||
# |
||||
|
||||
expect(page).to have_selector('.flash.notice', text: I18n.t(:notice_successful_create)) |
||||
expect(page).to have_selector('.webhooks--outgoing-webhook-row .name', text: 'My webhook') |
||||
webhook = ::Webhooks::Webhook.last |
||||
expect(webhook.event_names).to eq %w(work_package:created) |
||||
expect(webhook.all_projects).to be_truthy |
||||
|
||||
expect(page).to have_selector('.webhooks--outgoing-webhook-row .enabled .icon-yes') |
||||
expect(page).to have_selector('.webhooks--outgoing-webhook-row .selected_projects', text: '(all)') |
||||
expect(page).to have_selector('.webhooks--outgoing-webhook-row .events', text: 'Work packages') |
||||
expect(page).to have_selector('.webhooks--outgoing-webhook-row .description', text: webhook.description) |
||||
|
||||
# Edit this webhook |
||||
find(".webhooks--outgoing-webhook-row-#{webhook.id} a", text: 'Edit').click |
||||
|
||||
# Check the other event |
||||
find('.form--check-box[value="work_package:created"]').set false |
||||
find('.form--check-box[value="work_package:updated"]').set true |
||||
|
||||
# Check a subset of projects |
||||
choose 'webhook_project_ids_selection' |
||||
find(".webhooks--selected-project-ids[value='#{project.id}']").set true |
||||
|
||||
click_on 'Save' |
||||
expect(page).to have_selector('.flash.notice', text: I18n.t(:notice_successful_update)) |
||||
expect(page).to have_selector('.webhooks--outgoing-webhook-row .name', text: 'My webhook') |
||||
webhook = ::Webhooks::Webhook.last |
||||
expect(webhook.event_names).to eq %w(work_package:updated) |
||||
expect(webhook.projects.all).to eq [project] |
||||
expect(webhook.all_projects).to be_falsey |
||||
|
||||
# Delete webhook |
||||
find(".webhooks--outgoing-webhook-row-#{webhook.id} a", text: 'Delete').click |
||||
page.driver.browser.switch_to.alert.accept |
||||
|
||||
expect(page).to have_selector('.flash.notice', text: I18n.t(:notice_successful_delete)) |
||||
expect(page).to have_selector('.generic-table--empty-row') |
||||
end |
||||
|
||||
context 'with existing webhook' do |
||||
let!(:webhook) { FactoryBot.create :webhook, name: 'testing' } |
||||
let!(:log) { FactoryBot.create :webhook_log, response_headers: { test: :foo }, webhook: webhook } |
||||
|
||||
it 'shows the delivery' do |
||||
visit admin_outgoing_webhooks_path |
||||
find('.webhooks--outgoing-webhook-row .name a', text: 'testing').click |
||||
|
||||
expect(page).to have_selector('.on-off-status.-enabled') |
||||
expect(page).to have_selector('td.event_name', text: 'foo') |
||||
expect(page).to have_selector('td.response_code', text: '200') |
||||
|
||||
# Open modal |
||||
find('td.response_body a', text: 'Show').click |
||||
|
||||
page.within('.webhooks--response-body-modal') do |
||||
expect(page).to have_selector('.webhooks--response-headers strong', text: 'test') |
||||
expect(page).to have_selector('.webhooks--response-body', text: log.response_body) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,39 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require File.expand_path('../../spec_helper', __FILE__) |
||||
|
||||
|
||||
describe OpenProject::Webhooks::Hook do |
||||
describe '#relative_url' do |
||||
let(:hook) { OpenProject::Webhooks::Hook.new('myhook')} |
||||
|
||||
it "should return the correct URL" do |
||||
expect(hook.relative_url).to eql('webhooks/myhook') |
||||
end |
||||
end |
||||
|
||||
describe '#handle' do |
||||
let(:probe) { lambda{} } |
||||
let(:hook) { OpenProject::Webhooks::Hook.new('myhook', &probe) } |
||||
|
||||
before do |
||||
expect(probe).to receive(:call).with(hook, 1, 2, 3) |
||||
end |
||||
|
||||
it 'should execute the callback with the correct parameters' do |
||||
hook.handle(1, 2, 3) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,55 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require File.expand_path('../../spec_helper', __FILE__) |
||||
|
||||
|
||||
describe OpenProject::Webhooks do |
||||
describe '.register_hook' do |
||||
after do |
||||
OpenProject::Webhooks.unregister_hook('testhook1') |
||||
end |
||||
|
||||
it 'should succeed' do |
||||
OpenProject::Webhooks.register_hook('testhook1') {} |
||||
end |
||||
end |
||||
|
||||
describe '.find' do |
||||
let!(:hook) { OpenProject::Webhooks.register_hook('testhook3') {} } |
||||
|
||||
after do |
||||
OpenProject::Webhooks.unregister_hook('testhook3') |
||||
end |
||||
|
||||
it 'should succeed' do |
||||
expect(OpenProject::Webhooks.find('testhook3')).to equal(hook) |
||||
end |
||||
end |
||||
|
||||
describe '.unregister_hook' do |
||||
let(:probe) { lambda{} } |
||||
|
||||
before do |
||||
OpenProject::Webhooks.register_hook('testhook2', &probe) |
||||
|
||||
end |
||||
|
||||
it 'should result in the hook no longer being found' do |
||||
OpenProject::Webhooks.unregister_hook('testhook2') |
||||
expect(OpenProject::Webhooks.find('testhook2')).to be_nil |
||||
end |
||||
end |
||||
|
||||
end |
@ -0,0 +1,57 @@ |
||||
require 'spec_helper' |
||||
|
||||
describe ::Webhooks::Webhook, type: :model do |
||||
subject { FactoryBot.build :webhook } |
||||
|
||||
describe 'attributes' do |
||||
describe '#url' do |
||||
it 'accepts http' do |
||||
subject.url = 'http://foo.example.org' |
||||
expect(subject).to be_valid |
||||
end |
||||
|
||||
it 'accepts http' do |
||||
subject.url = 'https://foo.example.org' |
||||
expect(subject).to be_valid |
||||
end |
||||
|
||||
it 'accepts other schemas' do |
||||
subject.url = 'ftp://foo.example.org' |
||||
expect(subject).not_to be_valid |
||||
expect(subject.errors).to have_key(:url) |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#events' do |
||||
let(:events) { %w(work_package:updated work_package:created) } |
||||
before do |
||||
subject.event_names = events |
||||
subject.save! |
||||
end |
||||
|
||||
it 'has an event association' do |
||||
expect(subject.events.count).to eq 2 |
||||
expect(subject.event_names).to eq events |
||||
end |
||||
|
||||
it 'finds the webhook with the saved events' do |
||||
expect(described_class.with_event_name(events[0]).first).to eq(subject) |
||||
expect(described_class.with_event_name(events[1]).first).to eq(subject) |
||||
end |
||||
end |
||||
|
||||
describe '#projects' do |
||||
let(:project1) { FactoryBot.create :project } |
||||
|
||||
before do |
||||
subject.projects << project1 |
||||
subject.save! |
||||
end |
||||
|
||||
it 'has an event association' do |
||||
expect(subject.projects.count).to eq 1 |
||||
expect(subject.project_ids).to eq([project1.id]) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,63 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Outgoing webhooks administration', type: :routing do |
||||
it 'route to index' do |
||||
expect(get('/admin/webhooks')).to route_to('webhooks/outgoing/admin#index') |
||||
end |
||||
|
||||
it 'route to new' do |
||||
expect(get('/admin/webhooks/new')).to route_to('webhooks/outgoing/admin#new') |
||||
end |
||||
|
||||
it 'route to show' do |
||||
expect(get('/admin/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', |
||||
action: 'show', |
||||
webhook_id: '1') |
||||
end |
||||
|
||||
it 'route to edit' do |
||||
expect(get('/admin/webhooks/1/edit')).to route_to(controller: 'webhooks/outgoing/admin', |
||||
action: 'edit', |
||||
webhook_id: '1') |
||||
end |
||||
|
||||
it 'route to PUT update' do |
||||
expect(put('/admin/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', |
||||
action: 'update', |
||||
webhook_id: '1') |
||||
end |
||||
|
||||
it 'route to DELETE destroy' do |
||||
expect(delete('/admin/webhooks/1')).to route_to(controller: 'webhooks/outgoing/admin', |
||||
action: 'destroy', |
||||
webhook_id: '1') |
||||
end |
||||
end |
@ -0,0 +1,15 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.md for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
@ -0,0 +1,125 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# 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. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe WorkPackageWebhookJob, type: :model, webmock: true do |
||||
shared_examples "a work package webhook call" do |*flags| |
||||
let(:title) { "Some workpackage subject" } |
||||
let(:work_package) { FactoryBot.create :work_package, subject: title } |
||||
|
||||
let(:secret) { nil } |
||||
let(:webhook) { FactoryBot.create :webhook, url: request_url, secret: secret } |
||||
|
||||
let(:user) { FactoryBot.create :admin } |
||||
|
||||
let(:event) { "work_package:created" } |
||||
let(:job) { WorkPackageWebhookJob.new webhook.id, work_package.journals.last.id, event } |
||||
|
||||
let(:request_url) { "http://example.net/test/42" } |
||||
let(:stubbed_url) { request_url } |
||||
|
||||
let(:request_headers) do |
||||
{ content_type: "application/json", accept: "application/json" } |
||||
end |
||||
|
||||
let(:response_code) { 200 } |
||||
let(:response_body) { "hook called" } |
||||
let(:response_headers) do |
||||
{ content_type: "text/plain", x_spec: "foobar" } |
||||
end |
||||
|
||||
let(:stub) do |
||||
stub_request(:post, stubbed_url.sub("http://", "")) |
||||
.with( |
||||
body: hash_including( |
||||
"action" => event, |
||||
"work_package" => hash_including( |
||||
"_type" => "WorkPackage", |
||||
"subject" => title |
||||
) |
||||
), |
||||
headers: request_headers |
||||
) |
||||
.to_return( |
||||
status: response_code, |
||||
body: response_body, |
||||
headers: response_headers |
||||
) |
||||
end |
||||
|
||||
before do |
||||
User.current = user |
||||
|
||||
stub |
||||
|
||||
begin |
||||
job.perform |
||||
rescue |
||||
# ignoring it as it's expected to throw exceptions in certain scenarios |
||||
end |
||||
end |
||||
|
||||
it "calls the webhook url" do |
||||
expect(stub).to have_been_requested |
||||
end |
||||
|
||||
it "creates a log for the call" do |
||||
log = Webhooks::Log.last |
||||
|
||||
expect(log.webhook).to eq webhook |
||||
expect(log.url).to eq webhook.url |
||||
expect(log.event_name).to eq event |
||||
expect(log.request_headers).to eq request_headers |
||||
expect(log.response_code).to eq response_code |
||||
expect(log.response_body).to eq response_body |
||||
expect(log.response_headers).to eq response_headers |
||||
end |
||||
end |
||||
|
||||
describe "triggering a work package update" do |
||||
it_behaves_like "a work package webhook call" do |
||||
let(:event) { "work_package:updated" } |
||||
end |
||||
end |
||||
|
||||
describe "triggering a work package creation" do |
||||
it_behaves_like "a work package webhook call" do |
||||
let(:event) { "work_package:created" } |
||||
end |
||||
end |
||||
|
||||
describe "triggering a work package update with an invalid url" do |
||||
it_behaves_like "a work package webhook call" do |
||||
let(:event) { "work_package:updated" } |
||||
let(:response_code) { 404 } |
||||
let(:response_body) { "not found" } |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue