/[drupal]/drupal/modules/aggregator/aggregator.admin.inc
ViewVC logotype

Contents of /drupal/modules/aggregator/aggregator.admin.inc

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


Revision 1.45 - (show annotations) (download) (as text)
Wed Nov 4 04:33:37 2009 UTC (2 weeks, 6 days ago) by webchick
Branch: MAIN
CVS Tags: DRUPAL-7-0-UNSTABLE-10, HEAD
Changes since 1.44: +2 -2 lines
File MIME type: text/x-php
#620278 by alex_b: Fixed Add feed link.
1 <?php
2 // $Id: aggregator.admin.inc,v 1.44 2009/10/09 00:59:55 dries Exp $
3
4 /**
5 * @file
6 * Admin page callbacks for the aggregator module.
7 */
8
9 /**
10 * Menu callback; displays the aggregator administration page.
11 */
12 function aggregator_admin_overview() {
13 return aggregator_view();
14 }
15
16 /**
17 * Displays the aggregator administration page.
18 *
19 * @return
20 * The page HTML.
21 */
22 function aggregator_view() {
23 $result = db_query('SELECT f.*, COUNT(i.iid) AS items FROM {aggregator_feed} f LEFT JOIN {aggregator_item} i ON f.fid = i.fid GROUP BY f.fid, f.title, f.url, f.refresh, f.checked, f.link, f.description, f.hash, f.etag, f.modified, f.image, f.block ORDER BY f.title');
24
25 $output = '<h3>' . t('Feed overview') . '</h3>';
26
27 $header = array(t('Title'), t('Items'), t('Last update'), t('Next update'), array('data' => t('Operations'), 'colspan' => '3'));
28 $rows = array();
29 foreach ($result as $feed) {
30 $rows[] = array(l($feed->title, "aggregator/sources/$feed->fid"), format_plural($feed->items, '1 item', '@count items'), ($feed->checked ? t('@time ago', array('@time' => format_interval(REQUEST_TIME - $feed->checked))) : t('never')), ($feed->checked && $feed->refresh ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - REQUEST_TIME))) : t('never')), l(t('edit'), "admin/config/services/aggregator/edit/feed/$feed->fid"), l(t('remove items'), "admin/config/services/aggregator/remove/$feed->fid"), l(t('update items'), "admin/config/services/aggregator/update/$feed->fid"));
31 }
32 if (empty($rows)) {
33 $rows[] = array(array('data' => t('No feeds available. <a href="@link">Add feed</a>.', array('@link' => url('admin/config/services/aggregator/add/feed'))), 'colspan' => '5', 'class' => array('message')));
34 }
35 $output .= theme('table', array('header' => $header, 'rows' => $rows));
36
37 $result = db_query('SELECT c.cid, c.title, COUNT(ci.iid) as items FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid GROUP BY c.cid, c.title ORDER BY title');
38
39 $output .= '<h3>' . t('Category overview') . '</h3>';
40
41 $header = array(t('Title'), t('Items'), t('Operations'));
42 $rows = array();
43 foreach ($result as $category) {
44 $rows[] = array(l($category->title, "aggregator/categories/$category->cid"), format_plural($category->items, '1 item', '@count items'), l(t('edit'), "admin/config/services/aggregator/edit/category/$category->cid"));
45 }
46 if (empty($rows)) {
47 $rows[] = array(array('data' => t('No categories available. <a href="@link">Add category</a>.', array('@link' => url('admin/config/services/aggregator/add/category'))), 'colspan' => '5', 'class' => array('message')));
48 }
49 $output .= theme('table', array('header' => $header, 'rows' => $rows));
50
51 return $output;
52 }
53
54 /**
55 * Form builder; Generate a form to add/edit feed sources.
56 *
57 * @ingroup forms
58 * @see aggregator_form_feed_validate()
59 * @see aggregator_form_feed_submit()
60 */
61 function aggregator_form_feed($form, &$form_state, stdClass $feed = NULL) {
62 $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
63 $period[AGGREGATOR_CLEAR_NEVER] = t('Never');
64
65 $form['title'] = array('#type' => 'textfield',
66 '#title' => t('Title'),
67 '#default_value' => isset($feed->title) ? $feed->title : '',
68 '#maxlength' => 255,
69 '#description' => t('The name of the feed (or the name of the website providing the feed).'),
70 '#required' => TRUE,
71 );
72 $form['url'] = array('#type' => 'textfield',
73 '#title' => t('URL'),
74 '#default_value' => isset($feed->url) ? $feed->url : '',
75 '#maxlength' => 255,
76 '#description' => t('The fully-qualified URL of the feed.'),
77 '#required' => TRUE,
78 );
79 $form['refresh'] = array('#type' => 'select',
80 '#title' => t('Update interval'),
81 '#default_value' => isset($feed->refresh) ? $feed->refresh : 3600,
82 '#options' => $period,
83 '#description' => t('The length of time between feed updates. Requires a correctly configured <a href="@cron">cron maintenance task</a>.', array('@cron' => url('admin/reports/status'))),
84 );
85 $form['block'] = array('#type' => 'select',
86 '#title' => t('News items in block'),
87 '#default_value' => isset($feed->block) ? $feed->block : 5,
88 '#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)),
89 '#description' => t("Drupal can make a block with the most recent news items of this feed. You can <a href=\"@block-admin\">configure blocks</a> to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in this feed's block. If you choose '0' this feed's block will be disabled.", array('@block-admin' => url('admin/structure/block'))),
90 );
91
92 // Handling of categories.
93 $options = array();
94 $values = array();
95 $categories = db_query('SELECT c.cid, c.title, f.fid FROM {aggregator_category} c LEFT JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => isset($feed->fid) ? $feed->fid : NULL));
96 foreach ($categories as $category) {
97 $options[$category->cid] = check_plain($category->title);
98 if ($category->fid) $values[] = $category->cid;
99 }
100
101 if ($options) {
102 $form['category'] = array(
103 '#type' => 'checkboxes',
104 '#title' => t('Categorize news items'),
105 '#default_value' => $values,
106 '#options' => $options,
107 '#description' => t('New feed items are automatically filed in the checked categories.'),
108 );
109 }
110 $form['submit'] = array(
111 '#type' => 'submit',
112 '#value' => t('Save'),
113 );
114
115 if (!empty($feed->fid)) {
116 $form['delete'] = array(
117 '#type' => 'submit',
118 '#value' => t('Delete'),
119 );
120 $form['fid'] = array(
121 '#type' => 'hidden',
122 '#value' => $feed->fid,
123 );
124 }
125
126 return $form;
127 }
128
129 /**
130 * Validate aggregator_form_feed() form submissions.
131 */
132 function aggregator_form_feed_validate($form, &$form_state) {
133 if ($form_state['values']['op'] == t('Save')) {
134 // Ensure URL is valid.
135 if (!valid_url($form_state['values']['url'], TRUE)) {
136 form_set_error('url', t('The URL %url is invalid. Please enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $form_state['values']['url'])));
137 }
138 // Check for duplicate titles.
139 if (isset($form_state['values']['fid'])) {
140 $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE (title = :title OR url = :url) AND fid <> :fid", array(':title' => $form_state['values']['title'], ':url' => $form_state['values']['url'], ':fid' => $form_state['values']['fid']));
141 }
142 else {
143 $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = :title OR url = :url", array(':title' => $form_state['values']['title'], ':url' => $form_state['values']['url']));
144 }
145 foreach ($result as $feed) {
146 if (strcasecmp($feed->title, $form_state['values']['title']) == 0) {
147 form_set_error('title', t('A feed named %feed already exists. Please enter a unique title.', array('%feed' => $form_state['values']['title'])));
148 }
149 if (strcasecmp($feed->url, $form_state['values']['url']) == 0) {
150 form_set_error('url', t('A feed with this URL %url already exists. Please enter a unique URL.', array('%url' => $form_state['values']['url'])));
151 }
152 }
153 }
154 }
155
156 /**
157 * Process aggregator_form_feed() form submissions.
158 *
159 * @todo Add delete confirmation dialog.
160 */
161 function aggregator_form_feed_submit($form, &$form_state) {
162 if ($form_state['values']['op'] == t('Delete')) {
163 $title = $form_state['values']['title'];
164 // Unset the title.
165 unset($form_state['values']['title']);
166 }
167 aggregator_save_feed($form_state['values']);
168 if (isset($form_state['values']['fid'])) {
169 if (isset($form_state['values']['title'])) {
170 drupal_set_message(t('The feed %feed has been updated.', array('%feed' => $form_state['values']['title'])));
171 if (arg(0) == 'admin') {
172 $form_state['redirect'] = 'admin/config/services/aggregator/';
173 return;
174 }
175 else {
176 $form_state['redirect'] = 'aggregator/sources/' . $form_state['values']['fid'];
177 return;
178 }
179 }
180 else {
181 watchdog('aggregator', 'Feed %feed deleted.', array('%feed' => $title));
182 drupal_set_message(t('The feed %feed has been deleted.', array('%feed' => $title)));
183 if (arg(0) == 'admin') {
184 $form_state['redirect'] = 'admin/config/services/aggregator/';
185 return;
186 }
187 else {
188 $form_state['redirect'] = 'aggregator/sources/';
189 return;
190 }
191 }
192 }
193 else {
194 watchdog('aggregator', 'Feed %feed added.', array('%feed' => $form_state['values']['title']), WATCHDOG_NOTICE, l(t('view'), 'admin/config/services/aggregator'));
195 drupal_set_message(t('The feed %feed has been added.', array('%feed' => $form_state['values']['title'])));
196 }
197 }
198
199 function aggregator_admin_remove_feed($form, $form_state, $feed) {
200 return confirm_form(
201 array(
202 'feed' => array(
203 '#type' => 'value',
204 '#value' => $feed,
205 ),
206 ),
207 t('Are you sure you want to remove all items from the feed %feed?', array('%feed' => $feed->title)),
208 'admin/config/services/aggregator',
209 t('This action cannot be undone.'),
210 t('Remove items'),
211 t('Cancel')
212 );
213 }
214
215 /**
216 * Remove all items from a feed and redirect to the overview page.
217 *
218 * @param $feed
219 * An associative array describing the feed to be cleared.
220 */
221 function aggregator_admin_remove_feed_submit($form, &$form_state) {
222 aggregator_remove($form_state['values']['feed']);
223 $form_state['redirect'] = 'admin/config/services/aggregator';
224 }
225
226 /**
227 * Form builder; Generate a form to import feeds from OPML.
228 *
229 * @ingroup forms
230 * @see aggregator_form_opml_validate()
231 * @see aggregator_form_opml_submit()
232 */
233 function aggregator_form_opml($form, &$form_state) {
234 $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
235
236 $form['upload'] = array(
237 '#type' => 'file',
238 '#title' => t('OPML File'),
239 '#description' => t('Upload an OPML file containing a list of feeds to be imported.'),
240 );
241 $form['remote'] = array(
242 '#type' => 'textfield',
243 '#title' => t('OPML Remote URL'),
244 '#maxlength' => 1024,
245 '#description' => t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'),
246 );
247 $form['refresh'] = array(
248 '#type' => 'select',
249 '#title' => t('Update interval'),
250 '#default_value' => 3600,
251 '#options' => $period,
252 '#description' => t('The length of time between feed updates. Requires a correctly configured <a href="@cron">cron maintenance task</a>.', array('@cron' => url('admin/reports/status'))),
253 );
254 $form['block'] = array('#type' => 'select',
255 '#title' => t('News items in block'),
256 '#default_value' => 5,
257 '#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)),
258 '#description' => t("Drupal can make a block with the most recent news items of a feed. You can <a href=\"@block-admin\">configure blocks</a> to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in a feed's block. If you choose '0' these feeds' blocks will be disabled.", array('@block-admin' => url('admin/structure/block'))),
259 );
260
261 // Handling of categories.
262 $options = array_map('check_plain', db_query("SELECT cid, title FROM {aggregator_category} ORDER BY title")->fetchAllKeyed());
263 if ($options) {
264 $form['category'] = array(
265 '#type' => 'checkboxes',
266 '#title' => t('Categorize news items'),
267 '#options' => $options,
268 '#description' => t('New feed items are automatically filed in the checked categories.'),
269 );
270 }
271 $form['submit'] = array(
272 '#type' => 'submit',
273 '#value' => t('Import')
274 );
275
276 return $form;
277 }
278
279 /**
280 * Validate aggregator_form_opml form submissions.
281 */
282 function aggregator_form_opml_validate($form, &$form_state) {
283 // If both fields are empty or filled, cancel.
284 if (empty($form_state['values']['remote']) == empty($_FILES['files']['name']['upload'])) {
285 form_set_error('remote', t('You must <em>either</em> upload a file or enter a URL.'));
286 }
287
288 // Validate the URL, if one was entered.
289 if (!empty($form_state['values']['remote']) && !valid_url($form_state['values']['remote'], TRUE)) {
290 form_set_error('remote', t('This URL is not valid.'));
291 }
292 }
293
294 /**
295 * Process aggregator_form_opml form submissions.
296 */
297 function aggregator_form_opml_submit($form, &$form_state) {
298 $data = '';
299 if ($file = file_save_upload('upload')) {
300 $data = file_get_contents($file->uri);
301 }
302 else {
303 $response = drupal_http_request($form_state['values']['remote']);
304 if (!isset($response->error)) {
305 $data = $response->data;
306 }
307 }
308
309 $feeds = _aggregator_parse_opml($data);
310 if (empty($feeds)) {
311 drupal_set_message(t('No new feed has been added.'));
312 return;
313 }
314
315 $form_state['values']['op'] = t('Save');
316
317 foreach ($feeds as $feed) {
318 // Ensure URL is valid.
319 if (!valid_url($feed['url'], TRUE)) {
320 drupal_set_message(t('The URL %url is invalid.', array('%url' => $feed['url'])), 'warning');
321 continue;
322 }
323
324 // Check for duplicate titles or URLs.
325 $result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = :title OR url = :url", array(':title' => $feed['title'], ':url' => $feed['url']));
326 foreach ($result as $old) {
327 if (strcasecmp($old->title, $feed['title']) == 0) {
328 drupal_set_message(t('A feed named %title already exists.', array('%title' => $old->title)), 'warning');
329 continue 2;
330 }
331 if (strcasecmp($old->url, $feed['url']) == 0) {
332 drupal_set_message(t('A feed with the URL %url already exists.', array('%url' => $old->url)), 'warning');
333 continue 2;
334 }
335 }
336
337 $form_state['values']['title'] = $feed['title'];
338 $form_state['values']['url'] = $feed['url'];
339 drupal_form_submit('aggregator_form_feed', $form_state);
340 }
341
342 $form_state['redirect'] = 'admin/config/services/aggregator';
343 }
344
345 /**
346 * Parse an OPML file.
347 *
348 * Feeds are recognized as <outline> elements with the attributes
349 * <em>text</em> and <em>xmlurl</em> set.
350 *
351 * @param $opml
352 * The complete contents of an OPML document.
353 * @return
354 * An array of feeds, each an associative array with a <em>title</em> and
355 * a <em>url</em> element, or NULL if the OPML document failed to be parsed.
356 * An empty array will be returned if the document is valid but contains
357 * no feeds, as some OPML documents do.
358 */
359 function _aggregator_parse_opml($opml) {
360 $feeds = array();
361 $xml_parser = drupal_xml_parser_create($opml);
362 if (xml_parse_into_struct($xml_parser, $opml, $values)) {
363 foreach ($values as $entry) {
364 if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) {
365 $item = $entry['attributes'];
366 if (!empty($item['XMLURL']) && !empty($item['TEXT'])) {
367 $feeds[] = array('title' => $item['TEXT'], 'url' => $item['XMLURL']);
368 }
369 }
370 }
371 }
372 xml_parser_free($xml_parser);
373
374 return $feeds;
375 }
376
377 /**
378 * Menu callback; refreshes a feed, then redirects to the overview page.
379 *
380 * @param $feed
381 * An object describing the feed to be refreshed.
382 */
383 function aggregator_admin_refresh_feed($feed) {
384 aggregator_refresh($feed);
385 drupal_goto('admin/config/services/aggregator');
386 }
387
388 /**
389 * Form builder; Configure the aggregator system.
390 *
391 * @ingroup forms
392 */
393 function aggregator_admin_form($form, $form_state) {
394 // Make sure configuration is sane.
395 aggregator_sanitize_configuration();
396
397 // Get all available fetchers.
398 $fetchers = module_implements('aggregator_fetch');
399 foreach ($fetchers as $k => $module) {
400 if ($info = module_invoke($module, 'aggregator_fetch_info')) {
401 $label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>';
402 }
403 else {
404 $label = $module;
405 }
406 unset($fetchers[$k]);
407 $fetchers[$module] = $label;
408 }
409
410 // Get all available parsers.
411 $parsers = module_implements('aggregator_parse');
412 foreach ($parsers as $k => $module) {
413 if ($info = module_invoke($module, 'aggregator_parse_info')) {
414 $label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>';
415 }
416 else {
417 $label = $module;
418 }
419 unset($parsers[$k]);
420 $parsers[$module] = $label;
421 }
422
423 // Get all available processors.
424 $processors = module_implements('aggregator_process');
425 foreach ($processors as $k => $module) {
426 if ($info = module_invoke($module, 'aggregator_process_info')) {
427 $label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>';
428 }
429 else {
430 $label = $module;
431 }
432 unset($processors[$k]);
433 $processors[$module] = $label;
434 }
435
436 // Only show basic configuration if there are actually options.
437 $basic_conf = array();
438 if (count($fetchers) > 1) {
439 $basic_conf['aggregator_fetcher'] = array(
440 '#type' => 'radios',
441 '#title' => t('Fetcher'),
442 '#description' => t('Fetchers download data from an external source. Choose a fetcher suitable for the external source you would like to download from.'),
443 '#options' => $fetchers,
444 '#default_value' => variable_get('aggregator_fetcher', 'aggregator'),
445 );
446 }
447 if (count($parsers) > 1) {
448 $basic_conf['aggregator_parser'] = array(
449 '#type' => 'radios',
450 '#title' => t('Parser'),
451 '#description' => t('Parsers transform downloaded data into standard structures. Choose a parser suitable for the type of feeds you would like to aggregate.'),
452 '#options' => $parsers,
453 '#default_value' => variable_get('aggregator_parser', 'aggregator'),
454 );
455 }
456 if (count($processors) > 1) {
457 $basic_conf['aggregator_processors'] = array(
458 '#type' => 'checkboxes',
459 '#title' => t('Processors'),
460 '#description' => t('Processors act on parsed feed data, for example they store feed items. Choose the processors suitable for your task.'),
461 '#options' => $processors,
462 '#default_value' => variable_get('aggregator_processors', array('aggregator')),
463 );
464 }
465 if (count($basic_conf)) {
466 $form['basic_conf'] = array(
467 '#type' => 'fieldset',
468 '#title' => t('Basic configuration'),
469 '#description' => t('For most aggregation tasks, the default settings are fine.'),
470 '#collapsible' => TRUE,
471 '#collapsed' => FALSE,
472 );
473 $form['basic_conf'] += $basic_conf;
474 }
475
476 // Implementing modules will expect an array at $form['modules'].
477 $form['modules'] = array();
478
479 $form['submit'] = array(
480 '#type' => 'submit',
481 '#value' => t('Save configuration'),
482 );
483
484 return $form;
485 }
486
487 function aggregator_admin_form_submit($form, &$form_state) {
488 $form_state['values']['aggregator_processors'] = array_filter($form_state['values']['aggregator_processors']);
489 system_settings_form_submit($form, $form_state);
490 }
491
492 /**
493 * Form builder; Generate a form to add/edit/delete aggregator categories.
494 *
495 * @ingroup forms
496 * @see aggregator_form_category_validate()
497 * @see aggregator_form_category_submit()
498 */
499 function aggregator_form_category($form, &$form_state, $edit = array('title' => '', 'description' => '', 'cid' => NULL)) {
500 $form['title'] = array('#type' => 'textfield',
501 '#title' => t('Title'),
502 '#default_value' => $edit['title'],
503 '#maxlength' => 64,
504 '#required' => TRUE,
505 );
506 $form['description'] = array('#type' => 'textarea',
507 '#title' => t('Description'),
508 '#default_value' => $edit['description'],
509 );
510 $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
511
512 if ($edit['cid']) {
513 $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
514 $form['cid'] = array('#type' => 'hidden', '#value' => $edit['cid']);
515 }
516
517 return $form;
518 }
519
520 /**
521 * Validate aggregator_form_feed form submissions.
522 */
523 function aggregator_form_category_validate($form, &$form_state) {
524 if ($form_state['values']['op'] == t('Save')) {
525 // Check for duplicate titles
526 if (isset($form_state['values']['cid'])) {
527 $category = db_query("SELECT cid FROM {aggregator_category} WHERE title = :title AND cid <> :cid", array(':title' => $form_state['values']['title'], ':cid' => $form_state['values']['cid']))->fetchObject();
528 }
529 else {
530 $category = db_query("SELECT cid FROM {aggregator_category} WHERE title = :title", array(':title' => $form_state['values']['title']))->fetchObject();
531 }
532 if ($category) {
533 form_set_error('title', t('A category named %category already exists. Please enter a unique title.', array('%category' => $form_state['values']['title'])));
534 }
535 }
536 }
537
538 /**
539 * Process aggregator_form_category form submissions.
540 *
541 * @todo Add delete confirmation dialog.
542 */
543 function aggregator_form_category_submit($form, &$form_state) {
544 if ($form_state['values']['op'] == t('Delete')) {
545 $title = $form_state['values']['title'];
546 // Unset the title.
547 unset($form_state['values']['title']);
548 }
549 aggregator_save_category($form_state['values']);
550 if (isset($form_state['values']['cid'])) {
551 if (isset($form_state['values']['title'])) {
552 drupal_set_message(t('The category %category has been updated.', array('%category' => $form_state['values']['title'])));
553 if (arg(0) == 'admin') {
554 $form_state['redirect'] = 'admin/config/services/aggregator/';
555 return;
556 }
557 else {
558 $form_state['redirect'] = 'aggregator/categories/' . $form_state['values']['cid'];
559 return;
560 }
561 }
562 else {
563 watchdog('aggregator', 'Category %category deleted.', array('%category' => $title));
564 drupal_set_message(t('The category %category has been deleted.', array('%category' => $title)));
565 if (arg(0) == 'admin') {
566 $form_state['redirect'] = 'admin/config/services/aggregator/';
567 return;
568 }
569 else {
570 $form_state['redirect'] = 'aggregator/categories/';
571 return;
572 }
573 }
574 }
575 else {
576 watchdog('aggregator', 'Category %category added.', array('%category' => $form_state['values']['title']), WATCHDOG_NOTICE, l(t('view'), 'admin/config/services/aggregator'));
577 drupal_set_message(t('The category %category has been added.', array('%category' => $form_state['values']['title'])));
578 }
579 }

  ViewVC Help
Powered by ViewVC 1.1.2