/[drupal]/contributions/modules/hosting/hosting.module
ViewVC logotype

Contents of /contributions/modules/hosting/hosting.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.123 - (show annotations) (download) (as text)
Sun Oct 25 01:05:30 2009 UTC (5 weeks ago) by mig5
Branch: MAIN
Changes since 1.122: +16 -10 lines
File MIME type: text/x-php
#613402 spotted by adrinux - update the crontab replacement entry from pre-0.3 style crontab
1 <?php
2 // $Id: hosting.module,v 1.122 2009/10/23 11:42:18 mig5 Exp $
3
4 /**
5 * @file Hosting module
6 *
7 * Contains just about all the interface magic of hostmaster.
8 */
9
10 /**
11 * Not split for performance reasons. Just to keep code together.
12 */
13
14 include_once('hosting.inc');
15 include_once('hosting_help.inc');
16 include_once('hosting.queues.inc');
17 include_once('hosting.features.inc');
18
19 /**
20 * Implementation of hook_menu()
21 */
22 function hosting_menu() {
23 global $user;
24 $items = array();
25
26
27 $items['hosting/disabled'] = array(
28 'title' => 'Site disabled',
29 'page callback' => 'hosting_disabled_site',
30 'access arguments' => array('access content'),
31 'type' => MENU_CALLBACK
32 );
33
34 $items['hosting/js'] = array(
35 'title' => t('ahah callback'),
36 'page callback' => 'hosting_js_page',
37 'access arguments' => array('access content'),
38 'type' => MENU_CALLBACK
39 );
40
41 $items['hosting/maintenance'] = array(
42 'title' => 'Site maintenance',
43 'page callback' => 'hosting_site_maintenance',
44 'access arguments' => array('access content'),
45 'type' => MENU_CALLBACK
46 );
47
48
49 $items['admin/help/provision/requirements'] = array(
50 'title' => 'Provisioning requirements',
51 'description' => "Information of how to configure the provisioning system.",
52 'page callback' => 'hosting_help_requirements',
53 'type' => MENU_CALLBACK
54 );
55
56
57 $items['admin/hosting'] = array(
58 'title' => 'Hosting',
59 'description' => 'Configure and manage the hosting system',
60 'page callback' => 'drupal_get_form',
61 'page arguments' => array('hosting_features_form'),
62 'access arguments' => array('administer hosting'),
63 'type' => MENU_NORMAL_ITEM
64 );
65
66 $items['admin/hosting/features'] = array(
67 'title' => 'Features',
68 'description' => 'Configure the exposed functionality of the Hosting system',
69 'weight' => -100,
70 'page callback' => 'drupal_get_form',
71 'page arguments' => array('hosting_features_form'),
72 'type' => MENU_DEFAULT_LOCAL_TASK,
73 'access arguments' => array('administer hosting features'),
74 );
75
76 $items['admin/hosting/queues'] = array(
77 'title' => 'Queues',
78 'description' => 'Configure the frequency that cron, backup and task events are process',
79 'page callback' => 'drupal_get_form',
80 'page arguments' => array('hosting_queues_configure'),
81 'type' => MENU_LOCAL_TASK,
82 'access arguments' => array('administer hosting queues'),
83 );
84
85
86 $items['hosting/queues'] = array(
87 'page callback' => 'hosting_queues',
88 'type' => MENU_CALLBACK,
89 'access arguments' => array('access task logs')
90 );
91
92 return $items;
93 }
94
95 function hosting_js_page() {
96 modalframe_child_js();
97
98 $args = func_get_args();
99 $path = implode('/', $args);
100
101 menu_set_active_item($path);
102 # $_SERVER['REQUEST_URI'] = str_replace('/hosting/js', '', $_SERVER['REQUEST_URI']);
103
104 if ($router_item = menu_get_item($path)) {
105 if ($router_item['access']) {
106 if ($router_item['file']) {
107 require_once($router_item['file']);
108 }
109 $return = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
110 }
111 }
112 // Menu status constants are integers; page content is a string.
113 if (is_int($return)) {
114 modalframe_close_dialog();
115 }
116 return $return;
117 }
118
119 function hosting_menu_alter(&$items) {
120 $items['node/add']['page callback'] = '_hosting_node_add';
121
122 $types = hosting_feature_node_types(TRUE);
123 foreach ($types as $feature => $type) {
124 $path = sprintf('node/add/%s', str_replace('_', '-', $type));
125 $items[$path]['access callback'] = 'hosting_menu_access';
126 $items[$path]['access arguments'] = array($type, $feature);
127 }
128
129 // These node types should remain hidden, and provide no user interface.
130 unset($items['node/add/package']);
131 unset($items['node/add/task']);
132 }
133
134 function hosting_menu_access($type, $feature) {
135 global $user;
136 return (($user->uid == 1) || user_access('create ' . $type)) && (hosting_feature($feature) != HOSTING_FEATURE_DISABLED);
137 }
138
139 function hosting_disabled_site() {
140 drupal_set_breadcrumb(array());
141 return t("This site has been disabled by the site administrators");
142 }
143
144 function hosting_site_maintenance() {
145 drupal_set_breadcrumb(array());
146 return t("This site is currently in maintenance. Check back later.");
147 }
148
149 /**
150 * Implementation of hook_nodeapi
151 *
152 * This function redirects to hosting_nodeapi_$nodetype_$op calls, to save ourselves
153 * from an incessant amount of intricately nested code, and allow easier extension / maintenance.
154 */
155 function hosting_nodeapi(&$node, $op, $teaser) {
156 $func = "hosting_nodeapi_" . $node->type . "_" . str_replace(" ", "_", $op);
157 if (function_exists($func)) {
158 $func($node, $op, $teaser);
159 }
160 }
161 /**
162 * Implementation of hook_perm
163 */
164 function hosting_perm() {
165 return array('access hosting wizard', 'administer hosting queues', 'administer hosting features', 'administer hosting');
166 }
167
168 /**
169 * Implementation of hook_init
170 */
171 function hosting_init() {
172 // Definitions for the default platforms, clients etc.
173 // Done to avoid using 'magic numbers'
174 define('HOSTING_DEFAULT_CLIENT', variable_get('hosting_default_client', 1));
175 define('HOSTING_DEFAULT_DB_SERVER', variable_get('hosting_default_db_server', 2));
176 define('HOSTING_DEFAULT_WEB_SERVER', variable_get('hosting_default_web_server', 3));
177 define('HOSTING_DEFAULT_PLATFORM', variable_get('hosting_default_platform', 6));
178
179 define('HOSTING_OWN_DB_SERVER', variable_get('hosting_own_db_server', 2));
180 define('HOSTING_OWN_WEB_SERVER', variable_get('hosting_own_web_server', 3));
181 define('HOSTING_OWN_PLATFORM', variable_get('hosting_own_platform', 6));
182
183
184 // These defaults could be temporary.
185 $info = posix_getgrgid(posix_getgid());
186 define('HOSTING_DEFAULT_WEB_GROUP', $info['name']);
187 $user = get_current_user();
188 if ($user == 'root') {
189 $user = 'aegir'; # a better default than root
190 }
191 define('HOSTING_DEFAULT_SCRIPT_USER', $user);
192
193 define('HOSTING_DEFAULT_RESTART_CMD', _hosting_default_restart_cmd());
194
195 $path = ($_SERVER['PWD']) ? $_SERVER['PWD'] : $_SERVER['DOCUMENT_ROOT'];
196 define('HOSTING_DEFAULT_DOCROOT_PATH',rtrim($path, '/'));
197
198 $parts = explode("/", rtrim($path, '/'));
199 array_pop($parts);
200 define('HOSTING_DEFAULT_PARENT_PATH', rtrim(implode("/" , $parts), '/'));
201 define('HOSTING_DEFAULT_BACKUP_PATH', HOSTING_DEFAULT_PARENT_PATH . '/backups');
202 define('HOSTING_DEFAULT_CONFIG_PATH', HOSTING_DEFAULT_PARENT_PATH .'/config');
203 define('HOSTING_DEFAULT_VHOST_PATH', HOSTING_DEFAULT_CONFIG_PATH .'/vhost.d');
204
205 /**
206 * Find the base URL, this is used by the initial 'hosting setup' drush command
207 * This gets defined in the bootstrap, so just using the global definition.
208 */
209 define('HOSTING_DEFAULT_BASE_URL', $GLOBALS['base_url']);
210
211 // moved from hook_menu()
212 drupal_add_css(drupal_get_path('module', 'hosting') . '/hosting.css');
213 }
214
215
216 /**
217 * This function should not have to be duplicated between hosting/provision
218 */
219 function _hosting_default_restart_cmd() {
220 $command = '/usr/sbin/apachectl'; # a proper default for most of the world
221 foreach (explode(':', $_SERVER['PATH']) as $path) {
222 $options[] = "$path/apache2ctl";
223 $options[] = "$path/apachectl";
224 }
225 # try to detect the apache restart command
226 $options[] = '/usr/local/sbin/apachectl'; # freebsd
227 $options[] = '/usr/sbin/apache2ctl'; # debian + apache2
228 $options[] = $command;
229
230 foreach ($options as $test) {
231 if (is_executable($test)) {
232 $command = $test;
233 break;
234 }
235 }
236
237 return "sudo $command graceful";
238 }
239
240 /**
241 * Implementation of hook_theme().
242 */
243 function hosting_theme() {
244 return array(
245 'hosting_summary_block' => array(
246 'file' => 'hosting.module',
247 'arguments' => array(
248 'components' => NULL,
249 ),
250 ),
251 'hosting_queues_configure' => array(
252 'file' => 'hosting.module',
253 'arguments' => array(
254 'form' => NULL,
255 ),
256 ),
257 'requirement_help' => array(
258 'file' => 'hosting_help.inc',
259 'arguments' => array(
260 'form' => NULL,
261 ),
262 ),
263 );
264 }
265
266 function _hosting_node_link($nid, $title = null) {
267 if (is_null($nid)) {
268 return t("None");
269 }
270 $node = node_load($nid);
271 $title = (!is_null($title)) ? $title : filter_xss($node->title);
272 if ($node->nid) {
273 return node_access('view', $node) ? l($title, "node/" . $node->nid) : filter_xss($node->title);
274 }
275 }
276
277
278 function hosting_block($op = 'list', $delta = 0, $edit = array()) {
279 switch ($op) {
280 case 'list' :
281 $blocks['hosting_summary'] = array('info' => t('Hosting summary'),
282 'enabled' => 1, 'region' => 'left', 'weight' => 10);
283 $blocks['hosting_queues'] = array('info' => t('Hosting queues'),
284 'enabled' => 1, 'region' => 'right', 'weight' => 0);
285 $blocks['hosting_queues_summary'] = array('info' => t('Hosting queues summary'),
286 'enabled' => 1, 'region' => 'right', 'weight' => 1);
287 return $blocks;
288 case 'view' :
289 switch ($delta) {
290 case 'hosting_summary':
291 return array(
292 'title' => t('Summary'),
293 'content' => hosting_summary_block());
294 break;
295 case 'hosting_queues':
296 return array('title' => t('Queues'),
297 'content' => hosting_queue_block());
298 break;
299 case 'hosting_queues_summary':
300 return array('title' => t('Queues summary'),
301 'content' => hosting_queue_summary_block());
302 break;
303 }
304 }
305 }
306
307 function hosting_queue_summary_block() {
308 if (user_access('administer hosting queues')) {
309 $queues = hosting_get_queues();
310 $output = '';
311 foreach ($queues as $queue => $info) {
312 $disp = array();
313 # special case
314 if (!$info['enabled']) {
315 $disp[] = t('Status: disabled');
316 continue;
317 }
318 $disp[] = t('Status: enabled');
319 foreach (array('description' => t('Description'), 'frequency' => t('Frequency'), 'items' => t('Items per run'), 'total_items' => t('Items in queue'), 'last_run' => t('Last run')) as $key => $title) {
320 if ($key == 'last_run') {
321 $info[$key] = hosting_format_interval($info[$key]);
322 } elseif ($key == 'frequency') {
323 $info[$key] = t('every @interval', array('@interval' => format_interval($info[$key])));
324 }
325 $disp[] = $title . ": " . $info[$key];
326 }
327 $output .= theme('item_list', $disp, $info['name']);
328 }
329 return $output;
330 }
331 }
332
333 function hosting_queue_block() {
334 if (user_access('access task logs')) {
335 $queues = hosting_get_queues();
336 $output = '';
337 foreach ($queues as $queue => $info) {
338 $func = 'hosting_'.$info['singular'].'_summary';
339 if (function_exists($func)) {
340 $output .= $func();
341 }
342 }
343 return $output;
344 }
345 }
346
347 function hosting_summary_block() {
348 if (user_access('administer hosting')) {
349 return theme('hosting_summary_block', module_invoke_all('hosting_summary'));
350 }
351 }
352
353 function theme_hosting_summary_block($components) {
354 foreach ($components as $component) {
355 $output .= $component;
356 }
357 return $output;
358 }
359
360 /**
361 * Check site URL is allowed
362 *
363 * This function hooks into hook_allow_domain to let contrib modules
364 * weigh in on whether the site should be created.
365 *
366 * All the hooks must return true for the domain to be allowed.
367 */
368 function hosting_domain_allowed($url, $params = array()) {
369 $results = module_invoke_all('allow_domain', $url, $params);
370 $return = !in_array(FALSE, $results);
371 return $return;
372 }
373
374 /**
375 * Initial hosting setup
376 * Runs the 'hosting dispatch' command, and
377 * move on to setting up the crontab
378 */
379 function hosting_setup() {
380 variable_set('hosting_dispatch_enabled', FALSE);
381 // attempt to run the dispatch command, to make sure it runs without the queue being enabled.
382 variable_set('hosting_dispatch_enabled', TRUE);
383 drush_print(_hosting_dispatch_cmd());
384 exec(_hosting_dispatch_cmd(), $return, $code);
385 variable_set('hosting_dispatch_enabled', FALSE);
386 $return = join("\n", $return);
387 $data = unserialize($return);
388 if ($code == DRUSH_SUCCESS) {
389 variable_set('hosting_dispatch_enabled', TRUE);
390 drush_log(t("Dispatch command was run successfully"), 'success');
391 _hosting_setup_cron();
392 }
393 else {
394 drush_set_error('DRUSH_FRAMEWORK_ERROR', dt("Dispatch command could not be run. Returned: \n@return", array('@return' => $return)));
395 }
396 if (drush_get_error()) {
397 drush_log(t("The command did not complete successfully, please fix the issues and re-run this script"), 'error');
398 }
399 }
400
401 /**
402 * Set up the hosting dispatch command in the aegir user's crontab
403 * Replace the crontab entry if it exists, else create it from scratch
404 */
405 function _hosting_setup_cron() {
406 $existing = FALSE;
407 exec('crontab -l 2> /dev/null', $cron);
408 variable_set('hosting_cron_backup', $cron);
409 if (sizeof($cron)) {
410 foreach ($cron as $line => $entry) {
411 if (preg_match('/hosting dispatch/', $entry)) {
412 $pattern = "+(.*)'(.*)/drush.php'(.*)--root='(.*)'.*+";
413 $replace = sprintf("$1 '%s' hosting dispatch --root=%s --uri=%s)",
414 DRUSH_COMMAND,
415 escapeshellarg(HOSTING_DEFAULT_DOCROOT_PATH),
416 drush_get_option('uri')
417 );
418 $cron[$line] = preg_replace($pattern, $replace, $entry);
419 drush_log(dt("Existing hosting dispatch cron entry was found. Replacing"));
420 $existing = TRUE;
421 break;
422 }
423 }
424 }
425 else {
426 drush_log("message", t("No existing crontab was found"));
427 }
428 if (!$existing) {
429 $cron[] = hosting_queues_cron_cmd();
430 }
431 $tmpnam = tempnam('hostmaster', 'hm.cron');
432 $fp = fopen($tmpnam, "w");
433 foreach ($cron as $line) {
434 fwrite($fp, $line . "\n");
435 }
436 fclose($fp);
437 system(sprintf('crontab %s', escapeshellarg($tmpnam)));
438 unlink($tmpnam);
439 drush_log("Notice", t("Installed hosting dispatch cron entry to run every minute"));
440 }
441
442
443 /**
444 * Replacement node/add page.
445 *
446 * Major kludge to remove the hidden node types from node/add page
447 *
448 * Copied from node.module
449 */
450 function _hosting_node_add($type = '') {
451 global $user;
452
453 $types = node_get_types();
454 $type = ($type) ? str_replace('-', '_', $type) : NULL;
455 // If a node type has been specified, validate its existence.
456 if (isset($types[$type]) && user_access('create ' . $type) && (hosting_feature($type) !== HOSTING_FEATURE_DISABLED)) {
457 // Initialize settings:
458 $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => $type);
459
460 drupal_set_title(t('Submit @name', array('@name' => $types[$type]->name)));
461 $output = drupal_get_form($type .'_node_form', $node);
462 }
463 else {
464 // If no (valid) node type has been provided, display a node type overview.
465 foreach ($types as $type) {
466 if (function_exists($type->module .'_form') && user_access('create ' . $type->type) && (hosting_feature($type->type) !== HOSTING_FEATURE_DISABLED)) {
467 $type_url_str = str_replace('_', '-', $type->type);
468 $title = t('Add a new @s.', array('@s' => $type->name));
469 $out = '<dt>'. l(drupal_ucfirst($type->name), "node/add/$type_url_str", array('attributes' => array('title' => $title))) .'</dt>';
470 $out .= '<dd>'. filter_xss_admin($type->description) .'</dd>';
471 $item[$type->name] = $out;
472 }
473 }
474
475 if (isset($item)) {
476 uksort($item, 'strnatcasecmp');
477 $output = t('Choose the appropriate item from the list:') .'<dl>'. implode('', $item) .'</dl>';
478 }
479 else {
480 $output = t('No content types available.');
481 }
482 }
483
484 return $output;
485 }
486
487 /**
488 * List queues or tasks in a queue if a key is provided
489 */
490
491 function hosting_queues($key='') {
492 $queues = hosting_get_queues();
493
494 if ($queues[$key]) {
495 if ($queues[$key]['name'])
496 {
497 $output .= "<h1>".$queues[$key]['name']."</h1>";
498 }
499
500 $func = 'hosting_'.$queues[$key]['singular'].'_list';
501 if (function_exists($func)) {
502 $output .= $func();
503 }
504 }
505 else
506 {
507 foreach($queues as $key => $queue) {
508 $item[] = l($queue['name'], 'hosting/queues/'.$key);
509 }
510 $output .= theme('item_list', $item, t('Queues'));
511 }
512
513 return $output;
514 }
515
516 /**
517 * Generate context sensitive breadcrumbs
518 */
519 function hosting_set_breadcrumb($node) {
520 $breadcrumbs[] = l(t('Home'), NULL);
521 switch ($node->type) {
522 case 'task':
523 $breadcrumbs[] = _hosting_node_link($node->rid);
524 break;
525 case 'platform' :
526 $breadcrumbs[] = _hosting_node_link($node->web_server);
527 break;
528 case 'site' :
529 $breadcrumbs[] = _hosting_node_link($node->platform);
530 break;
531 }
532 drupal_set_breadcrumb($breadcrumbs);
533 }
534
535 /**
536 * Page callback
537 *
538 * Configure the frequency of tasks.
539 */
540 function hosting_queues_configure() {
541 drupal_add_css(drupal_get_path('module', 'hosting') . '/hosting.css');
542 $units = array(
543 strtotime("1 second", 0) => t("Seconds"),
544 strtotime("1 minute", 0) => t("Minutes"),
545 strtotime("1 hour", 0) => t("Hours"),
546 strtotime("1 day", 0) => t("Days"),
547 strtotime("1 week", 0) => t("Weeks"),
548 );
549
550 $queues = hosting_get_queues();
551 $form['#tree'] = TRUE;
552
553 foreach ($queues as $queue => $info) {
554 $form[$queue]['description'] = array(
555 '#type' => 'item',
556 '#value' => $info['name'],
557 '#description' => $info['description']
558 );
559
560 $form[$queue]["enabled"] = array(
561 '#type' => 'checkbox',
562 '#default_value' => $info['enabled']
563 );
564
565 $form[$queue]["last_run"] = array(
566 '#value' => hosting_format_interval(variable_get('hosting_queue_' . $queue . '_last_run', false))
567 );
568 $form[$queue]['frequency']['#prefix'] = "<div class='hosting-queue-frequency'>";
569 $form[$queue]['frequency']['#suffix'] = "</div>";
570
571 if ($info['type'] == 'batch') {
572 $form[$queue]['frequency']['items'] = array(
573 '#value' => t('%count %items every ', array("%count" => $info['total_items'], "%items" => format_plural($info['total_items'], $info['singular'], $info['plural']))),
574 );
575 }
576 else {
577 $form[$queue]['frequency']['items'] = array(
578 '#type' => 'textfield',
579 '#size' => 3,
580 '#maxlength' => 3,
581 '#default_value' => $info['items'],
582 '#suffix' => t(' %items every ', array('%items' => $info['plural'])),
583 );
584 }
585 foreach (array_reverse(array_keys($units)) as $length) {
586 $unit = $units[$length];
587
588 if (!($info['frequency'] % $length)) {
589 $frequency_ticks = $info['frequency'] / $length;
590 $frequency_length = $length;
591 break;
592 }
593 }
594 $form[$queue]['frequency']["ticks"] = array(
595 '#type' => 'textfield',
596 '#default_value' => $frequency_ticks,
597 '#maxlength' => 5,
598 '#size' => 5
599 );
600 $form[$queue]['frequency']["unit"] = array(
601 '#type' => 'select',
602 '#options' => $units,
603 '#default_value' => $frequency_length,
604 );
605 }
606 $form['submit'] = array('#type' => 'submit', '#value' => t('Save changes'));
607 return $form;
608 }
609
610 function theme_hosting_queues_configure($form) {
611 $queues = hosting_get_queues();
612
613 $rows = array();
614 $header = array('', t('Description'),
615 array('data' => t('Frequency'), 'class' => 'hosting-queue-frequency-head'),
616 t('Last run'),);
617 foreach ($queues as $key => $info) {
618 $row = array();
619 $row[] = drupal_render($form[$key]['enabled']);
620 $row[] = drupal_render($form[$key]['description']);
621 $row[] = drupal_render($form[$key]['frequency']);
622 $row[] = drupal_render($form[$key]['last_run']);
623 $rows[] = $row;
624 }
625 $output = theme('table', $header, $rows);
626 $output .= drupal_render($form['submit']);
627 $output .= drupal_render($form);
628 return $output;
629 }
630
631 /**
632 * Implementation of hook_validate()
633 */
634 function hosting_queues_configure_validate($form, &$form_state) {
635 foreach (hosting_get_queues() as $queue => $info) {
636 if ($form_state['values'][$queue]) {
637 if ($form_state['values'][$queue]['frequency']['ticks'] && !is_numeric($form_state['values'][$queue]['frequency']['ticks'])) {
638 form_set_error($queue, t('Please enter a valid frequency.'));
639 }
640 if ($form_state['values'][$queue]['frequency']['items'] && !is_numeric($form_state['values'][$queue]['frequency']['items'])) {
641 form_set_error($queue, t('Please enter a valid amount of items.'));
642 }
643 }
644 }
645 }
646
647 /**
648 * Implementation of hook_submit()
649 */
650 function hosting_queues_configure_submit($form, &$form_state) {
651 foreach (hosting_get_queues() as $queue => $info) {
652 if ($form_state['values'][$queue]) {
653 variable_set("hosting_queue_" . $queue . "_enabled", $form_state['values'][$queue]['enabled']);
654 variable_set("hosting_queue_" . $queue . "_frequency", $form_state['values'][$queue]['frequency']['ticks'] * $form_state['values'][$queue]['frequency']['unit']);
655 if ($info['type'] == 'serial') {
656 variable_set("hosting_queue_" . $queue . "_items", $form_state['values'][$queue]['frequency']['items']);
657 }
658 }
659 }
660 drupal_set_message(t('The queue settings have been updated.'));
661 }
662
663 /**
664 * Implementation of hook_form_alter()
665 */
666 function hosting_form_alter(&$form, &$form_state, $form_id) {
667 // Alter the 'Add User' form to remind users that this is not the New Client form
668 if ($form_id == 'user_register') {
669 $form[user_registration_help] = array(
670 '#type' => 'item',
671 '#description' => t('<strong>Adding a system user account does not make the user a Client that can add sites.</strong><br />
672 To add a Client, enable the Client feature and then add a new Client node.<br />
673 If you wish, you may then assign this system user to the Client as an \'Allowed user\' to inherit the permissions to add sites.'),
674 '#weight' => '-10'
675 );
676 }
677 }
678

  ViewVC Help
Powered by ViewVC 1.1.2