/[openfoncier]/trunk/obj/digitalizedDocument.class.php
ViewVC logotype

Contents of /trunk/obj/digitalizedDocument.class.php

Parent Directory Parent Directory | Revision Log Revision Log


Revision 15650 - (show annotations)
Thu Aug 31 17:29:39 2023 UTC (17 months ago) by softime
File size: 25462 byte(s)
chore(branch): fusion de la branche d'intégration 6.0.0-develop dans le trunk

1 <?php
2 /**
3 * Ce script permet de définir la classe 'DigitalizedDocument'.
4 *
5 * @package openads
6 * @version SVN : $Id
7 */
8
9 /**
10 * Cette classe permet d'effectuer les traitements sur les documents à incorporer dans l'application
11 *
12 * Pour imorter les données depuis un dossier il faut utiliser run_import dans une boucle
13 * qui parcourt le dossier qui abrite les répertoires des dossiers d'instructions
14 *
15 * Pour purger les documents il faut utiliser la fonction run_purge dans une boucle
16 * qui parcourt le dossier qui abrite les répertoires des dossiers d'instructions
17 */
18 class DigitalizedDocument {
19
20 /**
21 * [$log_file description]
22 * @var string
23 */
24 static protected $log_file = "digitalized_document.log";
25
26 /**
27 * @access static
28 * @var string Messages utilisées pour l'écriture dans le log
29 */
30 var $NO_REP;
31 var $NO_FILES;
32 var $NO_FILE_EXIST;
33 var $DOC_NO_CONFORME;
34 var $NO_LINK;
35 var $NO_MOVE;
36 var $NO_DELETE_FOLDER;
37 var $NO_DELETE_FILE;
38 var $NO_IMPORT;
39
40 /**
41 * Instance de la classe utils
42 * @var utils
43 */
44 var $f = NULL;
45
46 /**
47 * Instance du filestorage
48 * @var storage
49 */
50 var $filestorage = NULL;
51
52 /**
53 * Nom des fichiers qui ne se sont pas importés
54 * @var array
55 */
56 var $filenameError = array();
57
58 /**
59 * Constructeur
60 */
61 public function __construct($f) {
62
63 //Set des attributs
64 $this->NO_REP = _("Le dossier n'existe pas.");
65 $this->NO_FILES = _("Le dossier est vide.");
66 $this->NO_FILE_EXIST = _("Le fichier n'existe pas.");
67 $this->DOC_NO_CONFORME = _("Le document n'est pas conforme a la regle RG2 : ");
68 $this->NO_LINK = _("Le lien entre le document et le dossier d'instruction n'a pu etre etabli.");
69 $this->NO_MOVE = _("Le fichier n'a pas pu etre deplace.");
70 $this->NO_DELETE_FOLDER = _("Le dossier n'a pas pu etre supprime");
71 $this->NO_DELETE_FILE = _("Le fichier n'a pas pu etre supprime : ");
72 $this->NO_IMPORT = _("L'importation a été annulee.");
73
74 //
75 $this->f = $f;
76 // Permet lors de l'instantiation d'objets métiers d'avoir accès à f
77 $GLOBALS['f'] = $this->f;
78
79 // initialise le msg attribute qui est utilise pour stocker les
80 // messages de retour (reussite ou erreur)
81 $this->msg = '';
82
83 //Instance de filestorage
84 $this->filestorage = $this->f->storage;
85
86 }
87
88 /**
89 * Destructeur
90 */
91 public function __destruct() {
92
93 //Détruit les instance de utils et filestorage
94 unset($this->f);
95 unset($this->filestorage);
96
97 //Détruit la variable globale de 'f'
98 unset($GLOBALS['f']);
99 }
100
101 /**
102 * Récupère l'identifiant du type de document par rapport au code
103 * @param string $code Code du type de document
104 * @return int Identifiant du type de document
105 */
106 private function get_document_numerise_by_code($code) {
107 // Recherche du type de document avec le code de la nomenclature externe
108 // issu du nom du fichier
109 // Dans le nom du fichier des "_" sont utilisés à la place des "-" pour éviter
110 // la confusion entre le code de le pièce et son numéro de version. Pour
111 // retrouver la pièce ils doivent être remplacé.
112 $qres = $this->f->get_one_result_from_db_query(
113 sprintf(
114 'SELECT
115 lien_document_n_type_d_i_t.document_numerise_type
116 FROM
117 %1$slien_document_n_type_d_i_t
118 WHERE
119 lien_document_n_type_d_i_t.code LIKE \'%2$s\'',
120 DB_PREFIXE,
121 $this->f->db->escapeSimple(str_replace('_', '-', $code))
122 ),
123 array(
124 "origin" => __METHOD__,
125 )
126 );
127 $document_numerise_type = $qres["result"];
128 if (! empty($document_numerise_type) && is_numeric($document_numerise_type)) {
129 return $document_numerise_type;
130 }
131
132 //Requête SQL
133 $qres = $this->f->get_one_result_from_db_query(
134 sprintf(
135 'SELECT
136 document_numerise_type
137 FROM
138 %1$sdocument_numerise_type
139 WHERE
140 code = \'%2$s\'',
141 DB_PREFIXE,
142 $this->f->db->escapeSimple($code)
143 ),
144 array(
145 "origin" => __METHOD__,
146 )
147 );
148 $document_numerise_type = $qres["result"];
149 return $document_numerise_type;
150 }
151
152 /**
153 * Ajoute une chaine de caracteres dans le log.
154 * @param string $message Le message qui doit etre ajouté dans le log.
155 */
156 protected function add_to_log($message, $debug = false) {
157 //
158 logger::instance()->log_to_file(self::$log_file, $message);
159 if ($debug === true) {
160 $this->f->addToLog($message, DEBUG_MODE);
161 }
162 }
163
164 /**
165 * Cette fonction permet de recupérer et de classer dans un tableau
166 * la liste des documents présent dans le dossier passé en paramètre
167 * @param string $path Le chemin vers le dossier
168 * @return array Tableau des documents
169 */
170 public function listFiles($path) {
171
172 //Tableau qui sera retourné en fin de traitement
173 $listFiles = array();
174
175 //Ouvre le répertoire
176 $dir = opendir($path);
177
178 //Si un dossier est ouvert
179 if ($dir) {
180
181 //Parcours le dossier
182 while(false !== ($file = readdir($dir))) {
183
184 //Si le document est bien un fichier de type pdf
185 $autorisedExtension = explode(';', $this->f->config['upload_extension']);
186
187 // $this->add_to_log(__METHOD__."(): file = ".$file."; extension ? = ".strstr($file, '.')."; tableau= ".var_export($autorisedExtension, true));
188 if($file != '.' && $file != '..' && !is_dir($dir.$file)
189 && ! empty($autorisedExtension) && is_array($autorisedExtension)
190 && in_array(strtolower(strstr($file, '.')), $autorisedExtension, true) === true) {
191
192 // Alors il est ajouté dans le tableau
193 array_push($listFiles, $file);
194
195 }
196
197 }
198
199 //Ferme le répertoire
200 closedir($dir);
201
202 } else {
203
204 //Sinon renvoi null
205 $this->add_to_log(__METHOD__."(): ".$path." : ".$this->NO_REP);
206 return null;
207 }
208
209 //Si le tableau est vide on retourne null
210 if (count($listFiles) == 0) {
211
212 $this->add_to_log(__METHOD__."(): ".$path." : ".$this->NO_FILES);
213 return null;
214
215 }
216
217 //Retourne le tableau des documents
218 return $listFiles;
219
220 }
221
222 /**
223 * Cette fonction permet de construire les métadonnées d'un document
224 * à partir des informations du nom du fichier
225 * @param string $filename Nom du fichier
226 * @return array Tableau des metadonnées
227 */
228 public function extractMetadataFromFilename($filename) {
229
230 //Tableau qui sera retourné en fin de traitement
231 $metadata = array();
232
233 //Récupération de l'année
234 $year = substr($filename, 0, 4);
235 //Récupération du mois
236 $month = substr($filename, 4, 2);
237 //Récupération du jour
238 $day = substr($filename, 6, 2);
239
240 //Vérification que l'année, le mois et le jour sont des numériques
241 if (is_numeric($year) && is_numeric($month) && is_numeric($day)) {
242
243 //Vérification que cela correspond à une date possible
244 if (checkdate($month, $day, $year)) {
245
246 //Récupération du type de document
247 //Si le séparateur '-' n'est pas présent
248 if (strpos($filename, '-') === false) {
249
250 //On récupère le nom du fichier avant l'extension
251 $type_doc = substr(strstr($filename, '.', true), 8);
252
253 } else {
254
255 //Sinon on récupère le nom du fichier avant le '-'
256 $type_doc = substr(strstr($filename, '-', true), 8);
257
258 }
259
260 //Si aucun type de document n'a pu être extrait
261 if (empty($type_doc)) {
262
263 //On retourne null
264 $this->add_to_log(__METHOD__."(): ".$this->DOC_NO_CONFORME.$filename, true);
265 return null;
266
267 } else {
268
269 //Sinon on ajoute le type de document dans les métadonnées
270 $metadata['title'] = $type_doc;
271
272 }
273
274 //Formate la date du document
275 $metadata["dateEvenementDocument"] = date("d/m/Y", mktime(0, 0, 0, $month, $day, $year));
276
277 //Si le tableau est vide on retourne null
278 if (count($metadata) == 0) {
279
280 $this->add_to_log(__METHOD__."(): ".$this->DOC_NO_CONFORME.$filename, true);
281 return null;
282
283 }
284
285 //On retourne les métadonnées
286 return $metadata;
287 }
288 }
289
290 //Le nom du document n'est pas conforme
291 $this->add_to_log(__METHOD__."(): ".$this->DOC_NO_CONFORME.$filename, true);
292 return null;
293 }
294
295 /**
296 * Cette fonction permet de récupérer des informations sur le fichier
297 * nécessaire pour le filestorage
298 * @param string $path Chemin du dossier
299 * @param string $filename Nom du fichier
300 * @return array Tableau des métadonnées
301 */
302 public function extractMetadataToFilestorage($path, $filename) {
303
304 //Test si le fichier existe
305 if (!file_exists($path.'/'.$filename)) {
306 //
307 $this->add_to_log(__METHOD__.'(): '.$path.'/'.$filename.' : '.$this->NO_FILE_EXIST, true);
308 return null;
309 }
310
311 //Tableau qui sera retourné en fin de traitement
312 $metadata = array();
313
314 //Métadonnées nécessaire au filestorage
315 $metadata["filename"] = $filename;
316 $metadata["size"] = filesize($path.'/'.$filename);
317 $metadata["mimetype"] = mime_content_type($path.'/'.$filename);
318
319 //Si le tableau est vide on retourne null
320 if (count($metadata) == 0) {
321
322 $this->add_to_log(__METHOD__."(): ".$this->DOC_NO_CONFORME.$filename, true);
323 return null;
324
325 }
326
327 //Retourne le tableau des métadonnées
328 return $metadata;
329 }
330
331 /**
332 * Cette fonction permet de créer un document temporaire dans le filesystem
333 * @param string $file_content Contenu du fichier
334 * @param array $metadata Métadonnées du fichier
335 * @return string $uid identifiant du document dans le filesystem
336 */
337 public function createFileTemporary($file_content, $metadata) {
338
339 //Création du fichier sur le filestorage
340 $uid = $this->filestorage->create_temporary($file_content, $metadata);
341
342 //Retourne l'identifiant unique du fichier créé
343 return $uid;
344
345 }
346
347 /**
348 * Permet de lier le document importé à l'application et de le créer dans le filestorage
349 * @param object $document_numerise Instance de la classe document_numerise
350 * @param string $uid Identifiant du fichier temporaire
351 * @param string $dossier Identifiant du dossier d'instruction
352 * @param string $filename Nom du document
353 *
354 * @return boolean Vrai ou faux
355 */
356 public function createDocumentNumerise($document_numerise, $uid, $dossier, $filename) {
357
358 //Maj en ajout
359 $document_numerise->setParameter("maj",0);
360
361 //Extrait les informations du nom du document
362 $metadataFromFilename = $this->extractMetadataFromFilename($filename);
363
364 //Données
365 // Récupèration d'une nature de document numérisé pour pouvoir utiliser
366 // la méthode de récupèration de la valeur de la nature par défaut
367 $docNumNature = $this->f->get_inst__om_dbform(array(
368 'obj' => 'document_numerise_nature',
369 'idx' => ']'
370 ));
371
372 $values = array(
373 'document_numerise' => '',
374 'uid' => 'tmp|'.$uid,
375 'dossier' => $dossier,
376 'nom_fichier' => $filename,
377 'date_creation' => $metadataFromFilename['dateEvenementDocument'],
378 'document_numerise_type' => $this->get_document_numerise_by_code($metadataFromFilename['title']),
379 'uid_dossier_final' => '',
380 'document_numerise_nature' => $docNumNature->get_default_select_value($dossier),
381 'description_type' => '',
382 'document_travail' => false,
383 'uid_thumbnail' => null,
384 );
385
386 //Ajoute dans la table le lien
387 $add = $document_numerise->ajouter($values);
388
389 //Si le document n'est pas ajouté
390 if ($add === false) {
391
392 //Log d'erreur
393 $this->add_to_log(__METHOD__."(): ".$dossier."/".$filename." : "._("Une erreur s'est produite lors de l'ajout du document ").$filename.".", true);
394 $this->add_to_log(__METHOD__."(): ".$document_numerise->msg, true);
395 return false;
396 }
397
398 return $document_numerise->valF['document_numerise'];
399 }
400
401 /**
402 * Permet de déplacer les fichiers créés dans le filestorage vers le dossier
403 * des fichiers traités
404 * @param string $pathSrc Chemin du dossier source
405 * @param string $pathDes Chemin du dossier de destination
406 * @param string $filename Nom du fichier
407 * @return boolean Retourne true si le fichier à été déplacé sinon false
408 */
409 public function moveDocumentNumerise($pathSrc, $pathDes, $filename) {
410
411 //Si le dossier de destination n'existe pas, il est créé
412 if (!file_exists($pathDes)) {
413 mkdir($pathDes);
414 }
415
416 //Déplace le document
417 $move = rename($pathSrc.'/'.$filename, $pathDes.'/'.$filename);
418
419 //Si le déplacement à réussi
420 if ($move === true) {
421 return true;
422 } else {
423 $this->add_to_log(__METHOD__."(): ".$pathSrc."/".$filename." : ".$this->NO_MOVE, true);
424 return false;
425 }
426
427 //Si le deplacement n'est pas fait on retourne false
428 $this->add_to_log(__METHOD__."(): ".$pathSrc."/".$filename." : ".$this->NO_MOVE);
429 return false;
430 }
431
432 /**
433 * Cette fonction permet de vider un répertoire
434 * Si la date d'import du fichier et le nombre de jour ne sont pas renseignés
435 * alors les fichiers sont supprimés sans vérification sur la date
436 * @param string $file Fichier traité
437 * @param date $dateImport Date de l'importation du fichier
438 * @param int $nbDay Nombre de jour à soustraite à la date du jour
439 * @return boolean true si le traitement à été fait sinon false
440 */
441 public function purgeFiles($file, $dateImport = null, $nbDay = null) {
442
443 //Si la date et le nombre de jour ne sont pas renseigné
444 if (($nbDay == 'null' || $nbDay == null || $nbDay == '')
445 || ($dateImport == 'null' || $dateImport == null || $dateImport == '')) {
446
447 //On supprime le fichier sans faire de test
448 $delete_file = unlink($file);
449 if ($delete_file === true) {
450 return true;
451 } else {
452 $this->add_to_log(__METHOD__."(): ".$file." : ".$this->NO_DELETE_FILE.$file, true);
453 return false;
454 }
455
456 //Si la date d'import et le nombre de jour sont renseignés
457 } else {
458
459 //Date d'import dans un format correct pour la comparaison
460 $dateImport = new DateTime($dateImport);
461 $dateImport = $dateImport->format('Ymd');
462
463 //Date limite pour la suppresion des fichier (Date du jour - 60 jours)
464 $dateLimit = date('d-m-Y', strtotime("- $nbDay day", strtotime(date('d-m-Y'))));
465 $dateLimit = new DateTime($dateLimit);
466 $dateLimit = $dateLimit->format('Ymd');
467
468 //Si la date du fichier à dépassé la date limite
469 if ($dateImport <= $dateLimit) {
470
471 //on supprime le fichier
472 $delete_file = unlink($file);
473 if ($delete_file === true) {
474 return true;
475 } else {
476 $this->add_to_log(__METHOD__."(): ".$file." : ".$this->NO_DELETE_FILE.$file, true);
477 return false;
478 }
479
480 }
481
482 }
483
484 //Si aucun traitement n'a été fait on retourne false
485 $this->add_to_log(__METHOD__."(): ".$file." : ".$this->NO_DELETE_FILE.$file);
486 return false;
487
488 }
489
490 /**
491 * Cette fonction permet de supprimer un dossier
492 * @param string $path Chemin du dossier
493 * @return boolean Retourn vrai si le dossier à été supprimé sinon faux
494 */
495 public function deleteFolder($path) {
496
497 //Si le fichier est supprimé on retourne true
498 $delete_folder = rmdir($path);
499 if ($delete_folder === true) {
500 return true;
501 } else {
502 $this->add_to_log(__METHOD__."(): ".$path." : ".$this->NO_DELETE_FOLDER, true);
503 return false;
504 }
505
506 //Si le fichier n'a pas été supprimé on retourne false
507 $this->add_to_log(__METHOD__."(): ".$path." : ".$this->NO_DELETE_FOLDER);
508 return false;
509
510 }
511
512 /**
513 * Cette fonction permet de lancer toutes les fonctions utiles
514 * à l'importation des documents scannés
515 * @param string $pathSrc Le chemin vers le dossier à traiter
516 * @param string $pathDes Le chemin vers le dossier après le traitement
517 * @param string $pathErr Le chemin vers le dossier des fichiers en erreur
518 *
519 * @return boolean true si le traitement à été fait sinon false
520 */
521 function run_import($pathSrc, $pathDes, $pathErr = null) {
522
523 //Récupération du nom du répertoire
524 $foldername = substr(strrchr($pathSrc, "/"), 1);
525
526 //Identifiant du dossier
527 $dossier = str_replace('.', '', $foldername);
528
529 //Vérifie si le numéro de dossier d'instruction est sur quatres chiffres
530 if(preg_match('/[A-Za-z]{2,3}'.strtoupper($this->f->getParameter("departement")).
531 $this->f->getParameter("commune").'[0-9]{2}[0-9]{4}[A-Za-z]{1,5}[0-9]{1,2}/',
532 $dossier)){
533
534 //On modifie le nom du dossier d'instruction pour qu'il ait la numérotation
535 //standard
536 $tempDossier = preg_split('/([A-Za-z]{2,3}'.strtoupper($this->f->getParameter("departement")).
537 $this->f->getParameter("commune").'[0-9]{2})/i', $dossier, 0, PREG_SPLIT_NO_EMPTY |
538 PREG_SPLIT_DELIM_CAPTURE);
539
540 $dossier = $tempDossier[0]."0".$tempDossier[1];
541 }
542
543 //On vérifie que le dossier existe
544 $inst_dossier_instruction = $this->f->get_inst__om_dbform(array(
545 "obj" => "dossier",
546 "idx" => $dossier,
547 ));
548 // Si le dossier n'existe pas on verifie si le dossier existe avec un suffixe sur un seul caractère par exemple M1 au lieu de M01
549 if (!$inst_dossier_instruction->exists()){
550 $dossier = str_replace(' ', '', $dossier);
551 $matches = array();
552 // Cette expression reguliere permet de créer 3 groupe dans le numero de dossier afin
553 // de pouvoir supprimer le 0 du numero de suffixe grace a un intval
554 // Ceci est fait pour pouvoir verifier si le dossier traité existe avec un numero de suffixe sur un seul caractère
555 if (preg_match('/(?P<num_dossier_sans_suffix>^.*?)(?P<lettre_suffix>[A-Za-z]{1,5})(?P<num_suffix>[0-9]{1,2}$)/', $dossier, $matches) === 1) {
556 $dossier = $matches["num_dossier_sans_suffix"] . $matches["lettre_suffix"] . intval($matches["num_suffix"]);
557 $inst_dossier_instruction = $this->f->get_inst__om_dbform(array(
558 "obj" => "dossier",
559 "idx" => $dossier,
560 ));
561 if (!$inst_dossier_instruction->exists()){
562 $this->add_to_log(__METHOD__."(): ".$pathSrc."/ : "._("Le dossier d'instruction n'existe pas.")." ".$this->NO_IMPORT, true);
563 return false;
564 }
565 }
566 else {
567 $this->add_to_log(__METHOD__."(): ".$pathSrc."/ : "._("Le dossier d'instruction n'existe pas.")." ".$this->NO_IMPORT, true);
568 return false;
569 }
570 }
571
572 //Liste les documents contenus dans le dossier
573 $listFiles = $this->listFiles($pathSrc);
574 //Si il n'y a aucun document
575 if ($listFiles === null) {
576 //On annule l'importation
577 $this->add_to_log(__METHOD__."(): ".$pathSrc."/ ".$this->NO_FILES." ".$this->NO_IMPORT);
578 return false;
579 }
580
581 foreach ($listFiles as $key => $filename) {
582
583 //Construit les métadonnées
584 $metadata = array();
585 //Données récupérées pour le filestorage
586 $metadata = $this->extractMetadataToFilestorage($pathSrc, $filename);
587
588 //S'il y a des métadonnées
589 if ($metadata !== null) {
590
591 //Recupère le contenu du fichier
592 $file_content = file_get_contents($pathSrc.'/'.$filename);
593
594 //Créer le fichier temporaire
595 $uid = $this->createFileTemporary($file_content, $metadata);
596 // On vide la mémoire utilisée par le fichier
597 unset($file_content);
598 //Si le fichier est créé
599 if ($uid !== null) {
600
601 //Instancie la class document_numerise
602 $document_numerise = $this->f->get_inst__om_dbform(array(
603 "obj" => "document_numerise",
604 "idx" => "]",
605 ));
606
607 //Valeur retour formulaire
608 $document_numerise->setParameter("retourformulaire", "dossier_instruction");
609
610 //Créer le document sur le filestorage et dans la table document_numerise
611 $createFileStorage = $this->createDocumentNumerise($document_numerise, $uid, $dossier, $filename);
612
613 //Si le document est crée sur le filestorage
614 if ($createFileStorage !== false && $createFileStorage !== 'OP_FAILURE') {
615
616 //On déplace le document créé dans le filestorage
617 //du dossier des "à traiter" vers celui des "traités"
618 $this->moveDocumentNumerise($pathSrc, $pathDes, $filename);
619
620 }
621 else {
622 //On annule l'importation et on move dans le repertoire erreur
623 $this->moveDocumentNumerise($pathSrc, $pathErr, $filename);
624 $this->add_to_log(__METHOD__."(): ".$pathSrc." : "._("Une erreur s'est produite lors de l'ajout du document ").$filename.". ".$this->NO_IMPORT, true);
625 $this->filenameError[] = $filename;
626 }
627 }
628 }
629
630 }//Fin foreach
631
632 //Retourne true
633 return true;
634
635 }
636
637 /**
638 * Cette fonction permet de lancer toutes les fonctions utiles à la purge de dossier
639 * @param string $path Le chemin vers le dossier
640 * @param int $nbDay Nombre de jour à soustraite à la date du jour
641 * @return boolean true si le traitement à été fait sinon false
642 */
643 function run_purge($path, $nbDay = null) {
644
645 //Liste les documents contenus dans le dossier
646 $listFiles = $this->listFiles($path);
647 $count_purged_files = 0;
648
649 if ($listFiles !== null) {
650
651 //Parcours la liste des fichiers
652 foreach ($listFiles as $key => $filename) {
653 //Fichier
654 $file = $path.'/'.$filename;
655 //Si le nombre de jour est renseigné
656 if ($nbDay !== null) {
657
658 //il faut renseigner la date d'import du fichier
659 $dateImport = date("Y-m-d", filemtime($file));
660
661 } else {
662
663 //Sinon la date d'import est null
664 $dateImport = null;
665
666 }
667
668 //S'il n'y pas d'erreur on exécute la fonction purgeFiles
669 if($this->purgeFiles($file, $dateImport, $nbDay)) {
670 unset($listFiles[$key]);
671 $count_purged_files++;
672 }
673
674 }
675
676 }
677
678 //Si il n'y a plus de document
679 if (empty($listFiles)) {
680
681 //on supprime le dossier
682 $deleteFolder = $this->deleteFolder($path);
683 //Si le dossier n'a pas été supprimé on retourne false
684 if (!$deleteFolder) {
685
686 return false;
687 }
688 }
689
690 //Si il n'y a pas d'erreur on retourne true
691 return $count_purged_files;
692
693 }
694
695 }
696
697

Properties

Name Value
svn:keywords "Id"

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26