Indefero

Indefero Git Source Tree

Root/src/IDF/Plugin/SyncMonotone.php

1<?php
2/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3/*
4# ***** BEGIN LICENSE BLOCK *****
5# This file is part of InDefero, an open source project management application.
6# Copyright (C) 2008-2011 Céondo Ltd and contributors.
7#
8# InDefero is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# InDefero is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21#
22# ***** END LICENSE BLOCK ***** */
23
24/**
25 * This classes is a plugin which allows to synchronise access rights
26 * between indefero and monotone usher setups.
27 */
28class IDF_Plugin_SyncMonotone
29{
30 private $old_err_rep = 0;
31
32 public function __construct()
33 {
34 $this->old_err_rep = error_reporting(0);
35 }
36
37 public function __destruct()
38 {
39 error_reporting($this->old_err_rep);
40 }
41
42 /**
43 * Entry point of the plugin.
44 */
45 static public function entry($signal, &$params)
46 {
47 $plug = new IDF_Plugin_SyncMonotone();
48 switch ($signal) {
49 case 'IDF_Project::created':
50 $plug->processProjectCreate($params['project']);
51 break;
52 case 'IDF_Project::membershipsUpdated':
53 $plug->processMembershipsUpdated($params['project']);
54 break;
55 case 'IDF_Project::preDelete':
56 $plug->processProjectDelete($params['project']);
57 break;
58 case 'IDF_Key::postSave':
59 $plug->processKeyCreate($params['key']);
60 break;
61 case 'IDF_Key::preDelete':
62 $plug->processKeyDelete($params['key']);
63 break;
64 case 'mtnpostpush.php::run':
65 $plug->processSyncTimeline($params['project']);
66 break;
67 }
68 }
69
70 /**
71 * Initial steps to setup a new monotone project:
72 *
73 * 1) run mtn db init to initialize a new database underknees
74 * 'mtn_repositories'
75 * 2) create a new server key in the same directory
76 * 3) create a new client key for IDF and store it in the project conf
77 * 4) setup the configuration
78 * 5) add the database as new local server in the usher configuration
79 * 6) reload the running usher instance so it acknowledges the new server
80 *
81 * The initial right setup happens in processMembershipsUpdated()
82 *
83 * @param IDF_Project
84 */
85 function processProjectCreate($project)
86 {
87 if ($project->getConf()->getVal('scm') != 'mtn') {
88 return;
89 }
90
91 if (Pluf::f('mtn_db_access', 'local') == 'local') {
92 return;
93 }
94
95 // This guard cleans up on any kind of error, and here is how it works:
96 // As long as the guard is not committed, it keeps a reference to
97 // the given project. When the guard is destroyed and the reference
98 // is still present, it deletes the object. The deletion indirectly
99 // also calls into this plugin again, as the project delete hook
100 // will be called, that removes any changes we've made during the
101 // process.
102 $projectGuard = new IDF_Plugin_SyncMonotone_ModelGuard($project);
103
104 $projecttempl = Pluf::f('mtn_repositories', false);
105 if ($projecttempl === false) {
106 $this->_diagnoseProblem(
107 __('"mtn_repositories" must be defined in your configuration file')
108 );
109 }
110
111 $usher_config = Pluf::f('mtn_usher_conf', false);
112 if (!$usher_config || !is_writable($usher_config)) {
113 $this->_diagnoseProblem(
114 __('"mtn_usher_conf" does not exist or is not writable')
115 );
116 }
117
118 $mtnpostpush = realpath(dirname(__FILE__) . '/../../../scripts/mtn-post-push');
119 if (!file_exists($mtnpostpush)) {
120 $this->_diagnoseProblem(sprintf(
121 __('Could not find mtn-post-push script "%s"'), $mtnpostpush
122 ));
123 }
124
125 // check some static configuration files
126 $confdir = Pluf::f('mtn_confdir', false);
127 if ($confdir === false) {
128 $confdir = dirname(__FILE__).'/SyncMonotone/';
129 }
130 $confdir_contents = array(
131 'monotonerc.in',
132 'remote-automate-permissions.in',
133 'hooks.d/',
134 'hooks.d/indefero_authorize_remote_automate.lua',
135 'hooks.d/indefero_post_push.conf.in',
136 'hooks.d/indefero_post_push.lua',
137 );
138 // enable remote command execution of read-only commands
139 // only for public projects
140 if (!$project->private) {
141 // this is linked and not copied to be able to update
142 // the list of read-only commands on upgrades
143 $confdir_contents[] = 'hooks.d/indefero_authorize_remote_automate.conf';
144 }
145
146 // check whether we should handle additional files in the config directory
147 $confdir_extra_contents = Pluf::f('mtn_confdir_extra', false);
148 if ($confdir_extra_contents !== false) {
149 $confdir_contents =
150 array_merge($confdir_contents, $confdir_extra_contents);
151 }
152 foreach ($confdir_contents as $content) {
153 if (!file_exists($confdir.$content)) {
154 $this->_diagnoseProblem(sprintf(
155 __('The configuration file "%s" is missing'), $content
156 ));
157 }
158 }
159
160 $shortname = $project->shortname;
161 $projectpath = sprintf($projecttempl, $shortname);
162 if (file_exists($projectpath)) {
163 $this->_diagnoseProblem(sprintf(
164 __('The project path "%s" already exists'), $projectpath
165 ));
166 }
167
168 if (!@mkdir($projectpath)) {
169 $this->_diagnoseProblem(sprintf(
170 __('The project path "%s" could not be created'),
171 $projectpath
172 ));
173 }
174
175 //
176 // step 1) create a new database
177 //
178 $dbfile = $projectpath.'/database.mtn';
179 $cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
180 $this->_mtn_exec($cmd);
181
182 //
183 // step 2) create a server key
184 //
185 // try to parse the key's domain part from the remote_url's host
186 // name, otherwise fall back to the configured Apache server name
187 $server = $_SERVER['SERVER_NAME'];
188 $remote_url = Pluf::f('mtn_remote_url');
189 if (($parsed = parse_url($remote_url)) !== false &&
190 !empty($parsed['host'])) {
191 $server = $parsed['host'];
192 }
193
194 $serverkey = $shortname.'-server@'.$server;
195 $cmd = sprintf('au generate_key --confdir=%s %s ""',
196 escapeshellarg($projectpath),
197 escapeshellarg($serverkey)
198 );
199 $this->_mtn_exec($cmd);
200
201 //
202 // step 3) create a client key, and save it in IDF
203 //
204 $keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
205 if (!file_exists($keydir)) {
206 if (!@mkdir($keydir)) {
207 $this->_diagnoseProblem(sprintf(
208 __('The key directory "%s" could not be created'),
209 $keydir
210 ));
211 }
212 }
213
214 $clientkey_name = $shortname.'-client@'.$server;
215 $cmd = sprintf('au generate_key --keydir=%s %s ""',
216 escapeshellarg($keydir),
217 escapeshellarg($clientkey_name)
218 );
219 $keyinfo = $this->_mtn_exec($cmd);
220
221 $parsed_keyinfo = array();
222 try {
223 $parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo);
224 }
225 catch (Exception $e) {
226 $this->_diagnoseProblem(sprintf(
227 __('Could not parse key information: %s'), $e->getMessage()
228 ));
229 }
230
231 $clientkey_hash = $parsed_keyinfo[0][1]['hash'];
232 $clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash;
233 $clientkey_data = file_get_contents($clientkey_file);
234
235 $project->getConf()->setVal('mtn_client_key_name', $clientkey_name);
236 $project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash);
237 $project->getConf()->setVal('mtn_client_key_data', $clientkey_data);
238
239 // add the public client key to the server
240 $cmd = sprintf('au get_public_key --keydir=%s %s',
241 escapeshellarg($keydir),
242 escapeshellarg($clientkey_hash)
243 );
244 $clientkey_pubdata = $this->_mtn_exec($cmd);
245
246 $cmd = sprintf('au put_public_key --db=%s %s',
247 escapeshellarg($dbfile),
248 escapeshellarg($clientkey_pubdata)
249 );
250 $this->_mtn_exec($cmd);
251
252 //
253 // step 4) setup the configuration
254 //
255
256 // we assume that all confdir entries ending with a slash mean a
257 // directory that has to be created, that all files ending on ".in"
258 // have to be processed and copied in place and that all other files
259 // just need to be symlinked from the original location
260 foreach ($confdir_contents as $content) {
261 $filepath = $projectpath.'/'.$content;
262 if (substr($content, -1) == '/') {
263 if (!@mkdir($filepath)) {
264 $this->_diagnoseProblem(sprintf(
265 __('Could not create configuration directory "%s"'),
266 $filepath
267 ));
268 }
269 continue;
270 }
271
272 if (substr($content, -3) != '.in') {
273 if (!@symlink($confdir.$content, $filepath)) {
274 $this->_diagnoseProblem(sprintf(
275 __('Could not create symlink for configuration file "%s"'),
276 $filepath
277 ));
278 }
279 continue;
280 }
281
282 $filecontents = file_get_contents($confdir.'/'.$content);
283 $filecontents = str_replace(
284 array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
285 array($mtnpostpush, $shortname, $clientkey_hash),
286 $filecontents
287 );
288
289 // remove the .in
290 $filepath = substr($filepath, 0, -3);
291 if (@file_put_contents($filepath, $filecontents, LOCK_EX) === false) {
292 $this->_diagnoseProblem(sprintf(
293 __('Could not write configuration file "%s"'),
294 $filepath
295 ));
296 }
297 }
298
299 //
300 // step 5) read in and append the usher config with the new server
301 //
302 $usher_rc = file_get_contents($usher_config);
303 $parsed_config = array();
304 try {
305 $parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
306 }
307 catch (Exception $e) {
308 $this->_diagnoseProblem(sprintf(
309 __('Could not parse usher configuration in "%1$s": %2$s'),
310 $usher_config, $e->getMessage()
311 ));
312 }
313
314 // ensure we haven't configured a server with this name already
315 foreach ($parsed_config as $stanzas) {
316 foreach ($stanzas as $stanza_line) {
317 if ($stanza_line['key'] == 'server' &&
318 $stanza_line['values'][0] == $shortname) {
319 $this->_diagnoseProblem(sprintf(
320 __('usher configuration already contains a server '.
321 'entry named "%s"'),
322 $shortname
323 ));
324 }
325 }
326 }
327
328 $new_server = array(
329 array('key' => 'server', 'values' => array($shortname)),
330 array('key' => 'local', 'values' => array(
331 '--confdir', $projectpath,
332 '-d', $dbfile,
333 '--timestamps',
334 '--ticker=dot'
335 )),
336 );
337
338 $parsed_config[] = $new_server;
339 $usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
340
341 // FIXME: more sanity - what happens on failing writes? we do not
342 // have a backup copy of usher.conf around...
343 if (@file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
344 $this->_diagnoseProblem(sprintf(
345 __('Could not write usher configuration file "%s"'),
346 $usher_config
347 ));
348 }
349
350 //
351 // step 6) reload usher to pick up the new configuration
352 //
353 IDF_Scm_Monotone_Usher::reload();
354
355 // commit the guard, so the newly created project is not deleted
356 $projectGuard->commit();
357 }
358
359 /**
360 * Updates the read / write permissions for the monotone database
361 *
362 * @param IDF_Project
363 */
364 public function processMembershipsUpdated($project)
365 {
366 if ($project->getConf()->getVal('scm') != 'mtn') {
367 return;
368 }
369
370 if (Pluf::f('mtn_db_access', 'local') == 'local') {
371 return;
372 }
373
374 $mtn = IDF_Scm_Monotone::factory($project);
375 $stdio = $mtn->getStdio();
376
377 $projectpath = $this->_get_project_path($project);
378 $auth_ids = $this->_get_authorized_user_ids($project);
379 $key_ids = array();
380 foreach ($auth_ids as $auth_id) {
381 $sql = new Pluf_SQL('user=%s', array($auth_id));
382 $keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen()));
383 foreach ($keys as $key) {
384 if ($key->getType() != 'mtn')
385 continue;
386 $stdio->exec(array('put_public_key', $key->content));
387 $key_ids[] = $key->getMtnId();
388 }
389 }
390
391 $write_permissions = implode("\n", $key_ids);
392 $rcfile = $projectpath.'/write-permissions';
393 if (@file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) {
394 $this->_diagnoseProblem(sprintf(
395 __('Could not write write-permissions file "%s"'),
396 $rcfile
397 ));
398 }
399
400 if ($project->private) {
401 $stanza = array(
402 array('key' => 'pattern', 'values' => array('*')),
403 );
404 foreach ($key_ids as $key_id)
405 {
406 $stanza[] = array('key' => 'allow', 'values' => array($key_id));
407 }
408 }
409 else {
410 $stanza = array(
411 array('key' => 'pattern', 'values' => array('*')),
412 array('key' => 'allow', 'values' => array('*')),
413 );
414 }
415
416 $read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza));
417 $rcfile = $projectpath.'/read-permissions';
418 if (@file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) {
419 $this->_diagnoseProblem(sprintf(
420 __('Could not write read-permissions file "%s"'),
421 $rcfile
422 ));
423 }
424
425 // link / unlink the read-only automate permissions for the project
426 $confdir = Pluf::f('mtn_confdir', false);
427 if ($confdir === false) {
428 $confdir = dirname(__FILE__).'/SyncMonotone/';
429 }
430 $file = 'hooks.d/indefero_authorize_remote_automate.conf';
431 $projectfile = $projectpath.'/'.$file;
432 $templatefile = $confdir.'/'.$file;
433
434 $serverRestartRequired = false;
435 if ($project->private && file_exists($projectfile) && is_link($projectfile)) {
436 if (!@unlink($projectfile)) {
437 $this->_diagnoseProblem(sprintf(
438 __('Could not remove symlink "%s"'), $projectfile
439 ));
440 }
441 $serverRestartRequired = true;
442 } else
443 if (!$project->private && !file_exists($projectfile)) {
444 if (!@symlink($templatefile, $projectfile)) {
445 $this->_diagnoseProblem(sprintf(
446 __('Could not create symlink "%s"'), $projectfile
447 ));
448 }
449 $serverRestartRequired = true;
450 }
451
452 if ($serverRestartRequired) {
453 // FIXME: we should actually use stopServer() here, but this
454 // seems to be ignored when the server should be started
455 // again immediately afterwards
456 IDF_Scm_Monotone_Usher::killServer($project->shortname);
457 // give usher some time to cool down, otherwise it might hang
458 // (see https://code.monotone.ca/p/contrib/issues/175/)
459 sleep(2);
460 IDF_Scm_Monotone_Usher::startServer($project->shortname);
461 }
462 }
463
464 /**
465 * Clean up after a mtn project was deleted
466 *
467 * @param IDF_Project
468 */
469 public function processProjectDelete($project)
470 {
471 if ($project->getConf()->getVal('scm') != 'mtn') {
472 return;
473 }
474
475 if (Pluf::f('mtn_db_access', 'local') == 'local') {
476 return;
477 }
478
479 $usher_config = Pluf::f('mtn_usher_conf', false);
480 if (!$usher_config || !is_writable($usher_config)) {
481 $this->_diagnoseProblem(
482 __('"mtn_usher_conf" does not exist or is not writable')
483 );
484 }
485
486 $shortname = $project->shortname;
487 IDF_Scm_Monotone_Usher::killServer($shortname);
488
489 $projecttempl = Pluf::f('mtn_repositories', false);
490 if ($projecttempl === false) {
491 $this->_diagnoseProblem(
492 __('"mtn_repositories" must be defined in your configuration file')
493 );
494 }
495
496 $projectpath = sprintf($projecttempl, $shortname);
497 if (file_exists($projectpath)) {
498 if (!$this->_delete_recursive($projectpath)) {
499 $this->_diagnoseProblem(sprintf(
500 __('One or more paths underneath %s could not be deleted'), $projectpath
501 ));
502 }
503 }
504
505 $keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
506 $keyname = $project->getConf()->getVal('mtn_client_key_name', false);
507 $keyhash = $project->getConf()->getVal('mtn_client_key_hash', false);
508 if ($keyname && $keyhash &&
509 file_exists($keydir .'/'. $keyname . '.' . $keyhash)) {
510 if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) {
511 $this->_diagnoseProblem(sprintf(
512 __('Could not delete client private key "%s"'),
513 $keyname
514 ));
515 }
516 }
517
518 $usher_rc = file_get_contents($usher_config);
519 $parsed_config = array();
520 try {
521 $parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
522 }
523 catch (Exception $e) {
524 $this->_diagnoseProblem(sprintf(
525 __('Could not parse usher configuration in "%1$s": %2$s'),
526 $usher_config, $e->getMessage()
527 ));
528 }
529
530 foreach ($parsed_config as $idx => $stanzas) {
531 foreach ($stanzas as $stanza_line) {
532 if ($stanza_line['key'] == 'server' &&
533 $stanza_line['values'][0] == $shortname) {
534 unset($parsed_config[$idx]);
535 break;
536 }
537 }
538 }
539
540 $usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
541
542 // FIXME: more sanity - what happens on failing writes? we do not
543 // have a backup copy of usher.conf around...
544 if (@file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
545 $this->_diagnoseProblem(sprintf(
546 __('Could not write usher configuration file "%s"'),
547 $usher_config
548 ));
549 }
550
551 IDF_Scm_Monotone_Usher::reload();
552 }
553
554 /**
555 * Adds the (monotone) key to all monotone projects of this forge
556 * where the user of the key has write access to
557 */
558 public function processKeyCreate($key)
559 {
560 if ($key->getType() != 'mtn') {
561 return;
562 }
563
564 if (Pluf::f('mtn_db_access', 'local') == 'local') {
565 return;
566 }
567
568 $keyGuard = new IDF_Plugin_SyncMonotone_ModelGuard($key);
569
570 foreach (Pluf::factory('IDF_Project')->getList() as $project) {
571 $conf = new IDF_Conf();
572 $conf->setProject($project);
573 $scm = $conf->getVal('scm', 'mtn');
574 if ($scm != 'mtn')
575 continue;
576
577 $projectpath = $this->_get_project_path($project);
578 $auth_ids = $this->_get_authorized_user_ids($project);
579 if (!in_array($key->user, $auth_ids))
580 continue;
581
582 $mtn_key_id = $key->getMtnId();
583
584 // if the project is not defined as private, all people have
585 // read access already, so we don't need to write anything
586 // and we currently do not check if read-permissions really
587 // contains
588 // pattern "*"
589 // allow "*"
590 // which is the default for non-private projects
591 if ($project->private == true) {
592 $read_perms = file_get_contents($projectpath.'/read-permissions');
593 $parsed_read_perms = array();
594 try {
595 $parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
596 }
597 catch (Exception $e) {
598 $this->_diagnoseProblem(sprintf(
599 __('Could not parse read-permissions for project "%1$s": %2$s'),
600 $shortname, $e->getMessage()
601 ));
602 }
603
604 $wildcard_section = null;
605 for ($i=0; $i<count($parsed_read_perms); ++$i) {
606 foreach ($parsed_read_perms[$i] as $stanza_line) {
607 if ($stanza_line['key'] == 'pattern' &&
608 $stanza_line['values'][0] == '*') {
609 $wildcard_section =& $parsed_read_perms[$i];
610 break;
611 }
612 }
613 }
614
615 if ($wildcard_section == null)
616 {
617 $wildcard_section = array(
618 array('key' => 'pattern', 'values' => array('*'))
619 );
620 $parsed_read_perms[] =& $wildcard_section;
621 }
622
623 $key_found = false;
624 foreach ($wildcard_section as $line)
625 {
626 if ($line['key'] == 'allow' && $line['values'][0] == $mtn_key_id) {
627 $key_found = true;
628 break;
629 }
630 }
631
632 if (!$key_found) {
633 $wildcard_section[] = array(
634 'key' => 'allow', 'values' => array($mtn_key_id)
635 );
636 }
637
638 $read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
639
640 if (@file_put_contents($projectpath.'/read-permissions',
641 $read_perms, LOCK_EX) === false) {
642 $this->_diagnoseProblem(sprintf(
643 __('Could not write read-permissions for project "%s"'),
644 $shortname
645 ));
646 }
647 }
648
649 $write_perms = file_get_contents($projectpath.'/write-permissions');
650 $lines = preg_split("/(\n|\r\n)/", $write_perms, -1, PREG_SPLIT_NO_EMPTY);
651 if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) {
652 $lines[] = $mtn_key_id;
653 }
654 if (@file_put_contents($projectpath.'/write-permissions',
655 implode("\n", $lines) . "\n", LOCK_EX) === false) {
656 $this->_diagnoseProblem(sprintf(
657 __('Could not write write-permissions file for project "%s"'),
658 $shortname
659 ));
660 }
661
662 $mtn = IDF_Scm_Monotone::factory($project);
663 $stdio = $mtn->getStdio();
664 $stdio->exec(array('put_public_key', $key->content));
665 }
666
667 $keyGuard->commit();
668 }
669
670 /**
671 * Removes the (monotone) key from all monotone projects of this forge
672 * where the user of the key has write access to
673 */
674 public function processKeyDelete($key)
675 {
676 try {
677 if ($key->getType() != 'mtn') {
678 return;
679 }
680 } catch (Exception $e) {
681 // bad key type, skip it.
682 return;
683 }
684
685 if (Pluf::f('mtn_db_access', 'local') == 'local') {
686 return;
687 }
688
689 foreach (Pluf::factory('IDF_Project')->getList() as $project) {
690 $conf = new IDF_Conf();
691 $conf->setProject($project);
692 $scm = $conf->getVal('scm', 'mtn');
693 if ($scm != 'mtn')
694 continue;
695
696 $projectpath = $this->_get_project_path($project);
697 $auth_ids = $this->_get_authorized_user_ids($project);
698 if (!in_array($key->user, $auth_ids))
699 continue;
700
701 $mtn_key_id = $key->getMtnId();
702
703 // if the project is not defined as private, all people have
704 // read access already, so we don't need to write anything
705 // and we currently do not check if read-permissions really
706 // contains
707 // pattern "*"
708 // allow "*"
709 // which is the default for non-private projects
710 if ($project->private) {
711 $read_perms = file_get_contents($projectpath.'/read-permissions');
712 $parsed_read_perms = array();
713 try {
714 $parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
715 }
716 catch (Exception $e) {
717 $this->_diagnoseProblem(sprintf(
718 __('Could not parse read-permissions for project "%1$s": %2$s'),
719 $shortname, $e->getMessage()
720 ));
721 }
722
723 // while we add new keys only to an existing wild-card entry
724 // we remove dropped keys from all sections since the key
725 // should be simply unavailable for all of them
726 for ($h=0; $h<count($parsed_read_perms); ++$h) {
727 for ($i=0; $i<count($parsed_read_perms[$h]); ++$i) {
728 if ($parsed_read_perms[$h][$i]['key'] == 'allow' &&
729 $parsed_read_perms[$h][$i]['values'][0] == $mtn_key_id) {
730 unset($parsed_read_perms[$h][$i]);
731 continue;
732 }
733 }
734 }
735
736 $read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
737
738 if (@file_put_contents($projectpath.'/read-permissions',
739 $read_perms, LOCK_EX) === false) {
740 $this->_diagnoseProblem(sprintf(
741 __('Could not write read-permissions for project "%s"'),
742 $shortname
743 ));
744 }
745 }
746
747 $write_perms = file_get_contents($projectpath.'/write-permissions');
748 $lines = preg_split("/(\n|\r\n)/", $write_perms, -1, PREG_SPLIT_NO_EMPTY);
749 for ($i=0; $i<count($lines); ++$i) {
750 if ($lines[$i] == $mtn_key_id) {
751 unset($lines[$i]);
752 // the key should actually only exist once in the
753 // file, but we're paranoid
754 continue;
755 }
756 }
757 if (@file_put_contents($projectpath.'/write-permissions',
758 implode("\n", $lines) . "\n", LOCK_EX) === false) {
759 $this->_diagnoseProblem(sprintf(
760 __('Could not write write-permissions file for project "%s"'),
761 $shortname
762 ));
763 }
764
765 $mtn = IDF_Scm_Monotone::factory($project);
766 $stdio = $mtn->getStdio();
767 // if the public key did not sign any revisions, drop it from
768 // the database as well
769 try {
770 if (strlen($stdio->exec(array('select', 'k:' . $mtn_key_id))) == 0) {
771 $stdio->exec(array('drop_public_key', $mtn_key_id));
772 }
773 } catch (IDF_Scm_Exception $e) {
774 if (strpos($e->getMessage(), 'there is no key named') === false)
775 throw $e;
776 }
777 }
778 }
779
780 /**
781 * Update the timeline after a push
782 *
783 */
784 public function processSyncTimeline($project_name)
785 {
786 try {
787 $project = IDF_Project::getOr404($project_name);
788 } catch (Pluf_HTTP_Error404 $e) {
789 Pluf_Log::event(array(
790 'IDF_Plugin_SyncMonotone::processSyncTimeline',
791 'Project not found.',
792 array($project_name, $params)
793 ));
794 return false; // Project not found
795 }
796
797 Pluf_Log::debug(array(
798 'IDF_Plugin_SyncMonotone::processSyncTimeline',
799 'Project found', $project_name, $project->id
800 ));
801 IDF_Scm::syncTimeline($project, true);
802 Pluf_Log::event(array(
803 'IDF_Plugin_SyncMonotone::processSyncTimeline',
804 'sync', array($project_name, $project->id)
805 ));
806 }
807
808 private function _get_project_path($project)
809 {
810 $projecttempl = Pluf::f('mtn_repositories', false);
811 if ($projecttempl === false) {
812 $this->_diagnoseProblem(
813 __('"mtn_repositories" must be defined in your configuration file.')
814 );
815 }
816
817 $projectpath = sprintf($projecttempl, $project->shortname);
818 if (!file_exists($projectpath)) {
819 $this->_diagnoseProblem(sprintf(
820 __('The project path %s does not exists.'), $projectpath
821 ));
822 }
823 return $projectpath;
824 }
825
826 private function _mtn_exec($cmd)
827 {
828 $fullcmd = sprintf('%s %s %s',
829 Pluf::f('idf_exec_cmd_prefix', ''),
830 Pluf::f('mtn_path', 'mtn'),
831 $cmd
832 );
833
834 $output = $return = null;
835 exec($fullcmd, $output, $return);
836 if ($return != 0) {
837 $this->_diagnoseProblem(sprintf(
838 __('The command "%s" could not be executed.'), $cmd
839 ));
840 }
841 return implode("\n", $output);
842 }
843
844 private function _get_authorized_user_ids($project)
845 {
846 $mem = $project->getMembershipData();
847 $members = array_merge((array)$mem['members'],
848 (array)$mem['owners'],
849 (array)$mem['authorized']);
850 $userids = array();
851 foreach ($members as $member) {
852 $userids[] = $member->id;
853 }
854 return $userids;
855 }
856
857 private function _delete_recursive($path)
858 {
859 if (is_file($path) || is_link($path)) {
860 return @unlink($path);
861 }
862
863 if (is_dir($path)) {
864 $scan = glob(rtrim($path, '/') . '/*');
865 $status = 0;
866 foreach ($scan as $subpath) {
867 $status |= $this->_delete_recursive($subpath);
868 }
869 $status |= @rmdir($path);
870 return $status;
871 }
872 }
873
874 private function _diagnoseProblem($msg)
875 {
876 $system_err = error_get_last();
877 if (!empty($system_err)) {
878 $msg .= ': '.$system_err['message'];
879 }
880
881 error_reporting($this->old_err_rep);
882 throw new IDF_Scm_Exception($msg);
883 }
884}
885
886/**
887 * A simple helper class that deletes the model instance if
888 * it is not committed
889 */
890class IDF_Plugin_SyncMonotone_ModelGuard
891{
892 private $model;
893
894 public function __construct(Pluf_Model $m)
895 {
896 $this->model = $m;
897 }
898
899 public function __destruct()
900 {
901 if ($this->model == null)
902 return;
903 $this->model->delete();
904 }
905
906 public function commit()
907 {
908 $this->model = null;
909 }
910}
911
912

Archive Download this file