Added the webhooks.
This commit is contained in:
parent
2f22d48dd0
commit
47acc73451
69
scripts/queuecron.php
Normal file
69
scripts/queuecron.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/*
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
# Copyright (C) 2008 Céondo Ltd and contributors.
|
||||
#
|
||||
# InDefero 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.
|
||||
#
|
||||
# InDefero 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 St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK ***** */
|
||||
|
||||
/**
|
||||
* This script process the queue of items.
|
||||
*
|
||||
* At the moment the queue is only used for the webhooks, but it would
|
||||
* be good in the future to use it for indexing and email
|
||||
* notifications.
|
||||
*
|
||||
*/
|
||||
|
||||
require dirname(__FILE__).'/../src/IDF/conf/path.php';
|
||||
require 'Pluf.php';
|
||||
Pluf::start(dirname(__FILE__).'/../src/IDF/conf/idf.php');
|
||||
Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
|
||||
|
||||
#;*/ ::
|
||||
$lock_file = Pluf::f('idf_queuecron_lock',
|
||||
Pluf::f('tmp_folder', '/tmp').'/queuecron.lock');
|
||||
if (file_exists($lock_file)) {
|
||||
Pluf_Log::event(array('queuecron.php', 'skip'));
|
||||
return;
|
||||
}
|
||||
file_put_contents($lock_file, time(), LOCK_EX);
|
||||
|
||||
/**
|
||||
* [signal]
|
||||
*
|
||||
* queuecron.php::run
|
||||
*
|
||||
* [sender]
|
||||
*
|
||||
* queuecron.php
|
||||
*
|
||||
* [description]
|
||||
*
|
||||
* This signal allows an application to perform a set of tasks when
|
||||
* the queue cron job is run. This is done usually every 5 minutes.
|
||||
*
|
||||
* [parameters]
|
||||
*
|
||||
* array()
|
||||
*
|
||||
*/
|
||||
$params = array();
|
||||
Pluf_Signal::send('queuecron.php::run', 'queuecron.php', $params);
|
||||
|
||||
unlink($lock_file);
|
@ -288,5 +288,31 @@ class IDF_Commit extends Pluf_Model
|
||||
$email->addTextMessage($text_email);
|
||||
$email->sendMail();
|
||||
Pluf_Translation::loadSetLocale($current_locale);
|
||||
|
||||
// Now we add to the queue, soon we will push everything in
|
||||
// the queue, including email notifications and indexing.
|
||||
// Even if the url is empty, we add to the queue as some
|
||||
// plugins may want to do something with this information in
|
||||
// an asynchronous way.
|
||||
|
||||
$url = str_replace(array('%p', '%r'),
|
||||
array($project->shortname, $this->scm_id),
|
||||
$conf->getVal('webhook_url', ''));
|
||||
$payload = array('to_send' => array(
|
||||
'project' => $project->shortname,
|
||||
'rev' => $this->scm_id,
|
||||
'summary' => $this->summary,
|
||||
'fullmessage' => $this->fullmessage,
|
||||
'author' => $this->origauthor,
|
||||
'creation_date' => $this->creation_dtime,
|
||||
),
|
||||
'project_id' => $project->id,
|
||||
'authkey' => $project->getPostCommitHookKey(),
|
||||
'url' => $url,
|
||||
);
|
||||
$item = new IDF_Queue();
|
||||
$item->type = 'new_commit';
|
||||
$item->payload = $payload;
|
||||
$item->create();
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class IDF_Form_SourceConf extends Pluf_Form
|
||||
public function initFields($extra=array())
|
||||
{
|
||||
$this->conf = $extra['conf'];
|
||||
if ($this->conf->getVal('scm', 'git') == 'svn') {
|
||||
if ($extra['remote_svn']) {
|
||||
$this->fields['svn_username'] = new Pluf_Form_Field_Varchar(
|
||||
array('required' => false,
|
||||
'label' => __('Repository username'),
|
||||
@ -49,6 +49,16 @@ class IDF_Form_SourceConf extends Pluf_Form
|
||||
'widget' => 'Pluf_Form_Widget_PasswordInput',
|
||||
));
|
||||
}
|
||||
Pluf::loadFunction('Pluf_HTTP_URL_urlForView');
|
||||
$url = Pluf_HTTP_URL_urlForView('idf_faq').'#webhooks';
|
||||
$this->fields['webhook_url'] = new Pluf_Form_Field_Url(
|
||||
array('required' => false,
|
||||
'label' => __('Webhook URL'),
|
||||
'initial' => $this->conf->getVal('webhook_url', ''),
|
||||
'help_text' => sprintf(__('Learn more about the <a href="%s">post-commit web hooks</a>.'), $url),
|
||||
'widget_attrs' => array('size' => 35),
|
||||
));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -424,6 +424,17 @@ class IDF_Project extends Pluf_Model
|
||||
$this, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post commit hook key.
|
||||
*
|
||||
* The goal is to get something predictable but from which one
|
||||
* cannot reverse find the secret key.
|
||||
*/
|
||||
public function getPostCommitHookKey()
|
||||
{
|
||||
return md5($this->id.sha1(Pluf::f('secret_key')).$this->shortname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root name of the project scm
|
||||
*
|
||||
|
@ -138,6 +138,8 @@ class IDF_Queue extends Pluf_Model
|
||||
$this->creation_dtime = gmdate('Y-m-d H:i:s');
|
||||
$this->lasttry_dtime = gmdate('Y-m-d H:i:s');
|
||||
$this->results = array();
|
||||
$this->trials = 0;
|
||||
$this->status = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,4 +185,38 @@ class IDF_Queue extends Pluf_Model
|
||||
$this->lasttry_dtime = gmdate('Y-m-d H:i:s');
|
||||
$this->update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the queue.
|
||||
*
|
||||
* It is a signal handler to just hook itself at the right time in
|
||||
* the cron job performing the maintainance work.
|
||||
*
|
||||
* The processing relies on the fact that no other processing jobs
|
||||
* must run at the same time. That is, your cron job must use a
|
||||
* lock file or something like to not run in parallel.
|
||||
*
|
||||
* The processing is simple, first get 500 queue items, mark them
|
||||
* as being processed and for each of them call the processItem()
|
||||
* method which will trigger another event for processing.
|
||||
*
|
||||
* If you are processing more than 500 items per batch, you need
|
||||
* to switch to a different solution.
|
||||
*
|
||||
*/
|
||||
public static function process($sender, &$params)
|
||||
{
|
||||
$where = 'status=0 OR status=2';
|
||||
$items = Pluf::factory('IDF_Queue')->getList(array('filter'=>$where,
|
||||
'nb'=> 500));
|
||||
Pluf_Log::event(array('IDF_Queue::process', $items->count()));
|
||||
foreach ($items as $item) {
|
||||
$item->status = 1;
|
||||
$item->update();
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
$item->status = 1;
|
||||
$item->processItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -475,19 +475,21 @@ class IDF_Views_Project
|
||||
|
||||
/**
|
||||
* Administrate the source control.
|
||||
*
|
||||
* There, the login/password of the subversion remote repo can be
|
||||
* change together with the webhook url.
|
||||
*/
|
||||
public $adminSource_precond = array('IDF_Precondition::projectOwner');
|
||||
public function adminSource($request, $match)
|
||||
{
|
||||
$prj = $request->project;
|
||||
$title = sprintf(__('%s Source'), (string) $prj);
|
||||
$form = null;
|
||||
$remote_svn = false;
|
||||
if ($request->conf->getVal('scm') == 'svn' and
|
||||
strlen($request->conf->getVal('svn_remote_url')) > 0) {
|
||||
$remote_svn = true;
|
||||
|
||||
$remote_svn = ($request->conf->getVal('scm') == 'svn' and
|
||||
strlen($request->conf->getVal('svn_remote_url')) > 0);
|
||||
$extra = array(
|
||||
'conf' => $request->conf,
|
||||
'remote_svn' => $remote_svn,
|
||||
);
|
||||
if ($request->method == 'POST') {
|
||||
$form = new IDF_Form_SourceConf($request->POST, $extra);
|
||||
@ -502,7 +504,7 @@ class IDF_Views_Project
|
||||
}
|
||||
} else {
|
||||
$params = array();
|
||||
foreach (array('svn_username', 'svn_password') as $key) {
|
||||
foreach (array('svn_username', 'svn_password', 'webhook_url') as $key) {
|
||||
$_val = $request->conf->getVal($key, false);
|
||||
if ($_val !== false) {
|
||||
$params[$key] = $_val;
|
||||
@ -513,7 +515,6 @@ class IDF_Views_Project
|
||||
}
|
||||
$form = new IDF_Form_SourceConf($params, $extra);
|
||||
}
|
||||
}
|
||||
$scm = $request->conf->getVal('scm', 'git');
|
||||
$options = array(
|
||||
'git' => __('git'),
|
||||
@ -529,6 +530,7 @@ class IDF_Views_Project
|
||||
'repository_size' => $prj->getRepositorySize(),
|
||||
'page_title' => $title,
|
||||
'form' => $form,
|
||||
'hookkey' => $prj->getPostCommitHookKey(),
|
||||
),
|
||||
$request);
|
||||
}
|
||||
|
100
src/IDF/Webhook.php
Normal file
100
src/IDF/Webhook.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/*
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
# Copyright (C) 2008, 2009, 2010 Céondo Ltd and contributors.
|
||||
#
|
||||
# InDefero 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.
|
||||
#
|
||||
# InDefero 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 St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK ***** */
|
||||
|
||||
/**
|
||||
* Management of the webhooks.
|
||||
*
|
||||
* The class provides the tools to perform the POST request with
|
||||
* authentication for the webhooks.
|
||||
*
|
||||
*/
|
||||
class IDF_Webhook
|
||||
{
|
||||
/**
|
||||
* Perform the POST request given the webhook payload.
|
||||
*
|
||||
* @param array Payload
|
||||
* @return bool Success or error
|
||||
*/
|
||||
public static function postNotification($payload)
|
||||
{
|
||||
$data = json_encode($payload['to_send']);
|
||||
$sign = hash_hmac('md5', $data, $payload['authkey']);
|
||||
$params = array('http' => array(
|
||||
'method' => 'POST',
|
||||
'content' => $data,
|
||||
'user_agent' => 'Indefero Hook Sender (http://www.indefero.net)',
|
||||
'max_redirects' => 0,
|
||||
'timeout' => 15,
|
||||
'header'=> 'Indefero-Hook-Hmac: '.$sign."\r\n"
|
||||
.'Content-Type: application/json'."\r\n",
|
||||
)
|
||||
);
|
||||
$url = $payload['url'];
|
||||
$ctx = stream_context_create($params);
|
||||
$fp = @fopen($url, 'rb', false, $ctx);
|
||||
if (!$fp) {
|
||||
return false;
|
||||
}
|
||||
$meta = stream_get_meta_data($fp);
|
||||
@fclose($fp);
|
||||
if (!isset($meta['wrapper_data'][0]) or $meta['timed_out']) {
|
||||
return false;
|
||||
}
|
||||
if (0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 2') or
|
||||
0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 3')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process the webhook.
|
||||
*
|
||||
*/
|
||||
public static function process($sender, &$params)
|
||||
{
|
||||
$item = $params['item'];
|
||||
if ($item->type != 'new_commit') {
|
||||
// We do nothing.
|
||||
return;
|
||||
}
|
||||
if (isset($params['res']['IDF_Webhook::process']) and
|
||||
$params['res']['IDF_Webhook::process'] == true) {
|
||||
// Already processed.
|
||||
return;
|
||||
}
|
||||
// We have either to retry or to push for the first time.
|
||||
$res = self::postNotification($item->payload);
|
||||
if ($res) {
|
||||
$params['res']['IDF_Webhook::process'] = true;
|
||||
} elseif ($item->trials >= 9) {
|
||||
// We are at trial 10, give up
|
||||
$params['res']['IDF_Webhook::process'] = true;
|
||||
} else {
|
||||
// Need to try again
|
||||
$params['res']['IDF_Webhook::process'] = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -66,7 +66,8 @@ $ctl[] = array('regex' => '#^/logout/$#',
|
||||
$ctl[] = array('regex' => '#^/help/$#',
|
||||
'base' => $base,
|
||||
'model' => 'IDF_Views',
|
||||
'method' => 'faq');
|
||||
'method' => 'faq',
|
||||
'name' => 'idf_faq');
|
||||
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/$#',
|
||||
'base' => $base,
|
||||
|
@ -84,5 +84,16 @@ Pluf_Signal::connect('IDF_Key::preDelete',
|
||||
Pluf_Signal::connect('gitpostupdate.php::run',
|
||||
array('IDF_Plugin_SyncGit', 'entry'));
|
||||
|
||||
#
|
||||
# -- Processing of the webhook queue --
|
||||
Pluf_Signal::connect('queuecron.php::run',
|
||||
array('IDF_Queue', 'process'));
|
||||
|
||||
#
|
||||
# Processing of a given webhook, the hook can be configured
|
||||
# directly in the configuration file if a different solution
|
||||
# is required.
|
||||
Pluf_Signal::connect('IDF_Queue::processItem',
|
||||
Pluf::f('idf_hook_process_item',
|
||||
array('IDF_Webhook', 'process')));
|
||||
return $m;
|
||||
|
@ -1,7 +1,7 @@
|
||||
{extends "idf/admin/base.html"}
|
||||
{block docclass}yui-t1{assign $inSource = true}{/block}
|
||||
{block docclass}yui-t3{assign $inSource = true}{/block}
|
||||
{block body}
|
||||
{if $remote_svn and $form.errors}
|
||||
{if $form.errors}
|
||||
<div class="px-message-error">
|
||||
<p>{trans 'The form contains some errors. Please correct them to update the source configuration.'}</p>
|
||||
{if $form.get_top_errors}
|
||||
@ -37,13 +37,25 @@
|
||||
<td>{if $form.f.svn_password.errors}{$form.f.svn_password.fieldErrors}{/if}
|
||||
{$form.f.svn_password|unsafe}
|
||||
</td>
|
||||
</tr>{/if}
|
||||
<tr>
|
||||
<th>{$form.f.webhook_url.labelTag}:</th>
|
||||
<td>{if $form.f.webhook_url.errors}{$form.f.webhook_url.fieldErrors}{/if}
|
||||
{$form.f.webhook_url|unsafe}<br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{trans 'Post-commit authentication key:'}</th>
|
||||
<td>{$hookkey}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input type="submit" value="{trans 'Save Changes'}" name="submit" />
|
||||
</td>
|
||||
</tr>{/if}
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
{/block}
|
||||
@ -51,4 +63,31 @@
|
||||
<div class="issue-submit-info">
|
||||
<p>{blocktrans}You can find here the current repository configuration of your project.{/blocktrans}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<br>
|
||||
<div class="issue-submit-info">
|
||||
|
||||
{blocktrans}<p>The webhook URL setting specifies a URL to which a HTTP POST
|
||||
request is sent after each repository commit. If this field is empty,
|
||||
notifications are disabled.</p>
|
||||
|
||||
<p>Only properly-escaped <strong>HTTP</strong> URLs are supported, for example:</p>
|
||||
|
||||
<ul>
|
||||
<li>http://domain.com/commit</li>
|
||||
<li>http://domain.com/commit?my%20param</li>
|
||||
</ul>
|
||||
|
||||
<p>In addition, the URL may contain the following "%" notation, which
|
||||
will be replaced with specific project values for each commit:</p>
|
||||
|
||||
<ul>
|
||||
<li>%p - project name</li>
|
||||
<li>%r - revision number</li>
|
||||
</ul>
|
||||
|
||||
<p>For example, committing revision 123 to project 'my-project' with
|
||||
post-commit URL http://mydomain.com/%p/%r would send a request to
|
||||
http://mydomain.com/my-project/123.</p>{/blocktrans}</div>
|
||||
{/block}
|
||||
|
Loading…
Reference in New Issue
Block a user