Initial commit
This commit is contained in:
81
indefero/src/IDF/Plugin/SyncGit.php
Normal file
81
indefero/src/IDF/Plugin/SyncGit.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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-2011 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 class is a plugin which allows to synchronise access riths
|
||||
* between InDefero and a common restricted SSH account for git
|
||||
* access.
|
||||
*
|
||||
* As the authentication is directly performed by accessing the
|
||||
* InDefero database, we only need to synchronize the SSH keys. This
|
||||
* synchronization process can only be performed by a process running
|
||||
* under the git user as we need to write in
|
||||
* /home/git/.ssh/authorized_keys
|
||||
*
|
||||
* So, here, we are just creating a file informing that a sync needs
|
||||
* to be done. We connect this plugin to the IDF_Key::postSave signal.
|
||||
*/
|
||||
class IDF_Plugin_SyncGit
|
||||
{
|
||||
/**
|
||||
* Entry point of the plugin.
|
||||
*/
|
||||
static public function entry($signal, &$params)
|
||||
{
|
||||
// First check for the single mandatory config variable.
|
||||
if (!Pluf::f('idf_plugin_syncgit_sync_file', false)) {
|
||||
Pluf_Log::debug('IDF_Plugin_SyncGit plugin not configured.');
|
||||
return;
|
||||
}
|
||||
if ($signal != 'gitpostupdate.php::run') {
|
||||
Pluf_Log::event('IDF_Plugin_SyncGit', 'create',
|
||||
Pluf::f('idf_plugin_syncgit_sync_file'));
|
||||
@touch(Pluf::f('idf_plugin_syncgit_sync_file'));
|
||||
@chmod(Pluf::f('idf_plugin_syncgit_sync_file'), 0777);
|
||||
} else {
|
||||
self::postUpdate($signal, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for the post-update signal.
|
||||
*
|
||||
* It tries to find the name of the project, when found it runs an
|
||||
* update of the timeline.
|
||||
*/
|
||||
static public function postUpdate($signal, &$params)
|
||||
{
|
||||
// Chop the ".git" and get what is left
|
||||
$pname = basename($params['git_dir'], '.git');
|
||||
try {
|
||||
$project = IDF_Project::getOr404($pname);
|
||||
} catch (Pluf_HTTP_Error404 $e) {
|
||||
Pluf_Log::event(array('IDF_Plugin_SyncGit::postUpdate', 'Project not found.', array($pname, $params)));
|
||||
return false; // Project not found
|
||||
}
|
||||
// Now we have the project and can update the timeline
|
||||
Pluf_Log::debug(array('IDF_Plugin_SyncGit::postUpdate', 'Project found', $pname, $project->id));
|
||||
IDF_Scm::syncTimeline($project, true);
|
||||
Pluf_Log::event(array('IDF_Plugin_SyncGit::postUpdate', 'sync', array($pname, $project->id)));
|
||||
}
|
||||
}
|
141
indefero/src/IDF/Plugin/SyncGit/Cron.php
Normal file
141
indefero/src/IDF/Plugin/SyncGit/Cron.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?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-2011 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 ***** */
|
||||
|
||||
/**
|
||||
* Synchronize the SSH keys with InDefero.
|
||||
*/
|
||||
class IDF_Plugin_SyncGit_Cron
|
||||
{
|
||||
/**
|
||||
* Template for the SSH key.
|
||||
*/
|
||||
public $template = 'command="python %s %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s';
|
||||
|
||||
/**
|
||||
* Synchronize.
|
||||
*/
|
||||
public static function sync()
|
||||
{
|
||||
$template = Pluf::factory(__CLASS__)->template;
|
||||
$cmd = Pluf::f('idf_plugin_syncgit_path_gitserve', '/dev/null');
|
||||
$authorized_keys = Pluf::f('idf_plugin_syncgit_path_authorized_keys', false);
|
||||
if (false == $authorized_keys) {
|
||||
throw new Pluf_Exception_SettingError('Setting idf_plugin_syncgit_path_authorized_keys not set.');
|
||||
}
|
||||
if (!is_writable($authorized_keys)) {
|
||||
throw new Exception('Cannot create file: '.$authorized_keys);
|
||||
}
|
||||
$out = '';
|
||||
$keys = Pluf::factory('IDF_Key')->getList(array('view'=>'join_user'));
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$key_type = $key->getType();
|
||||
} catch (Exception $e) {
|
||||
// The key is a bad key, skip it
|
||||
continue;
|
||||
}
|
||||
if ($key_type == 'ssh' and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) {
|
||||
$content = trim(str_replace(array("\n", "\r"), '', $key->content));
|
||||
$out .= sprintf($template, $cmd, $key->login, $content)."\n";
|
||||
}
|
||||
}
|
||||
$out = "# indefero start" . PHP_EOL . $out . "# indefero end" . PHP_EOL;
|
||||
|
||||
// We update only the part of the file between IDF_START / IDF_END comment
|
||||
$original_keys = file_get_contents($authorized_keys);
|
||||
if (strstr($original_keys, "# indefero start") && strstr($original_keys, "# indefero end")) {
|
||||
$out = preg_replace('%(#\sindefero\sstart).+(#\sindefero\send\s\s?)%isU',
|
||||
$out, $original_keys);
|
||||
} else {
|
||||
$out .= $original_keys;
|
||||
}
|
||||
file_put_contents($authorized_keys, $out, LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark export of git repositories for the daemon.
|
||||
*/
|
||||
public static function markExport()
|
||||
{
|
||||
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||
$rep = sprintf(Pluf::f('git_repositories'), $project->shortname);
|
||||
$serve = new IDF_Plugin_SyncGit_Serve();
|
||||
$serve->setGitExport($project->shortname, $rep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove orphan repositories.
|
||||
*/
|
||||
public static function removeOrphanRepositories()
|
||||
{
|
||||
$path = Pluf::f('idf_plugin_syncgit_base_repositories', '/home/git/repositories');
|
||||
if (!is_dir($path) || is_link($path)) {
|
||||
throw new Pluf_Exception_SettingError(sprintf(
|
||||
'Directory %s does not exist! Setting "idf_plugin_syncgit_base_repositories not set.',
|
||||
$path));
|
||||
}
|
||||
if (!is_writable($path)) {
|
||||
throw new Exception(sprintf('Repository %s is not writable.', $path));
|
||||
}
|
||||
$projects = array();
|
||||
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||
$projects[] = $project->shortname;
|
||||
}
|
||||
unset($project);
|
||||
$it = new DirectoryIterator($path);
|
||||
$orphans = array();
|
||||
while ($it->valid()) {
|
||||
if (!$it->isDot() && $it->isDir() && !in_array(basename($it->getFileName(), '.git'), $projects)) {
|
||||
$orphans[] = $it->getPathName();
|
||||
}
|
||||
$it->next();
|
||||
}
|
||||
if (count($orphans)) {
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'rm -rf '.implode(' ', $orphans);
|
||||
exec($cmd);
|
||||
clearstatcache();
|
||||
while (list(, $project) = each($orphans)) {
|
||||
if (is_dir($project)) {
|
||||
throw new Exception(sprintf('Cannot remove %s directory.', $project));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a sync is needed.
|
||||
*
|
||||
*/
|
||||
public static function main()
|
||||
{
|
||||
if (file_exists(Pluf::f('idf_plugin_syncgit_sync_file'))) {
|
||||
@unlink(Pluf::f('idf_plugin_syncgit_sync_file'));
|
||||
self::sync();
|
||||
self::markExport();
|
||||
if (Pluf::f('idf_plugin_syncgit_remove_orphans', false)) {
|
||||
self::removeOrphanRepositories();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
317
indefero/src/IDF/Plugin/SyncGit/Serve.php
Normal file
317
indefero/src/IDF/Plugin/SyncGit/Serve.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?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-2011 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 ***** */
|
||||
|
||||
/**
|
||||
* Main application to serve git repositories through a restricted SSH
|
||||
* access.
|
||||
*/
|
||||
class IDF_Plugin_SyncGit_Serve
|
||||
{
|
||||
/**
|
||||
* Regular expression to match the path in the git command.
|
||||
*/
|
||||
public $preg = '#^\'/*(?P<path>[a-zA-Z0-9][a-zA-Z0-9@._-]*(/[a-zA-Z0-9][a-zA-Z0-9@._-]*)*)\'$#';
|
||||
|
||||
public $commands_readonly = array('git-upload-pack', 'git upload-pack');
|
||||
public $commands_write = array('git-receive-pack', 'git receive-pack');
|
||||
|
||||
/**
|
||||
* Serve a git request.
|
||||
*
|
||||
* @param string Username.
|
||||
* @param string Command to be run.
|
||||
*/
|
||||
public function serve($username, $cmd)
|
||||
{
|
||||
if (false !== strpos($cmd, "\n")) {
|
||||
throw new Exception('Command may not contain newline.');
|
||||
}
|
||||
$splitted = preg_split('/\s/', $cmd, 2);
|
||||
if (count($splitted) != 2) {
|
||||
throw new Exception('Unknown command denied.');
|
||||
}
|
||||
if ($splitted[0] == 'git') {
|
||||
$sub_splitted = preg_split('/\s/', $splitted[1], 2);
|
||||
if (count($sub_splitted) != 2) {
|
||||
throw new Exception('Unknown command denied.');
|
||||
}
|
||||
$verb = sprintf('%s %s', $splitted[0], $sub_splitted[0]);
|
||||
$args = $sub_splitted[1];
|
||||
} else {
|
||||
$verb = $splitted[0];
|
||||
$args = $splitted[1];
|
||||
}
|
||||
if (!in_array($verb, $this->commands_write)
|
||||
and !in_array($verb, $this->commands_readonly)) {
|
||||
throw new Exception('Unknown command denied.');
|
||||
}
|
||||
if (!preg_match($this->preg, $args, $matches)) {
|
||||
throw new Exception('Arguments to command look dangerous.');
|
||||
}
|
||||
$path = $matches['path'];
|
||||
// Check read/write rights
|
||||
$new_path = $this->haveAccess($username, $path, 'writable');
|
||||
if ($new_path == false) {
|
||||
$new_path = $this->haveAccess($username, $path, 'readonly');
|
||||
if ($new_path == false) {
|
||||
throw new Exception('Repository read access denied.');
|
||||
}
|
||||
if (in_array($verb, $this->commands_write)) {
|
||||
throw new Exception('Repository write access denied.');
|
||||
}
|
||||
}
|
||||
list($topdir, $relpath) = $new_path;
|
||||
$repopath = sprintf('%s.git', $relpath);
|
||||
$fullpath = $topdir.DIRECTORY_SEPARATOR.$repopath;
|
||||
if (!file_exists($fullpath)
|
||||
and in_array($verb, $this->commands_write)) {
|
||||
// it doesn't exist on the filesystem, but the
|
||||
// configuration refers to it, we're serving a write
|
||||
// request, and the user is authorized to do that: create
|
||||
// the repository on the fly
|
||||
$p = explode(DIRECTORY_SEPARATOR, $fullpath);
|
||||
$mpath = implode(DIRECTORY_SEPARATOR, array_slice($p, 0, -1));
|
||||
if (!file_exists($mpath)) {
|
||||
mkdir($mpath, 0750, true);
|
||||
}
|
||||
$this->initRepository($fullpath);
|
||||
$this->setGitExport($relpath, $fullpath);
|
||||
}
|
||||
$new_cmd = sprintf("%s '%s'", $verb, $fullpath);
|
||||
Pluf_Log::info(array('IDF_Plugin_Git_Serve::serve', $username, $cmd, $new_cmd));
|
||||
return $new_cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function called by the serve script.
|
||||
*/
|
||||
public static function main($argv, $env)
|
||||
{
|
||||
if (count($argv) != 2) {
|
||||
self::fatalError('Missing argument USER.');
|
||||
}
|
||||
$username = $argv[1];
|
||||
umask(0022);
|
||||
if (!isset($env['SSH_ORIGINAL_COMMAND'])) {
|
||||
self::fatalError('Need SSH_ORIGINAL_COMMAND in environment.');
|
||||
}
|
||||
$cmd = $env['SSH_ORIGINAL_COMMAND'];
|
||||
chdir(Pluf::f('idf_plugin_syncgit_git_home_dir', '/home/git'));
|
||||
$serve = new IDF_Plugin_SyncGit_Serve();
|
||||
try {
|
||||
$new_cmd = $serve->serve($username, $cmd);
|
||||
} catch (Exception $e) {
|
||||
self::fatalError($e->getMessage());
|
||||
}
|
||||
print $new_cmd;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the access rights to the repository.
|
||||
*
|
||||
* @param string Username
|
||||
* @param string Path including the possible .git
|
||||
* @param string Type of access. 'readonly' or ('writable')
|
||||
* @return mixed False or array(base_git_reps, relative path to repo)
|
||||
*/
|
||||
public function haveAccess($username, $path, $mode='writable')
|
||||
{
|
||||
if ('.git' == substr($path, -4)) {
|
||||
$path = substr($path, 0, -4);
|
||||
}
|
||||
$sql = new Pluf_SQL('shortname=%s', array($path));
|
||||
$projects = Pluf::factory('IDF_Project')->getList(array('filter'=>$sql->gen()));
|
||||
if ($projects->count() != 1) {
|
||||
return false;
|
||||
}
|
||||
$project = $projects[0];
|
||||
$conf = new IDF_Conf();
|
||||
$conf->setProject($project);
|
||||
$scm = $conf->getVal('scm', 'git');
|
||||
if ($scm != 'git') {
|
||||
return false;
|
||||
}
|
||||
$sql = new Pluf_SQL('login=%s', array($username));
|
||||
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
|
||||
if ($users->count() != 1 or !$users[0]->active) {
|
||||
return false;
|
||||
}
|
||||
$user = $users[0];
|
||||
$request = new StdClass();
|
||||
$request->user = $user;
|
||||
$request->conf = $conf;
|
||||
$request->project = $project;
|
||||
if (true === IDF_Precondition::accessSource($request)) {
|
||||
if ($mode == 'readonly') {
|
||||
return array(Pluf::f('idf_plugin_syncgit_base_repositories', '/home/git/repositories'),
|
||||
$project->shortname);
|
||||
}
|
||||
if (true === IDF_Precondition::projectMemberOrOwner($request)) {
|
||||
return array(Pluf::f('idf_plugin_syncgit_base_repositories', '/home/git/repositories'),
|
||||
$project->shortname);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die on a message on stderr.
|
||||
*
|
||||
* @param string Message
|
||||
*/
|
||||
public static function fatalError($mess)
|
||||
{
|
||||
fwrite(STDERR, $mess."\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init a new empty bare repository.
|
||||
*
|
||||
* @param string Full path to the repository
|
||||
*/
|
||||
public function initRepository($fullpath)
|
||||
{
|
||||
if (!file_exists($fullpath)) {
|
||||
mkdir($fullpath, 0750, true);
|
||||
}
|
||||
$out = array();
|
||||
$res = 0;
|
||||
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').
|
||||
Pluf::f('git_path', 'git').' --git-dir=%s init', escapeshellarg($fullpath)),
|
||||
$out, $res);
|
||||
if ($res != 0) {
|
||||
Pluf_Log::error(array('IDF_Plugin_Git_Serve::initRepository', $res, $fullpath));
|
||||
throw new Exception(sprintf('Init repository error, exit status %d.', $res));
|
||||
}
|
||||
Pluf_Log::event(array('IDF_Plugin_Git_Serve::initRepository', 'success', $fullpath));
|
||||
// Add the post-update hook by removing the original one and add the
|
||||
// Indefero's one.
|
||||
$p = realpath(dirname(__FILE__).'/../../../../scripts/git-post-update');
|
||||
$p = Pluf::f('idf_plugin_syncgit_post_update', $p);
|
||||
$post_update_hook = $fullpath.'/hooks/post-update';
|
||||
if (file_exists($post_update_hook) && !@unlink($post_update_hook)) {
|
||||
Pluf_Log::warn(array('IDF_Plugin_Git_Serve::initRepository',
|
||||
'post-update hook removal error.',
|
||||
$post_update_hook));
|
||||
return;
|
||||
}
|
||||
$out = array();
|
||||
$res = 0;
|
||||
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').'ln -s %s %s',
|
||||
escapeshellarg($p),
|
||||
escapeshellarg($post_update_hook)),
|
||||
$out, $res);
|
||||
if ($res != 0) {
|
||||
Pluf_Log::warn(array('IDF_Plugin_Git_Serve::initRepository',
|
||||
'post-update hook creation error.',
|
||||
$post_update_hook));
|
||||
return;
|
||||
}
|
||||
Pluf_Log::debug(array('IDF_Plugin_Git_Serve::initRepository',
|
||||
'Added post-update hook.', $fullpath));
|
||||
// Configure the core.quotepath option
|
||||
$quotepath = (Pluf::f('git_core_quotepath', true) == true) ? 'true' : 'false';
|
||||
$out = array();
|
||||
$res = 0;
|
||||
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').
|
||||
Pluf::f('git_path', 'git').' config -f %s/config --add core.quotepath %s',
|
||||
escapeshellarg($fullpath),
|
||||
escapeshellarg($quotepath)
|
||||
),
|
||||
$out, $res);
|
||||
if ($res != 0) {
|
||||
Pluf_Log::warn(array('IDF_Plugin_Git_Serve::initRepository',
|
||||
'core.quotepath configuration error.',
|
||||
$quotepath));
|
||||
return;
|
||||
}
|
||||
Pluf_Log::debug(array('IDF_Plugin_Git_Serve::initRepository',
|
||||
'core.quotepath configured.', $quotepath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the git export value.
|
||||
*
|
||||
* @param string Relative path of the repository (not .git)
|
||||
* @param string Full path of the repository with .git
|
||||
*/
|
||||
public function setGitExport($relpath, $fullpath)
|
||||
{
|
||||
$sql = new Pluf_SQL('shortname=%s', array($relpath));
|
||||
$projects = Pluf::factory('IDF_Project')->getList(array('filter'=>$sql->gen()));
|
||||
if ($projects->count() != 1 and file_exists($fullpath)) {
|
||||
return $this->gitExportDeny($fullpath);
|
||||
}
|
||||
$project = $projects[0];
|
||||
$conf = new IDF_Conf();
|
||||
$conf->setProject($project);
|
||||
$scm = $conf->getVal('scm', 'git');
|
||||
if ($scm == 'git' and !file_exists($fullpath)) {
|
||||
// No repository yet, just skip
|
||||
return false;
|
||||
}
|
||||
if ($scm != 'git' or $project->private) {
|
||||
return $this->gitExportDeny($fullpath);
|
||||
}
|
||||
if ('all' == $conf->getVal('source_access_rights', 'all')) {
|
||||
return $this->gitExportAllow($fullpath);
|
||||
}
|
||||
return $this->gitExportDeny($fullpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the export flag.
|
||||
*
|
||||
* @param string Full path to the repository
|
||||
*/
|
||||
public function gitExportDeny($fullpath)
|
||||
{
|
||||
if (!file_exists($fullpath)) {
|
||||
return; // Not created yet.
|
||||
}
|
||||
@unlink($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok');
|
||||
if (file_exists($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok')) {
|
||||
throw new Exception('Cannot remove git-daemon-export-ok file.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the export flag.
|
||||
*
|
||||
* @param string Full path to the repository
|
||||
*/
|
||||
public function gitExportAllow($fullpath)
|
||||
{
|
||||
if (!file_exists($fullpath)) {
|
||||
return; // Not created yet.
|
||||
}
|
||||
touch($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok');
|
||||
if (!file_exists($fullpath.DIRECTORY_SEPARATOR.'git-daemon-export-ok')) {
|
||||
throw new Exception('Cannot create git-daemon-export-ok file.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
264
indefero/src/IDF/Plugin/SyncMercurial.php
Normal file
264
indefero/src/IDF/Plugin/SyncMercurial.php
Normal file
@@ -0,0 +1,264 @@
|
||||
<?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-2011 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 classes is a plugin which allows to synchronise access rights
|
||||
* between indefero and mercurial web-published repositories.
|
||||
*/
|
||||
class IDF_Plugin_SyncMercurial
|
||||
{
|
||||
|
||||
/**
|
||||
* Entry point of the plugin.
|
||||
*/
|
||||
static public function entry($signal, &$params)
|
||||
{
|
||||
// First check for the 3 mandatory config variables.
|
||||
if (!Pluf::f('idf_plugin_syncmercurial_passwd_file', false) or
|
||||
!Pluf::f('idf_plugin_syncmercurial_path', false) or
|
||||
!Pluf::f('idf_plugin_syncmercurial_hgrc', false)) {
|
||||
return;
|
||||
}
|
||||
include_once 'File/Passwd/Authdigest.php';
|
||||
$plug = new IDF_Plugin_SyncMercurial();
|
||||
switch ($signal) {
|
||||
case 'IDF_Project::created':
|
||||
$plug->processMercurialCreate($params['project']);
|
||||
break;
|
||||
case 'IDF_Project::membershipsUpdated':
|
||||
$plug->processSyncAuthz($params['project']);
|
||||
break;
|
||||
case 'Pluf_User::passwordUpdated':
|
||||
$plug->processSyncPasswd($params['user']);
|
||||
break;
|
||||
case 'hgchangegroup.php::run':
|
||||
$plug->processSyncTimeline($params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run hg init command to create the corresponding Mercurial
|
||||
* repository.
|
||||
*
|
||||
* @param IDF_Project
|
||||
* @return bool Success
|
||||
*/
|
||||
function processMercurialCreate($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'mercurial') {
|
||||
return false;
|
||||
}
|
||||
$shortname = $project->shortname;
|
||||
if (false===($mercurial_path=Pluf::f('idf_plugin_syncmercurial_path',false))) {
|
||||
throw new Pluf_Exception_SettingError("'idf_plugin_syncmercurial_path' must be defined in your configuration file.");
|
||||
}
|
||||
|
||||
if (file_exists($mercurial_path.'/'.$shortname)) {
|
||||
throw new Exception(sprintf(__('The repository %s already exists.'),
|
||||
$mercurial_path.'/'.$shortname));
|
||||
}
|
||||
$return = 0;
|
||||
$output = array();
|
||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' init %s',
|
||||
escapeshellarg($mercurial_path.'/'.$shortname));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
$ll = exec($cmd, $output, $return);
|
||||
return ($return == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise an user's password.
|
||||
*
|
||||
* @param Pluf_User
|
||||
*/
|
||||
function processSyncPasswd($user)
|
||||
{
|
||||
$passwd_file = Pluf::f('idf_plugin_syncmercurial_passwd_file');
|
||||
if (!file_exists($passwd_file) or !is_writable($passwd_file)) {
|
||||
return false;
|
||||
}
|
||||
$ht = new File_Passwd_Authbasic($passwd_file);
|
||||
$ht->load();
|
||||
//$ht->setMode(Pluf::f('idf_plugin_syncmercurial_passwd_mode',
|
||||
// FILE_PASSWD_SHA));
|
||||
$ht->setMode("plain");
|
||||
if ($ht->userExists($user->login)) {
|
||||
$ht->changePasswd($user->login, "{SHA}" . $this->getMercurialPass($user));
|
||||
} else {
|
||||
$ht->addUser($user->login, "{SHA}" . $this->getMercurialPass($user));
|
||||
}
|
||||
$ht->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the hgrc file and the passwd file for the project.
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
function processSyncAuthz($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'mercurial') {
|
||||
return false;
|
||||
}
|
||||
$this->SyncAccess($project);
|
||||
$this->generateProjectPasswd($project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository password for the user
|
||||
*/
|
||||
function getMercurialPass($user){
|
||||
//echo $user->password.Pluf::f('secret_key');
|
||||
# return base64_encode(sha1($_POST["password"], true));
|
||||
#file_put_contents("/tmp/test3", "test");
|
||||
//return sha1($_POST["password"], true);
|
||||
//if (isset($_POST["password"])) return $_POST["password"];
|
||||
//return $_POST["password"];
|
||||
//file_put_contents("/tmp/test", $user->password.Pluf::f('secret_key'));
|
||||
//file_put_contents("/tmp/test", $user->password);
|
||||
//return substr(sha1($user->password.Pluf::f('secret_key')), 0, 8);
|
||||
return $user->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a particular project: update all passwd information
|
||||
*/
|
||||
function generateProjectPasswd($project)
|
||||
{
|
||||
$passwd_file = Pluf::f('idf_plugin_syncmercurial_passwd_file');
|
||||
if (!file_exists($passwd_file) or !is_writable($passwd_file)) {
|
||||
throw new Exception (sprintf(__('%s does not exist or is not writable.'), $passwd_file));
|
||||
}
|
||||
$ht = new File_Passwd_Authbasic($passwd_file);
|
||||
//$ht->setMode(Pluf::f('idf_plugin_syncmercurial_passwd_mode',
|
||||
// FILE_PASSWD_SHA));
|
||||
$ht->setMode("plain");
|
||||
$ht->load();
|
||||
$mem = $project->getMembershipData();
|
||||
$members = array_merge((array)$mem['members'], (array)$mem['owners'],
|
||||
(array)$mem['authorized']);
|
||||
foreach($members as $user) {
|
||||
//file_put_contents("/tmp/test", $this->getMercurialPass($user));
|
||||
if ($ht->userExists($user->login)) {
|
||||
$ht->changePasswd($user->login, "{SHA}" . $this->getMercurialPass($user));
|
||||
} else {
|
||||
$ht->addUser($user->login, "{SHA}" . $this->getMercurialPass($user));
|
||||
}
|
||||
}
|
||||
$ht->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the hgrc file
|
||||
*/
|
||||
function SyncAccess($project)
|
||||
{
|
||||
$shortname = $project->shortname;
|
||||
$hgrc_file = Pluf::f('idf_plugin_syncmercurial_path').sprintf('/%s/.hg/hgrc', $shortname);
|
||||
|
||||
// Get allow_push list
|
||||
$allow_push = '';
|
||||
$mem = $project->getMembershipData();
|
||||
foreach ($mem['owners'] as $v) {
|
||||
$allow_push .= $v->login.' ';
|
||||
}
|
||||
foreach ($mem['members'] as $v) {
|
||||
$allow_push .= $v->login.' ';
|
||||
}
|
||||
|
||||
// Generate hgrc content
|
||||
if (is_file($hgrc_file)) {
|
||||
$tmp_content = @parse_ini_file($hgrc_file, true, INI_SCANNER_RAW);
|
||||
if ($tmp_content === false) {
|
||||
throw new Exception('could not parse "'.$hgrc_file.'" because of syntax problems');
|
||||
}
|
||||
$tmp_content['web']['allow_push'] = $allow_push;
|
||||
}
|
||||
else {
|
||||
$tmp_content = Pluf::f('idf_plugin_syncmercurial_hgrc');
|
||||
$tmp_content['web']['allow_push'] = $allow_push;
|
||||
}
|
||||
$fcontent = '';
|
||||
foreach ($tmp_content as $key => $elem){
|
||||
$fcontent .= '['.$key."]\n";
|
||||
foreach ($elem as $key2 => $elem2){
|
||||
$fcontent .= $key2.' = '.$elem2."\n";
|
||||
}
|
||||
}
|
||||
file_put_contents($hgrc_file, $fcontent, LOCK_EX);
|
||||
|
||||
// Generate private repository config file
|
||||
$private_file = Pluf::f('idf_plugin_syncmercurial_private_include');
|
||||
$notify_file = Pluf::f('idf_plugin_syncmercurial_private_notify');
|
||||
$fcontent = '';
|
||||
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||
$conf = new IDF_Conf();
|
||||
$conf->setProject($project);
|
||||
if ($project->private == true){
|
||||
$mem = $project->getMembershipData();
|
||||
$user = '';
|
||||
foreach ($mem['owners'] as $v) {
|
||||
$user .= $v->login.' ';
|
||||
}
|
||||
foreach ($mem['members'] as $v) {
|
||||
$user .= $v->login.' ';
|
||||
}
|
||||
foreach ($mem['authorized'] as $v) {
|
||||
$user .= $v->login.' ';
|
||||
}
|
||||
$fcontent .= '<Location '. sprintf(Pluf::f('idf_plugin_syncmercurial_private_url'), $project->shortname).'>'."\n";
|
||||
$fcontent .= 'AuthType Basic'."\n";
|
||||
$fcontent .= 'AuthName "Restricted"'."\n";
|
||||
$fcontent .= sprintf('AuthUserFile %s', Pluf::f('idf_plugin_syncmercurial_passwd_file'))."\n";
|
||||
$fcontent .= sprintf('Require user %s', $user)."\n";
|
||||
$fcontent .= '</Location>'."\n\n";
|
||||
}
|
||||
}
|
||||
file_put_contents($private_file, $fcontent, LOCK_EX);
|
||||
file_put_contents($notify_file, ' ', LOCK_EX);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timeline in post commit.
|
||||
*
|
||||
*/
|
||||
public function processSyncTimeline($params)
|
||||
{
|
||||
$pname = basename($params['rel_dir']);
|
||||
try {
|
||||
$project = IDF_Project::getOr404($pname);
|
||||
} catch (Pluf_HTTP_Error404 $e) {
|
||||
Pluf_Log::event(array('IDF_Plugin_SyncMercurial::processSyncTimeline', 'Project not found.', array($pname, $params)));
|
||||
return false; // Project not found
|
||||
}
|
||||
// Now we have the project and can update the timeline
|
||||
Pluf_Log::debug(array('IDF_Plugin_SyncMercurial::processSyncTimeline', 'Project found', $pname, $project->id));
|
||||
IDF_Scm::syncTimeline($project, true);
|
||||
Pluf_Log::event(array('IDF_Plugin_SyncMercurial::processSyncTimeline', 'sync', array($pname, $project->id)));
|
||||
|
||||
|
||||
}
|
||||
}
|
911
indefero/src/IDF/Plugin/SyncMonotone.php
Normal file
911
indefero/src/IDF/Plugin/SyncMonotone.php
Normal file
@@ -0,0 +1,911 @@
|
||||
<?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-2011 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 classes is a plugin which allows to synchronise access rights
|
||||
* between indefero and monotone usher setups.
|
||||
*/
|
||||
class IDF_Plugin_SyncMonotone
|
||||
{
|
||||
private $old_err_rep = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->old_err_rep = error_reporting(0);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
error_reporting($this->old_err_rep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point of the plugin.
|
||||
*/
|
||||
static public function entry($signal, &$params)
|
||||
{
|
||||
$plug = new IDF_Plugin_SyncMonotone();
|
||||
switch ($signal) {
|
||||
case 'IDF_Project::created':
|
||||
$plug->processProjectCreate($params['project']);
|
||||
break;
|
||||
case 'IDF_Project::membershipsUpdated':
|
||||
$plug->processMembershipsUpdated($params['project']);
|
||||
break;
|
||||
case 'IDF_Project::preDelete':
|
||||
$plug->processProjectDelete($params['project']);
|
||||
break;
|
||||
case 'IDF_Key::postSave':
|
||||
$plug->processKeyCreate($params['key']);
|
||||
break;
|
||||
case 'IDF_Key::preDelete':
|
||||
$plug->processKeyDelete($params['key']);
|
||||
break;
|
||||
case 'mtnpostpush.php::run':
|
||||
$plug->processSyncTimeline($params['project']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial steps to setup a new monotone project:
|
||||
*
|
||||
* 1) run mtn db init to initialize a new database underknees
|
||||
* 'mtn_repositories'
|
||||
* 2) create a new server key in the same directory
|
||||
* 3) create a new client key for IDF and store it in the project conf
|
||||
* 4) setup the configuration
|
||||
* 5) add the database as new local server in the usher configuration
|
||||
* 6) reload the running usher instance so it acknowledges the new server
|
||||
*
|
||||
* The initial right setup happens in processMembershipsUpdated()
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
function processProjectCreate($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pluf::f('mtn_db_access', 'local') == 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
// This guard cleans up on any kind of error, and here is how it works:
|
||||
// As long as the guard is not committed, it keeps a reference to
|
||||
// the given project. When the guard is destroyed and the reference
|
||||
// is still present, it deletes the object. The deletion indirectly
|
||||
// also calls into this plugin again, as the project delete hook
|
||||
// will be called, that removes any changes we've made during the
|
||||
// process.
|
||||
$projectGuard = new IDF_Plugin_SyncMonotone_ModelGuard($project);
|
||||
|
||||
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||
if ($projecttempl === false) {
|
||||
$this->_diagnoseProblem(
|
||||
__('"mtn_repositories" must be defined in your configuration file')
|
||||
);
|
||||
}
|
||||
|
||||
$usher_config = Pluf::f('mtn_usher_conf', false);
|
||||
if (!$usher_config || !is_writable($usher_config)) {
|
||||
$this->_diagnoseProblem(
|
||||
__('"mtn_usher_conf" does not exist or is not writable')
|
||||
);
|
||||
}
|
||||
|
||||
$mtnpostpush = realpath(dirname(__FILE__) . '/../../../scripts/mtn-post-push');
|
||||
if (!file_exists($mtnpostpush)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not find mtn-post-push script "%s"'), $mtnpostpush
|
||||
));
|
||||
}
|
||||
|
||||
// check some static configuration files
|
||||
$confdir = Pluf::f('mtn_confdir', false);
|
||||
if ($confdir === false) {
|
||||
$confdir = dirname(__FILE__).'/SyncMonotone/';
|
||||
}
|
||||
$confdir_contents = array(
|
||||
'monotonerc.in',
|
||||
'remote-automate-permissions.in',
|
||||
'hooks.d/',
|
||||
'hooks.d/indefero_authorize_remote_automate.lua',
|
||||
'hooks.d/indefero_post_push.conf.in',
|
||||
'hooks.d/indefero_post_push.lua',
|
||||
);
|
||||
// enable remote command execution of read-only commands
|
||||
// only for public projects
|
||||
if (!$project->private) {
|
||||
// this is linked and not copied to be able to update
|
||||
// the list of read-only commands on upgrades
|
||||
$confdir_contents[] = 'hooks.d/indefero_authorize_remote_automate.conf';
|
||||
}
|
||||
|
||||
// check whether we should handle additional files in the config directory
|
||||
$confdir_extra_contents = Pluf::f('mtn_confdir_extra', false);
|
||||
if ($confdir_extra_contents !== false) {
|
||||
$confdir_contents =
|
||||
array_merge($confdir_contents, $confdir_extra_contents);
|
||||
}
|
||||
foreach ($confdir_contents as $content) {
|
||||
if (!file_exists($confdir.$content)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('The configuration file "%s" is missing'), $content
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$shortname = $project->shortname;
|
||||
$projectpath = sprintf($projecttempl, $shortname);
|
||||
if (file_exists($projectpath)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('The project path "%s" already exists'), $projectpath
|
||||
));
|
||||
}
|
||||
|
||||
if (!@mkdir($projectpath)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('The project path "%s" could not be created'),
|
||||
$projectpath
|
||||
));
|
||||
}
|
||||
|
||||
//
|
||||
// step 1) create a new database
|
||||
//
|
||||
$dbfile = $projectpath.'/database.mtn';
|
||||
$cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
|
||||
$this->_mtn_exec($cmd);
|
||||
|
||||
//
|
||||
// step 2) create a server key
|
||||
//
|
||||
// try to parse the key's domain part from the remote_url's host
|
||||
// name, otherwise fall back to the configured Apache server name
|
||||
$server = $_SERVER['SERVER_NAME'];
|
||||
$remote_url = Pluf::f('mtn_remote_url');
|
||||
if (($parsed = parse_url($remote_url)) !== false &&
|
||||
!empty($parsed['host'])) {
|
||||
$server = $parsed['host'];
|
||||
}
|
||||
|
||||
$serverkey = $shortname.'-server@'.$server;
|
||||
$cmd = sprintf('au generate_key --confdir=%s %s ""',
|
||||
escapeshellarg($projectpath),
|
||||
escapeshellarg($serverkey)
|
||||
);
|
||||
$this->_mtn_exec($cmd);
|
||||
|
||||
//
|
||||
// step 3) create a client key, and save it in IDF
|
||||
//
|
||||
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||
if (!file_exists($keydir)) {
|
||||
if (!@mkdir($keydir)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('The key directory "%s" could not be created'),
|
||||
$keydir
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$clientkey_name = $shortname.'-client@'.$server;
|
||||
$cmd = sprintf('au generate_key --keydir=%s %s ""',
|
||||
escapeshellarg($keydir),
|
||||
escapeshellarg($clientkey_name)
|
||||
);
|
||||
$keyinfo = $this->_mtn_exec($cmd);
|
||||
|
||||
$parsed_keyinfo = array();
|
||||
try {
|
||||
$parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not parse key information: %s'), $e->getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
$clientkey_hash = $parsed_keyinfo[0][1]['hash'];
|
||||
$clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash;
|
||||
$clientkey_data = file_get_contents($clientkey_file);
|
||||
|
||||
$project->getConf()->setVal('mtn_client_key_name', $clientkey_name);
|
||||
$project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash);
|
||||
$project->getConf()->setVal('mtn_client_key_data', $clientkey_data);
|
||||
|
||||
// add the public client key to the server
|
||||
$cmd = sprintf('au get_public_key --keydir=%s %s',
|
||||
escapeshellarg($keydir),
|
||||
escapeshellarg($clientkey_hash)
|
||||
);
|
||||
$clientkey_pubdata = $this->_mtn_exec($cmd);
|
||||
|
||||
$cmd = sprintf('au put_public_key --db=%s %s',
|
||||
escapeshellarg($dbfile),
|
||||
escapeshellarg($clientkey_pubdata)
|
||||
);
|
||||
$this->_mtn_exec($cmd);
|
||||
|
||||
//
|
||||
// step 4) setup the configuration
|
||||
//
|
||||
|
||||
// we assume that all confdir entries ending with a slash mean a
|
||||
// directory that has to be created, that all files ending on ".in"
|
||||
// have to be processed and copied in place and that all other files
|
||||
// just need to be symlinked from the original location
|
||||
foreach ($confdir_contents as $content) {
|
||||
$filepath = $projectpath.'/'.$content;
|
||||
if (substr($content, -1) == '/') {
|
||||
if (!@mkdir($filepath)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not create configuration directory "%s"'),
|
||||
$filepath
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (substr($content, -3) != '.in') {
|
||||
if (!@symlink($confdir.$content, $filepath)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not create symlink for configuration file "%s"'),
|
||||
$filepath
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$filecontents = file_get_contents($confdir.'/'.$content);
|
||||
$filecontents = str_replace(
|
||||
array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
|
||||
array($mtnpostpush, $shortname, $clientkey_hash),
|
||||
$filecontents
|
||||
);
|
||||
|
||||
// remove the .in
|
||||
$filepath = substr($filepath, 0, -3);
|
||||
if (@file_put_contents($filepath, $filecontents, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write configuration file "%s"'),
|
||||
$filepath
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// step 5) read in and append the usher config with the new server
|
||||
//
|
||||
$usher_rc = file_get_contents($usher_config);
|
||||
$parsed_config = array();
|
||||
try {
|
||||
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not parse usher configuration in "%1$s": %2$s'),
|
||||
$usher_config, $e->getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
// ensure we haven't configured a server with this name already
|
||||
foreach ($parsed_config as $stanzas) {
|
||||
foreach ($stanzas as $stanza_line) {
|
||||
if ($stanza_line['key'] == 'server' &&
|
||||
$stanza_line['values'][0] == $shortname) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('usher configuration already contains a server '.
|
||||
'entry named "%s"'),
|
||||
$shortname
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$new_server = array(
|
||||
array('key' => 'server', 'values' => array($shortname)),
|
||||
array('key' => 'local', 'values' => array(
|
||||
'--confdir', $projectpath,
|
||||
'-d', $dbfile,
|
||||
'--timestamps',
|
||||
'--ticker=dot'
|
||||
)),
|
||||
);
|
||||
|
||||
$parsed_config[] = $new_server;
|
||||
$usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
|
||||
|
||||
// FIXME: more sanity - what happens on failing writes? we do not
|
||||
// have a backup copy of usher.conf around...
|
||||
if (@file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write usher configuration file "%s"'),
|
||||
$usher_config
|
||||
));
|
||||
}
|
||||
|
||||
//
|
||||
// step 6) reload usher to pick up the new configuration
|
||||
//
|
||||
IDF_Scm_Monotone_Usher::reload();
|
||||
|
||||
// commit the guard, so the newly created project is not deleted
|
||||
$projectGuard->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the read / write permissions for the monotone database
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
public function processMembershipsUpdated($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pluf::f('mtn_db_access', 'local') == 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
$mtn = IDF_Scm_Monotone::factory($project);
|
||||
$stdio = $mtn->getStdio();
|
||||
|
||||
$projectpath = $this->_get_project_path($project);
|
||||
$auth_ids = $this->_get_authorized_user_ids($project);
|
||||
$key_ids = array();
|
||||
foreach ($auth_ids as $auth_id) {
|
||||
$sql = new Pluf_SQL('user=%s', array($auth_id));
|
||||
$keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen()));
|
||||
foreach ($keys as $key) {
|
||||
if ($key->getType() != 'mtn')
|
||||
continue;
|
||||
$stdio->exec(array('put_public_key', $key->content));
|
||||
$key_ids[] = $key->getMtnId();
|
||||
}
|
||||
}
|
||||
|
||||
$write_permissions = implode("\n", $key_ids);
|
||||
$rcfile = $projectpath.'/write-permissions';
|
||||
if (@file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write write-permissions file "%s"'),
|
||||
$rcfile
|
||||
));
|
||||
}
|
||||
|
||||
if ($project->private) {
|
||||
$stanza = array(
|
||||
array('key' => 'pattern', 'values' => array('*')),
|
||||
);
|
||||
foreach ($key_ids as $key_id)
|
||||
{
|
||||
$stanza[] = array('key' => 'allow', 'values' => array($key_id));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stanza = array(
|
||||
array('key' => 'pattern', 'values' => array('*')),
|
||||
array('key' => 'allow', 'values' => array('*')),
|
||||
);
|
||||
}
|
||||
|
||||
$read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza));
|
||||
$rcfile = $projectpath.'/read-permissions';
|
||||
if (@file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write read-permissions file "%s"'),
|
||||
$rcfile
|
||||
));
|
||||
}
|
||||
|
||||
// link / unlink the read-only automate permissions for the project
|
||||
$confdir = Pluf::f('mtn_confdir', false);
|
||||
if ($confdir === false) {
|
||||
$confdir = dirname(__FILE__).'/SyncMonotone/';
|
||||
}
|
||||
$file = 'hooks.d/indefero_authorize_remote_automate.conf';
|
||||
$projectfile = $projectpath.'/'.$file;
|
||||
$templatefile = $confdir.'/'.$file;
|
||||
|
||||
$serverRestartRequired = false;
|
||||
if ($project->private && file_exists($projectfile) && is_link($projectfile)) {
|
||||
if (!@unlink($projectfile)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not remove symlink "%s"'), $projectfile
|
||||
));
|
||||
}
|
||||
$serverRestartRequired = true;
|
||||
} else
|
||||
if (!$project->private && !file_exists($projectfile)) {
|
||||
if (!@symlink($templatefile, $projectfile)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not create symlink "%s"'), $projectfile
|
||||
));
|
||||
}
|
||||
$serverRestartRequired = true;
|
||||
}
|
||||
|
||||
if ($serverRestartRequired) {
|
||||
// FIXME: we should actually use stopServer() here, but this
|
||||
// seems to be ignored when the server should be started
|
||||
// again immediately afterwards
|
||||
IDF_Scm_Monotone_Usher::killServer($project->shortname);
|
||||
// give usher some time to cool down, otherwise it might hang
|
||||
// (see https://code.monotone.ca/p/contrib/issues/175/)
|
||||
sleep(2);
|
||||
IDF_Scm_Monotone_Usher::startServer($project->shortname);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after a mtn project was deleted
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
public function processProjectDelete($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pluf::f('mtn_db_access', 'local') == 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
$usher_config = Pluf::f('mtn_usher_conf', false);
|
||||
if (!$usher_config || !is_writable($usher_config)) {
|
||||
$this->_diagnoseProblem(
|
||||
__('"mtn_usher_conf" does not exist or is not writable')
|
||||
);
|
||||
}
|
||||
|
||||
$shortname = $project->shortname;
|
||||
IDF_Scm_Monotone_Usher::killServer($shortname);
|
||||
|
||||
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||
if ($projecttempl === false) {
|
||||
$this->_diagnoseProblem(
|
||||
__('"mtn_repositories" must be defined in your configuration file')
|
||||
);
|
||||
}
|
||||
|
||||
$projectpath = sprintf($projecttempl, $shortname);
|
||||
if (file_exists($projectpath)) {
|
||||
if (!$this->_delete_recursive($projectpath)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('One or more paths underneath %s could not be deleted'), $projectpath
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||
$keyname = $project->getConf()->getVal('mtn_client_key_name', false);
|
||||
$keyhash = $project->getConf()->getVal('mtn_client_key_hash', false);
|
||||
if ($keyname && $keyhash &&
|
||||
file_exists($keydir .'/'. $keyname . '.' . $keyhash)) {
|
||||
if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not delete client private key "%s"'),
|
||||
$keyname
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$usher_rc = file_get_contents($usher_config);
|
||||
$parsed_config = array();
|
||||
try {
|
||||
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not parse usher configuration in "%1$s": %2$s'),
|
||||
$usher_config, $e->getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($parsed_config as $idx => $stanzas) {
|
||||
foreach ($stanzas as $stanza_line) {
|
||||
if ($stanza_line['key'] == 'server' &&
|
||||
$stanza_line['values'][0] == $shortname) {
|
||||
unset($parsed_config[$idx]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
|
||||
|
||||
// FIXME: more sanity - what happens on failing writes? we do not
|
||||
// have a backup copy of usher.conf around...
|
||||
if (@file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write usher configuration file "%s"'),
|
||||
$usher_config
|
||||
));
|
||||
}
|
||||
|
||||
IDF_Scm_Monotone_Usher::reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the (monotone) key to all monotone projects of this forge
|
||||
* where the user of the key has write access to
|
||||
*/
|
||||
public function processKeyCreate($key)
|
||||
{
|
||||
if ($key->getType() != 'mtn') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pluf::f('mtn_db_access', 'local') == 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
$keyGuard = new IDF_Plugin_SyncMonotone_ModelGuard($key);
|
||||
|
||||
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||
$conf = new IDF_Conf();
|
||||
$conf->setProject($project);
|
||||
$scm = $conf->getVal('scm', 'mtn');
|
||||
if ($scm != 'mtn')
|
||||
continue;
|
||||
|
||||
$projectpath = $this->_get_project_path($project);
|
||||
$auth_ids = $this->_get_authorized_user_ids($project);
|
||||
if (!in_array($key->user, $auth_ids))
|
||||
continue;
|
||||
|
||||
$mtn_key_id = $key->getMtnId();
|
||||
|
||||
// if the project is not defined as private, all people have
|
||||
// read access already, so we don't need to write anything
|
||||
// and we currently do not check if read-permissions really
|
||||
// contains
|
||||
// pattern "*"
|
||||
// allow "*"
|
||||
// which is the default for non-private projects
|
||||
if ($project->private == true) {
|
||||
$read_perms = file_get_contents($projectpath.'/read-permissions');
|
||||
$parsed_read_perms = array();
|
||||
try {
|
||||
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not parse read-permissions for project "%1$s": %2$s'),
|
||||
$shortname, $e->getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
$wildcard_section = null;
|
||||
for ($i=0; $i<count($parsed_read_perms); ++$i) {
|
||||
foreach ($parsed_read_perms[$i] as $stanza_line) {
|
||||
if ($stanza_line['key'] == 'pattern' &&
|
||||
$stanza_line['values'][0] == '*') {
|
||||
$wildcard_section =& $parsed_read_perms[$i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($wildcard_section == null)
|
||||
{
|
||||
$wildcard_section = array(
|
||||
array('key' => 'pattern', 'values' => array('*'))
|
||||
);
|
||||
$parsed_read_perms[] =& $wildcard_section;
|
||||
}
|
||||
|
||||
$key_found = false;
|
||||
foreach ($wildcard_section as $line)
|
||||
{
|
||||
if ($line['key'] == 'allow' && $line['values'][0] == $mtn_key_id) {
|
||||
$key_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$key_found) {
|
||||
$wildcard_section[] = array(
|
||||
'key' => 'allow', 'values' => array($mtn_key_id)
|
||||
);
|
||||
}
|
||||
|
||||
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
|
||||
|
||||
if (@file_put_contents($projectpath.'/read-permissions',
|
||||
$read_perms, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write read-permissions for project "%s"'),
|
||||
$shortname
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$write_perms = file_get_contents($projectpath.'/write-permissions');
|
||||
$lines = preg_split("/(\n|\r\n)/", $write_perms, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) {
|
||||
$lines[] = $mtn_key_id;
|
||||
}
|
||||
if (@file_put_contents($projectpath.'/write-permissions',
|
||||
implode("\n", $lines) . "\n", LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write write-permissions file for project "%s"'),
|
||||
$shortname
|
||||
));
|
||||
}
|
||||
|
||||
$mtn = IDF_Scm_Monotone::factory($project);
|
||||
$stdio = $mtn->getStdio();
|
||||
$stdio->exec(array('put_public_key', $key->content));
|
||||
}
|
||||
|
||||
$keyGuard->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the (monotone) key from all monotone projects of this forge
|
||||
* where the user of the key has write access to
|
||||
*/
|
||||
public function processKeyDelete($key)
|
||||
{
|
||||
try {
|
||||
if ($key->getType() != 'mtn') {
|
||||
return;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// bad key type, skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pluf::f('mtn_db_access', 'local') == 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||
$conf = new IDF_Conf();
|
||||
$conf->setProject($project);
|
||||
$scm = $conf->getVal('scm', 'mtn');
|
||||
if ($scm != 'mtn')
|
||||
continue;
|
||||
|
||||
$projectpath = $this->_get_project_path($project);
|
||||
$auth_ids = $this->_get_authorized_user_ids($project);
|
||||
if (!in_array($key->user, $auth_ids))
|
||||
continue;
|
||||
|
||||
$mtn_key_id = $key->getMtnId();
|
||||
|
||||
// if the project is not defined as private, all people have
|
||||
// read access already, so we don't need to write anything
|
||||
// and we currently do not check if read-permissions really
|
||||
// contains
|
||||
// pattern "*"
|
||||
// allow "*"
|
||||
// which is the default for non-private projects
|
||||
if ($project->private) {
|
||||
$read_perms = file_get_contents($projectpath.'/read-permissions');
|
||||
$parsed_read_perms = array();
|
||||
try {
|
||||
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not parse read-permissions for project "%1$s": %2$s'),
|
||||
$shortname, $e->getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
// while we add new keys only to an existing wild-card entry
|
||||
// we remove dropped keys from all sections since the key
|
||||
// should be simply unavailable for all of them
|
||||
for ($h=0; $h<count($parsed_read_perms); ++$h) {
|
||||
for ($i=0; $i<count($parsed_read_perms[$h]); ++$i) {
|
||||
if ($parsed_read_perms[$h][$i]['key'] == 'allow' &&
|
||||
$parsed_read_perms[$h][$i]['values'][0] == $mtn_key_id) {
|
||||
unset($parsed_read_perms[$h][$i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
|
||||
|
||||
if (@file_put_contents($projectpath.'/read-permissions',
|
||||
$read_perms, LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write read-permissions for project "%s"'),
|
||||
$shortname
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$write_perms = file_get_contents($projectpath.'/write-permissions');
|
||||
$lines = preg_split("/(\n|\r\n)/", $write_perms, -1, PREG_SPLIT_NO_EMPTY);
|
||||
for ($i=0; $i<count($lines); ++$i) {
|
||||
if ($lines[$i] == $mtn_key_id) {
|
||||
unset($lines[$i]);
|
||||
// the key should actually only exist once in the
|
||||
// file, but we're paranoid
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (@file_put_contents($projectpath.'/write-permissions',
|
||||
implode("\n", $lines) . "\n", LOCK_EX) === false) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('Could not write write-permissions file for project "%s"'),
|
||||
$shortname
|
||||
));
|
||||
}
|
||||
|
||||
$mtn = IDF_Scm_Monotone::factory($project);
|
||||
$stdio = $mtn->getStdio();
|
||||
// if the public key did not sign any revisions, drop it from
|
||||
// the database as well
|
||||
try {
|
||||
if (strlen($stdio->exec(array('select', 'k:' . $mtn_key_id))) == 0) {
|
||||
$stdio->exec(array('drop_public_key', $mtn_key_id));
|
||||
}
|
||||
} catch (IDF_Scm_Exception $e) {
|
||||
if (strpos($e->getMessage(), 'there is no key named') === false)
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timeline after a push
|
||||
*
|
||||
*/
|
||||
public function processSyncTimeline($project_name)
|
||||
{
|
||||
try {
|
||||
$project = IDF_Project::getOr404($project_name);
|
||||
} catch (Pluf_HTTP_Error404 $e) {
|
||||
Pluf_Log::event(array(
|
||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||
'Project not found.',
|
||||
array($project_name, $params)
|
||||
));
|
||||
return false; // Project not found
|
||||
}
|
||||
|
||||
Pluf_Log::debug(array(
|
||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||
'Project found', $project_name, $project->id
|
||||
));
|
||||
IDF_Scm::syncTimeline($project, true);
|
||||
Pluf_Log::event(array(
|
||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||
'sync', array($project_name, $project->id)
|
||||
));
|
||||
}
|
||||
|
||||
private function _get_project_path($project)
|
||||
{
|
||||
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||
if ($projecttempl === false) {
|
||||
$this->_diagnoseProblem(
|
||||
__('"mtn_repositories" must be defined in your configuration file.')
|
||||
);
|
||||
}
|
||||
|
||||
$projectpath = sprintf($projecttempl, $project->shortname);
|
||||
if (!file_exists($projectpath)) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('The project path %s does not exists.'), $projectpath
|
||||
));
|
||||
}
|
||||
return $projectpath;
|
||||
}
|
||||
|
||||
private function _mtn_exec($cmd)
|
||||
{
|
||||
$fullcmd = sprintf('%s %s %s',
|
||||
Pluf::f('idf_exec_cmd_prefix', ''),
|
||||
Pluf::f('mtn_path', 'mtn'),
|
||||
$cmd
|
||||
);
|
||||
|
||||
$output = $return = null;
|
||||
exec($fullcmd, $output, $return);
|
||||
if ($return != 0) {
|
||||
$this->_diagnoseProblem(sprintf(
|
||||
__('The command "%s" could not be executed.'), $cmd
|
||||
));
|
||||
}
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
private function _get_authorized_user_ids($project)
|
||||
{
|
||||
$mem = $project->getMembershipData();
|
||||
$members = array_merge((array)$mem['members'],
|
||||
(array)$mem['owners'],
|
||||
(array)$mem['authorized']);
|
||||
$userids = array();
|
||||
foreach ($members as $member) {
|
||||
$userids[] = $member->id;
|
||||
}
|
||||
return $userids;
|
||||
}
|
||||
|
||||
private function _delete_recursive($path)
|
||||
{
|
||||
if (is_file($path) || is_link($path)) {
|
||||
return @unlink($path);
|
||||
}
|
||||
|
||||
if (is_dir($path)) {
|
||||
$scan = glob(rtrim($path, '/') . '/*');
|
||||
$status = 0;
|
||||
foreach ($scan as $subpath) {
|
||||
$status |= $this->_delete_recursive($subpath);
|
||||
}
|
||||
$status |= @rmdir($path);
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
||||
private function _diagnoseProblem($msg)
|
||||
{
|
||||
$system_err = error_get_last();
|
||||
if (!empty($system_err)) {
|
||||
$msg .= ': '.$system_err['message'];
|
||||
}
|
||||
|
||||
error_reporting($this->old_err_rep);
|
||||
throw new IDF_Scm_Exception($msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple helper class that deletes the model instance if
|
||||
* it is not committed
|
||||
*/
|
||||
class IDF_Plugin_SyncMonotone_ModelGuard
|
||||
{
|
||||
private $model;
|
||||
|
||||
public function __construct(Pluf_Model $m)
|
||||
{
|
||||
$this->model = $m;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->model == null)
|
||||
return;
|
||||
$this->model->delete();
|
||||
}
|
||||
|
||||
public function commit()
|
||||
{
|
||||
$this->model = null;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,10 @@
|
||||
ARA_safe_commands = {
|
||||
"get_corresponding_path", "get_content_changed", "tags", "branches",
|
||||
"common_ancestors", "packet_for_fdelta", "packet_for_fdata",
|
||||
"packets_for_certs", "packet_for_rdata", "get_manifest_of",
|
||||
"get_revision", "select", "graph", "children", "parents", "roots",
|
||||
"leaves", "ancestry_difference", "toposort", "erase_ancestors",
|
||||
"descendents", "ancestors", "heads", "get_file_of", "get_file",
|
||||
"interface_version", "get_attributes", "content_diff",
|
||||
"file_merge", "show_conflicts", "certs", "keys", "get_extended_manifest_of"
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
-- ***** BEGIN LICENSE BLOCK *****
|
||||
-- This file is part of InDefero, an open source project management application.
|
||||
-- Copyright (C) 2008-2011 Céondo Ltd and contributors.
|
||||
-- Copyright (C) 2010 Thomas Keller <me@thomaskeller.biz>
|
||||
-- Richard Levitte <richard@levitte.org>
|
||||
--
|
||||
-- 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 reads key identities from a file "remote-automate-permissions"
|
||||
-- in the configuration directory and permits those authenticating with one
|
||||
-- of those keys to perform dangerous (read/write) remote automate operations.
|
||||
-- The format of the file is very simple, one key identity on every line.
|
||||
-- Lines starting with # are ignore, as well as empty lines.
|
||||
--
|
||||
-- It's possible to configure this script to allow the performance of some
|
||||
-- remote automate commands anonymously, through the variable
|
||||
-- ARA_safe_commands, which has to be a table of commands as strings.
|
||||
-- One example configuration, taken from the setup at code.monotone.ca, could
|
||||
-- be this:
|
||||
--
|
||||
-- ARA_safe_commands = {
|
||||
-- "get_corresponding_path", "get_content_changed", "tags", "branches",
|
||||
-- "common_ancestors", "packet_for_fdelta", "packet_for_fdata",
|
||||
-- "packets_for_certs", "packet_for_rdata", "get_manifest_of",
|
||||
-- "get_revision", "select", "graph", "children", "parents", "roots",
|
||||
-- "leaves", "ancestry_difference", "toposort", "erase_ancestors",
|
||||
-- "descendents", "ancestors", "heads", "get_file_of", "get_file",
|
||||
-- "interface_version", "get_attributes", "content_diff",
|
||||
-- "file_merge", "show_conflicts", "certs", "keys", "get_extended_manifest_of"
|
||||
-- }
|
||||
--
|
||||
do
|
||||
local _safe_commands = {}
|
||||
if ARA_safe_commands then
|
||||
_safe_commands = ARA_safe_commands
|
||||
end
|
||||
|
||||
local _save_get_remote_automate_permitted = get_remote_automate_permitted
|
||||
function get_remote_automate_permitted(key_identity, command, options)
|
||||
local permfile =
|
||||
io.open(get_confdir() .. "/remote-automate-permissions", "r")
|
||||
if (permfile == nil) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- See if the incoming key matches any of the key identities or
|
||||
-- patterns found in the permissions file.
|
||||
local matches = false
|
||||
local line = permfile:read()
|
||||
while (not matches and line ~= nil) do
|
||||
if not globish_match("#*", line) then
|
||||
local _, _, ln = string.find(line, "%s*([^%s]*)%s*")
|
||||
if ln == "*" then matches = true end
|
||||
if ln == key_identity.id then matches = true end
|
||||
if globish_match(ln, key_identity.name) then matches = true end
|
||||
line = permfile:read()
|
||||
end
|
||||
end
|
||||
io.close(permfile)
|
||||
if matches then return true end
|
||||
|
||||
-- No matching key found, let's see if the command matches one the
|
||||
-- admin allowed to be performed anonymously
|
||||
for _,v in ipairs(_safe_commands) do
|
||||
if (v == command[1]) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- No matches found anywhere, then don't permit this operation
|
||||
return false
|
||||
end
|
||||
end
|
@@ -0,0 +1,2 @@
|
||||
IDF_project = "%%PROJECT%%"
|
||||
IDF_push_script = "%%MTNPOSTPUSH%%"
|
@@ -0,0 +1,58 @@
|
||||
-- ***** BEGIN LICENSE BLOCK *****
|
||||
-- This file is part of InDefero, an open source project management application.
|
||||
-- Copyright (C) 2008-2011 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 *****
|
||||
|
||||
--
|
||||
-- let IDF know of new arriving revisions to fill its timeline
|
||||
--
|
||||
_idf_revs = {}
|
||||
push_hook_functions(
|
||||
{
|
||||
start =
|
||||
function (session_id)
|
||||
_idf_revs[session_id] = {}
|
||||
return "continue",nil
|
||||
end,
|
||||
revision_received =
|
||||
function (new_id, revision, certs, session_id)
|
||||
table.insert(_idf_revs[session_id], new_id)
|
||||
return "continue",nil
|
||||
end,
|
||||
["end"] =
|
||||
function (session_id, ...)
|
||||
if table.getn(_idf_revs[session_id]) == 0 then
|
||||
return "continue",nil
|
||||
end
|
||||
|
||||
local pin,pout,pid = spawn_pipe(IDF_push_script, IDF_project);
|
||||
if pid == -1 then
|
||||
print("could not execute " .. IDF_push_script)
|
||||
return "continue",nil
|
||||
end
|
||||
|
||||
for _,r in ipairs(_idf_revs[session_id]) do
|
||||
pin:write(r .. "\n")
|
||||
end
|
||||
pin:close()
|
||||
|
||||
wait(pid)
|
||||
return "continue",nil
|
||||
end
|
||||
})
|
||||
|
30
indefero/src/IDF/Plugin/SyncMonotone/monotonerc.in
Normal file
30
indefero/src/IDF/Plugin/SyncMonotone/monotonerc.in
Normal file
@@ -0,0 +1,30 @@
|
||||
-- ***** BEGIN LICENSE BLOCK *****
|
||||
-- This file is part of InDefero, an open source project management application.
|
||||
-- Copyright (C) 2008-2011 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 *****
|
||||
|
||||
---- Load local hooks if they exist.
|
||||
-- The way this is supposed to work is that hooks.d can contain symbolic
|
||||
-- links to lua scripts. These links MUST have the extension .lua
|
||||
-- If the script needs some configuration, a corresponding file with
|
||||
-- the extension .conf is the right spot.
|
||||
----
|
||||
-- First load the configuration of the hooks, if applicable
|
||||
includedirpattern(get_confdir() .. "/hooks.d/","*.conf")
|
||||
-- Then load the hooks themselves
|
||||
includedirpattern(get_confdir() .. "/hooks.d/","*.lua")
|
@@ -0,0 +1 @@
|
||||
%%MTNCLIENTKEY%%
|
294
indefero/src/IDF/Plugin/SyncSvn.php
Normal file
294
indefero/src/IDF/Plugin/SyncSvn.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?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-2011 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 classes is a plugin which allows to synchronise access rights
|
||||
* between indefero and a DAV powered Subversion repository.
|
||||
*/
|
||||
class IDF_Plugin_SyncSvn
|
||||
{
|
||||
|
||||
/**
|
||||
* Entry point of the plugin.
|
||||
*/
|
||||
static public function entry($signal, &$params)
|
||||
{
|
||||
// First check for the 3 mandatory config variables.
|
||||
if (!Pluf::f('idf_plugin_syncsvn_authz_file', false) or
|
||||
!Pluf::f('idf_plugin_syncsvn_passwd_file', false) or
|
||||
!Pluf::f('idf_plugin_syncsvn_svn_path', false)) {
|
||||
return;
|
||||
}
|
||||
include_once 'File/Passwd/Authdigest.php'; // $ pear install File_Passwd
|
||||
$plug = new IDF_Plugin_SyncSvn();
|
||||
switch ($signal) {
|
||||
case 'IDF_Project::created':
|
||||
$plug->processSvnCreate($params['project']);
|
||||
break;
|
||||
case 'IDF_Project::membershipsUpdated':
|
||||
$plug->processSyncAuthz($params['project']);
|
||||
break;
|
||||
case 'Pluf_User::passwordUpdated':
|
||||
$plug->processSyncPasswd($params['user']);
|
||||
break;
|
||||
case 'IDF_Project::preDelete':
|
||||
$plug->processSvnDelete($params['project']);
|
||||
break;
|
||||
case 'svnpostcommit.php::run':
|
||||
$plug->processSvnUpdateTimeline($params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run svnadmin command to create the corresponding Subversion
|
||||
* repository.
|
||||
*
|
||||
* @param IDF_Project
|
||||
* @return bool Success
|
||||
*/
|
||||
function processSvnCreate($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'svn') {
|
||||
return false;
|
||||
}
|
||||
$shortname = $project->shortname;
|
||||
if (false===($svn_path=Pluf::f('idf_plugin_syncsvn_svn_path',false))) {
|
||||
throw new Pluf_Exception_SettingError("'idf_plugin_syncsvn_svn_path' must be defined in your configuration file.");
|
||||
}
|
||||
if (file_exists($svn_path.'/'.$shortname)) {
|
||||
throw new Exception(sprintf(__('The repository %s already exists.'),
|
||||
$svn_path.'/'.$shortname));
|
||||
}
|
||||
$return = 0;
|
||||
$output = array();
|
||||
$cmd = sprintf(Pluf::f('svnadmin_path', 'svnadmin').' create %s',
|
||||
escapeshellarg($svn_path.'/'.$shortname));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
$ll = exec($cmd, $output, $return);
|
||||
if ($return != 0) {
|
||||
Pluf_Log::error(array('IDF_Plugin_SyncSvn::processSvnCreate',
|
||||
'Error',
|
||||
array('path' => $svn_path.'/'.$shortname,
|
||||
'output' => $output)));
|
||||
return;
|
||||
}
|
||||
$p = realpath(dirname(__FILE__).'/../../../scripts/svn-post-commit');
|
||||
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').'ln -s %s %s',
|
||||
escapeshellarg($p),
|
||||
escapeshellarg($svn_path.'/'.$shortname.'/hooks/post-commit')),
|
||||
$out, $res);
|
||||
if ($res != 0) {
|
||||
Pluf_Log::warn(array('IDF_Plugin_SyncSvn::processSvnCreate',
|
||||
'post-commit hook creation error.',
|
||||
$svn_path.'/'.$shortname.'/hooks/post-commit'));
|
||||
return;
|
||||
}
|
||||
$p = realpath(dirname(__FILE__).'/../../../scripts/svn-post-revprop-change');
|
||||
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').'ln -s %s %s',
|
||||
escapeshellarg($p),
|
||||
escapeshellarg($svn_path.'/'.$shortname.'/hooks/post-revprop-change')),
|
||||
$out, $res);
|
||||
if ($res != 0) {
|
||||
Pluf_Log::warn(array('IDF_Plugin_SyncSvn::processSvnCreate',
|
||||
'post-revprop-change hook creation error.',
|
||||
$svn_path.'/'.$shortname.'/hooks/post-revprop-change'));
|
||||
return;
|
||||
}
|
||||
|
||||
return ($return == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the project from the drive and update the access rights.
|
||||
*
|
||||
* @param IDF_Project
|
||||
* @return bool Success
|
||||
*/
|
||||
function processSvnDelete($project)
|
||||
{
|
||||
if (!Pluf::f('idf_plugin_syncsvn_remove_orphans', false)) {
|
||||
return;
|
||||
}
|
||||
if ($project->getConf()->getVal('scm') != 'svn') {
|
||||
return false;
|
||||
}
|
||||
$this->SyncAccess($project); // exclude $project
|
||||
$shortname = $project->shortname;
|
||||
if (false===($svn_path=Pluf::f('idf_plugin_syncsvn_svn_path',false))) {
|
||||
throw new Pluf_Exception_SettingError("'idf_plugin_syncsvn_svn_path' must be defined in your configuration file.");
|
||||
}
|
||||
if (file_exists($svn_path.'/'.$shortname)) {
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'rm -rf '.$svn_path.'/'.$shortname;
|
||||
exec($cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise an user's password.
|
||||
*
|
||||
* @param Pluf_User
|
||||
*/
|
||||
function processSyncPasswd($user)
|
||||
{
|
||||
$passwd_file = Pluf::f('idf_plugin_syncsvn_passwd_file');
|
||||
if (!file_exists($passwd_file) or !is_writable($passwd_file)) {
|
||||
return false;
|
||||
}
|
||||
$ht = new File_Passwd_Authbasic($passwd_file);
|
||||
$ht->load();
|
||||
$ht->setMode('plain');
|
||||
//$ht->setMode(FILE_PASSWD_SHA);
|
||||
if ($ht->userExists($user->login)) {
|
||||
$ht->changePasswd($user->login, '{SHA}' . $this->getSvnPass($user));
|
||||
} else {
|
||||
$ht->addUser($user->login, '{SHA}' . $this->getSvnPass($user));
|
||||
}
|
||||
$ht->save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the authz file and the passwd file for the project.
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
function processSyncAuthz($project)
|
||||
{
|
||||
$this->SyncAccess();
|
||||
$this->generateProjectPasswd($project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository password for the user
|
||||
*/
|
||||
function getSvnPass($user){
|
||||
//if (isset($_POST["password"])) return $_POST["password"];
|
||||
//return $_POST["password"];
|
||||
//return substr(sha1($user->password.Pluf::f('secret_key')), 0, 8);
|
||||
return $user->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a particular project: update all passwd information
|
||||
*/
|
||||
function generateProjectPasswd($project)
|
||||
{
|
||||
$passwd_file = Pluf::f('idf_plugin_syncsvn_passwd_file');
|
||||
if (!file_exists($passwd_file) or !is_writable($passwd_file)) {
|
||||
return false;
|
||||
}
|
||||
$ht = new File_Passwd_Authbasic($passwd_file);
|
||||
$ht->setMode('plain');
|
||||
$ht->load();
|
||||
$mem = $project->getMembershipData();
|
||||
$members = array_merge((array)$mem['members'], (array)$mem['owners'],
|
||||
(array)$mem['authorized']);
|
||||
foreach($members as $user) {
|
||||
if ($ht->userExists($user->login)) {
|
||||
|
||||
$ht->changePasswd($user->login, '{SHA}' . $this->getSvnPass($user));
|
||||
} else {
|
||||
$ht->addUser($user->login, '{SHA}' . $this->getSvnPass($user));
|
||||
}
|
||||
}
|
||||
$ht->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the dav_svn.authz file
|
||||
*
|
||||
* We rebuild the complete file each time. This is just to be sure
|
||||
* not to bork the rights when trying to just edit part of the
|
||||
* file.
|
||||
*
|
||||
* @param IDF_Project Possibly exclude a project (null)
|
||||
*/
|
||||
function SyncAccess($exclude=null)
|
||||
{
|
||||
$authz_file = Pluf::f('idf_plugin_syncsvn_authz_file');
|
||||
$access_owners = Pluf::f('idf_plugin_syncsvn_access_owners', 'rw');
|
||||
$access_members = Pluf::f('idf_plugin_syncsvn_access_members', 'rw');
|
||||
$access_extra = Pluf::f('idf_plugin_syncsvn_access_extra', 'r');
|
||||
$access_public = Pluf::f('idf_plugin_syncsvn_access_public', 'r');
|
||||
$access_public_priv = Pluf::f('idf_plugin_syncsvn_access_private', '');
|
||||
if (!file_exists($authz_file) or !is_writable($authz_file)) {
|
||||
return false;
|
||||
}
|
||||
$fcontent = '';
|
||||
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||
if ($exclude and $exclude->id == $project->id) {
|
||||
continue;
|
||||
}
|
||||
$conf = new IDF_Conf();
|
||||
$conf->setProject($project);
|
||||
if ($conf->getVal('scm') != 'svn' or
|
||||
strlen($conf->getVal('svn_remote_url')) > 0) {
|
||||
continue;
|
||||
}
|
||||
$mem = $project->getMembershipData();
|
||||
// [shortname:/]
|
||||
$fcontent .= '['.$project->shortname.':/]'."\n";
|
||||
foreach ($mem['owners'] as $v) {
|
||||
$fcontent .= $v->login.' = '.$access_owners."\n";
|
||||
}
|
||||
foreach ($mem['members'] as $v) {
|
||||
$fcontent .= $v->login.' = '.$access_members."\n";
|
||||
}
|
||||
// access for all users
|
||||
if ($project->private == true) {
|
||||
foreach ($mem['authorized'] as $v) {
|
||||
$fcontent .= $v->login.' = '.$access_extra."\n";
|
||||
}
|
||||
$fcontent .= '* = '.$access_public_priv."\n";
|
||||
} else {
|
||||
$fcontent .= '* = '.$access_public."\n";
|
||||
}
|
||||
$fcontent .= "\n";
|
||||
}
|
||||
file_put_contents($authz_file, $fcontent, LOCK_EX);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timeline in post commit.
|
||||
*
|
||||
*/
|
||||
public function processSvnUpdateTimeline($params)
|
||||
{
|
||||
$pname = basename($params['repo_dir']);
|
||||
try {
|
||||
$project = IDF_Project::getOr404($pname);
|
||||
} catch (Pluf_HTTP_Error404 $e) {
|
||||
Pluf_Log::event(array('IDF_Plugin_SyncSvn::processSvnUpdateTimeline', 'Project not found.', array($pname, $params)));
|
||||
return false; // Project not found
|
||||
}
|
||||
// Now we have the project and can update the timeline
|
||||
Pluf_Log::debug(array('IDF_Plugin_SyncGit::processSvnUpdateTimeline', 'Project found', $pname, $project->id));
|
||||
IDF_Scm::syncTimeline($project, true);
|
||||
Pluf_Log::event(array('IDF_Plugin_SyncGit::processSvnUpdateTimeline', 'sync', array($pname, $project->id)));
|
||||
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user