1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4/**
5 * Class ilTestArchiver
6 *
7 * Helper class to deal with the generation and maintenance of test archives.
8 *
9 * @author Maximilian Becker <mbecker@databay.de>
10 *
11 * @version $Id$
12 *
13 * @ingroup ModulesTest
14 */
15class ilTestArchiver
16{
17    #region Constants / Config
18
19    const DIR_SEP = '/';
20
21    const HTML_SUBMISSION_FILENAME = 'test_submission.html';
22    const PDF_SUBMISSION_FILENAME = 'test_submission.pdf';
23    const PASS_MATERIALS_PATH_COMPONENT = 'materials';
24    const QUESTION_PATH_COMPONENT_PREFIX = 'q_';
25
26    const TEST_BEST_SOLUTION_PATH_COMPONENT = 'best_solution';
27    const HTML_BEST_SOLUTION_FILENAME = 'best_solution.html';
28    const PDF_BEST_SOLUTION_FILENAME = 'best_solution.pdf';
29    const TEST_MATERIALS_PATH_COMPONENT = 'materials';
30
31    const TEST_RESULT_FILENAME = 'test_result_v';
32    const TEST_RESULT_POSTFIX = '.pdf';
33
34    const TEST_OVERVIEW_PDF_FILENAME = 'results_overview_html_v';
35    const TEST_OVERVIEW_PDF_POSTFIX = '.pdf';
36
37    const TEST_OVERVIEW_HTML_FILENAME = 'results_overview_pdf_v';
38    const TEST_OVERVIEW_HTML_POSTFIX = '.html';
39
40    const LOG_DTSGROUP_FORMAT = 'D M j G:i:s T Y';
41    const LOG_ADDITION_STRING = ' Adding ';
42    const LOG_CREATION_STRING = ' Creating ';
43    const LOG_UPDATE_STRING = ' Updating ';
44    const LOG_DELETION_STRING = ' Deleting ';
45
46    const TEST_LOG_FILENAME = 'test.log';
47    const DATA_INDEX_FILENAME = 'data_index.csv';
48    const ARCHIVE_LOG = 'archive.log';
49
50    const EXPORT_DIRECTORY = 'archive_exports';
51
52    #endregion
53
54    /*
55     * Test-Archive Schema:
56     *
57     * <external directory>/<client>/tst_data/archive/tst_<obj_id>/
58     * 		- archive_data_index.dat
59     * 		- archive_log.log
60     *
61     * 		- test_log.log
62     *
63     * 		- test_results_v<n>.pdf
64     * 		- test_results_v<n>.csv
65     *
66     * 		-> best_solution/
67     * 			best_solution_v<n>.pdf
68     * 			-> /materials/q_<question_fi>/<n>_<filename>
69     *
70     * 		-> <year>/<month>/<day>/<ActiveFi>_<Pass>[_<Lastname>][_<Firstname>][_<Matriculation>]/
71     * 			-> test_submission.pdf
72     * 			-> test_submission.html
73     * 			-> test_submission.sig (et al)
74     * 			-> test_result_v<n>.pdf
75     * 			-> /materials_v<n>/<question_fi>/<n>_<filename>
76     */
77
78    #region Properties
79
80    protected $external_directory_path;	/** @var $external_directory_path string External directory base path  */
81    protected $client_id;			 	/** @var $client_id string Client id of the current client */
82    protected $test_obj_id;				/** @var $test_obj_id integer Object-ID of the test, the archiver is instantiated for */
83    protected $archive_data_index;		/** @var $archive_data_index array[string[]] Archive data index as associative array */
84
85    protected $ilDB;					/** @var $ilDB ilDBInterface */
86
87    /**
88     * @var ilTestParticipantData
89     */
90    protected $participantData;
91
92    #endregion
93
94    /**
95     * Returns a new ilTestArchiver object
96     *
97     * @param $test_obj_id integer Object-ID of the test, the archiver is instantiated for.
98     */
99    public function __construct($test_obj_id)
100    {
101        /** @var $ilias ILIAS */
102        global $DIC;
103        $ilias = $DIC['ilias'];
104        $this->external_directory_path = $ilias->ini_ilias->readVariable('clients', 'datadir');
105        $this->client_id = $ilias->client_id;
106        $this->test_obj_id = $test_obj_id;
107        $this->ilDB = $ilias->db;
108
109        $this->archive_data_index = $this->readArchiveDataIndex();
110
111        $this->participantData = null;
112    }
113
114    /**
115     * @return ilTestParticipantData
116     */
117    public function getParticipantData()
118    {
119        return $this->participantData;
120    }
121
122    /**
123     * @param ilTestParticipantData $participantData
124     */
125    public function setParticipantData($participantData)
126    {
127        $this->participantData = $participantData;
128    }
129
130    #region API methods
131
132    /**
133     * Hands in a participants test submission ("a completed test") for archiving.
134     *
135     * The archive takes an html-string and a path to a PDF-file and saves it according to the archives
136     * general structure. The test is identified by active_fi and pass number, allowing to store relevant
137     * files even for anonymous tests.
138     *
139     * @param $active_fi	integer	Active-FI of the test participant
140     * @param $pass			integer	Pass-number of the actual test
141     * @param $html_string	string	HTML-string of the test submission
142     * @param $pdf_path		string	Path to a pdf representation of the test submission.
143     */
144    public function handInParticipantSubmission($active_fi, $pass, $pdf_path, $html_string)
145    {
146        $this->ensureTestArchiveIsAvailable();
147        $this->ensurePassDataDirectoryIsAvailable($active_fi, $pass);
148
149        $pdf_new_path = $this->getPassDataDirectory($active_fi, $pass) . self::DIR_SEP
150            . self::PDF_SUBMISSION_FILENAME;
151        copy($pdf_path, $pdf_new_path);
152        # /home/mbecker/public_html/ilias/trunk-primary/extern/default/tst_data/archive/tst_350/2013/09/19/80_1_root_user_/test_submission.pdf
153        $html_new_path = $this->getPassDataDirectory($active_fi, $pass) . self::DIR_SEP
154            . self::HTML_SUBMISSION_FILENAME;
155        file_put_contents($html_new_path, $html_string);
156
157        $this->logArchivingProcess(date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING . $pdf_new_path);
158        $this->logArchivingProcess(date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING . $html_new_path);
159    }
160
161    /**
162     * Hands in a particpants question material, such as an upload or other binary content.
163     *
164     * @param $active_fi			integer Active-FI of the test participant
165     * @param $pass					integer	Pass-number of the actual test
166     * @param $question_fi			integer Question-FI of the question, the file is to be stored for.
167     * @param $original_filename	string  Original filename of the material to be stored.
168     * @param $file_path			string	Location of the file to be archived
169     */
170    public function handInParticipantQuestionMaterial($active_fi, $pass, $question_fi, $original_filename, $file_path)
171    {
172        $this->ensureTestArchiveIsAvailable();
173        $this->ensurePassDataDirectoryIsAvailable($active_fi, $pass);
174
175        $pass_question_directory = $this->getPassDataDirectory($active_fi, $pass)
176                                    . self::DIR_SEP . self::QUESTION_PATH_COMPONENT_PREFIX . $question_fi;
177        if (!is_dir($pass_question_directory)) {
178            mkdir($pass_question_directory, 0777, true);
179        }
180
181        copy($file_path, $pass_question_directory . self::DIR_SEP . $original_filename);
182
183        $this->logArchivingProcess(
184            date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING
185            . $pass_question_directory . self::DIR_SEP . $original_filename
186        );
187    }
188
189    /**
190     * Hands in a participants file, which is relevant for archiving but an unspecified type.
191     *
192     * Examples for such are signature files, remarks, feedback or the like.
193     *
194     * @param $active_fi			integer Active-FI of the test participant
195     * @param $pass					integer	Pass-number of the actual test
196     * @param $original_filename	string  Original filename of the material to be stored.
197     * @param $file_path			string	Location of the file to be archived
198     */
199    public function handInParticipantMisc($active_fi, $pass, $original_filename, $file_path)
200    {
201        $this->ensureTestArchiveIsAvailable();
202        $this->ensurePassDataDirectoryIsAvailable($active_fi, $pass);
203        $new_path = $this->getPassDataDirectory($active_fi, $pass) . self::DIR_SEP . $original_filename;
204        copy($file_path, $new_path);
205        $this->logArchivingProcess(date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING . $new_path);
206    }
207
208    /**
209     * Hands in the best solution for a test.
210     *
211     * @param $html_string	string	HTML-string of the test submission
212     * @param $pdf_path		string	Path to a pdf representation of the test submission.
213     */
214    public function handInTestBestSolution($html_string, $pdf_path)
215    {
216        $this->ensureTestArchiveIsAvailable();
217
218        $best_solution_path = $this->getTestArchive() . self::DIR_SEP . self::TEST_BEST_SOLUTION_PATH_COMPONENT;
219        if (!is_dir($best_solution_path)) {
220            mkdir($best_solution_path, 0777, true);
221        }
222
223        file_put_contents($best_solution_path . self::DIR_SEP . self::HTML_BEST_SOLUTION_FILENAME, $html_string);
224
225        copy($pdf_path, $best_solution_path . self::DIR_SEP . self::PDF_BEST_SOLUTION_FILENAME);
226
227        $this->logArchivingProcess(
228            date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING
229                . $best_solution_path . self::DIR_SEP . self::HTML_BEST_SOLUTION_FILENAME
230        );
231
232        $this->logArchivingProcess(
233            date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING
234                . $best_solution_path . self::DIR_SEP . self::PDF_BEST_SOLUTION_FILENAME
235        );
236    }
237
238    /**
239     * Hands in a file related to a question in context of the best solution.
240     *
241     * @param $question_fi			integer QuestionFI of the question, material is to be stored for.
242     * @param $orginial_filename	string	Original filename of the material to be stored.
243     * @param $file_path			string  Path to the material to be stored.
244     */
245    public function handInBestSolutionQuestionMaterial($question_fi, $orginial_filename, $file_path)
246    {
247        $this->ensureTestArchiveIsAvailable();
248
249        $best_solution_path = $this->getTestArchive() . self::DIR_SEP . self::TEST_BEST_SOLUTION_PATH_COMPONENT;
250        if (!is_dir($best_solution_path)) {
251            mkdir($best_solution_path, 0777, true);
252        }
253
254        $materials_path = $best_solution_path . self::DIR_SEP . self::TEST_MATERIALS_PATH_COMPONENT;
255        if (!is_dir($materials_path)) {
256            mkdir($materials_path, 0777, true);
257        }
258
259        $question_materials_path = $materials_path . self::DIR_SEP . self::QUESTION_PATH_COMPONENT_PREFIX . $question_fi;
260        if (!is_dir($question_materials_path)) {
261            mkdir($question_materials_path, 0777, true);
262        }
263
264        copy($file_path, $question_materials_path . self::DIR_SEP . $orginial_filename);
265
266        $this->logArchivingProcess(
267            date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING
268                . $question_materials_path . self::DIR_SEP . $orginial_filename
269        );
270    }
271
272    /**
273     * Hands in an individual test result for a pass.
274     *
275     * @param $active_fi	integer ActiveFI of the participant.
276     * @param $pass			integer	Pass of the test.
277     * @param $pdf_path		string 	Path to the PDF containing the result.
278     *
279     * @return void
280     */
281    public function handInTestResult($active_fi, $pass, $pdf_path)
282    {
283        $this->ensureTestArchiveIsAvailable();
284        $this->ensurePassDataDirectoryIsAvailable($active_fi, $pass);
285        $new_path = $this->getPassDataDirectory($active_fi, $pass) . self::DIR_SEP
286            . self::TEST_RESULT_FILENAME . ($this->countFilesInDirectory($this->getPassDataDirectory($active_fi, $pass), self::TEST_RESULT_FILENAME))
287            . self::TEST_RESULT_POSTFIX;
288        copy($pdf_path, $new_path);
289        $this->logArchivingProcess(date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING . $new_path);
290    }
291
292    /**
293     * Hands in a test results overview.
294     *
295     * @param $html_string	string HTML of the test results overview.
296     * @param $pdf_path		string Path
297     */
298    public function handInTestResultsOverview($html_string, $pdf_path)
299    {
300        $this->ensureTestArchiveIsAvailable();
301        $new_pdf_path = $this->getTestArchive() . self::DIR_SEP
302            . self::TEST_OVERVIEW_PDF_FILENAME
303            . $this->countFilesInDirectory($this->getTestArchive(), self::TEST_OVERVIEW_PDF_FILENAME) . self::TEST_OVERVIEW_PDF_POSTFIX;
304        copy($pdf_path, $new_pdf_path);
305        $html_path = $this->getTestArchive() . self::DIR_SEP . self::TEST_OVERVIEW_HTML_FILENAME
306            . $this->countFilesInDirectory($this->getTestArchive(), self::TEST_OVERVIEW_HTML_FILENAME) . self::TEST_OVERVIEW_HTML_POSTFIX;
307        file_put_contents($html_path, $html_string);
308
309        $this->logArchivingProcess(date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING . $new_pdf_path);
310        $this->logArchivingProcess(date(self::LOG_DTSGROUP_FORMAT) . self::LOG_ADDITION_STRING . $html_path);
311    }
312
313    #endregion
314
315    #region TestArchive
316    // The TestArchive lives here: <external directory>/<client>/tst_data/archive/tst_<obj_id>/
317
318    /**
319     * Returns if the archive directory structure for the test the object is created for exists.
320     *
321     * @return bool $hasTestArchive True, if the archive directory structure exists.
322     */
323    protected function hasTestArchive()
324    {
325        return is_dir($this->getTestArchive());
326    }
327
328    /**
329     * Creates the directory for the test archive.
330     */
331    protected function createArchiveForTest()
332    {
333        ilUtil::makeDirParents($this->getTestArchive());
334        //mkdir( $this->getTestArchive(), 0777, true );
335    }
336
337    /**
338     * Returns the (theoretical) path to the archive directory of the test, this object is created for.
339     *
340     * @return string $test_archive Path to this tests archive directory.
341     */
342    protected function getTestArchive()
343    {
344        $test_archive_directory = $this->external_directory_path . self::DIR_SEP . $this->client_id . self::DIR_SEP . 'tst_data'
345            . self::DIR_SEP . 'archive' . self::DIR_SEP . 'tst_' . $this->test_obj_id;
346        return $test_archive_directory;
347    }
348
349    /**
350     * Ensures the availability of the test archive directory.
351     *
352     * Checks if the directory exists and creates it if necessary.
353     *
354     * @return void
355     */
356    protected function ensureTestArchiveIsAvailable()
357    {
358        if (!$this->hasTestArchive()) {
359            $this->createArchiveForTest();
360        }
361        return;
362    }
363
364    /**
365     * Replaces the test-log with the current one.
366     *
367     * @return void
368     */
369    public function updateTestArchive()
370    {
371        $query = 'SELECT * FROM ass_log WHERE obj_fi = ' . $this->ilDB->quote($this->test_obj_id, 'integer');
372        $result = $this->ilDB->query($query);
373
374        $outfile_lines = '';
375        /** @noinspection PhpAssignmentInConditionInspection */
376        while ($row = $this->ilDB->fetchAssoc($result)) {
377            $outfile_lines .= "\r\n" . implode("\t", $row);
378        }
379        file_put_contents($this->getTestArchive() . self::DIR_SEP . self::TEST_LOG_FILENAME, $outfile_lines);
380
381        // Generate test pass overview
382        $test = new ilObjTest($this->test_obj_id, false);
383        require_once 'Modules/Test/classes/class.ilParticipantsTestResultsGUI.php';
384        $gui = new ilParticipantsTestResultsGUI();
385        $gui->setTestObj($test);
386        require_once 'Modules/Test/classes/class.ilTestObjectiveOrientedContainer.php';
387        $objectiveOrientedContainer = new ilTestObjectiveOrientedContainer();
388        $gui->setObjectiveParent($objectiveOrientedContainer);
389        $array_of_actives = array();
390        $participants = $test->getParticipants();
391
392        foreach ($participants as $key => $value) {
393            $array_of_actives[] = $key;
394        }
395        $output_template = $gui->createUserResults(true, false, true, $array_of_actives);
396
397        $filename = realpath($this->getTestArchive()) . self::DIR_SEP . 'participant_pass_overview.pdf';
398        ilTestPDFGenerator::generatePDF($output_template->get(), ilTestPDFGenerator::PDF_OUTPUT_FILE, $filename, PDF_USER_RESULT);
399
400        return;
401    }
402
403    public function ensureZipExportDirectoryExists()
404    {
405        if (!$this->hasZipExportDirectory()) {
406            $this->createZipExportDirectory();
407        }
408    }
409
410    /**
411     * Returns if the export directory for zips exists.
412     *
413     * @return bool
414     */
415    public function hasZipExportDirectory()
416    {
417        return is_dir($this->getZipExportDirectory());
418    }
419
420    protected function createZipExportDirectory()
421    {
422        mkdir($this->getZipExportDirectory(), 0777, true);
423    }
424
425    /**
426     * Return the export directory, where zips are placed.
427     *
428     * @return string
429     */
430    public function getZipExportDirectory()
431    {
432        return $this->external_directory_path . self::DIR_SEP . $this->client_id . self::DIR_SEP . 'tst_data'
433            . self::DIR_SEP . self::EXPORT_DIRECTORY . self::DIR_SEP . 'tst_' . $this->test_obj_id;
434    }
435
436    /**
437     * Generate the test archive for download.
438     *
439     * @return void
440     */
441    public function compressTestArchive()
442    {
443        $this->updateTestArchive();
444        $this->ensureZipExportDirectoryExists();
445
446        $zip_output_path = $this->getZipExportDirectory();
447        $zip_output_filename = 'test_archive_obj_' . $this->test_obj_id . '_' . time() . '_.zip';
448
449        ilUtil::zip($this->getTestArchive(), $zip_output_path . self::DIR_SEP . $zip_output_filename, true);
450        return;
451    }
452
453    #endregion
454
455    #region PassDataDirectory
456    // The pass data directory contains all data relevant for a participants pass.
457    // In addition to the test-archive-directory, this directory lives here:
458    // .../<year>/<month>/<day>/<ActiveFi>_<Pass>[_<Lastname>][_<Firstname>][_<Matriculation>]/
459    // Lastname, Firstname and Matriculation are not mandatory in the directory name.
460
461    /**
462     * Checks if the directory for pass data is available.
463     *
464     * @param $active_fi	integer ActiveFI of the pass.
465     * @param $pass			integer Pass-number of the pass.
466     *
467     * @return bool $hasPassDataDirectory True, if the pass data directory exists.
468     */
469    protected function hasPassDataDirectory($active_fi, $pass)
470    {
471        $pass_data_dir = $this->getPassDataDirectory($active_fi, $pass);
472        return is_dir($this->getPassDataDirectory($active_fi, $pass));
473    }
474
475    /**
476     * Creates pass data directory
477     *
478     * @param $active_fi integer ActiveFI of the participant.
479     * @param $pass		 integer Pass number of the test.
480     *
481     * @return void
482     */
483    protected function createPassDataDirectory($active_fi, $pass)
484    {
485        mkdir($this->getPassDataDirectory($active_fi, $pass), 0777, true);
486        return;
487    }
488
489    private function buildPassDataDirectory($active_fi, $pass)
490    {
491        foreach ($this->archive_data_index as $data_index_entry) {
492            if ($data_index_entry != null && $data_index_entry['identifier'] == $active_fi . '|' . $pass) {
493                array_shift($data_index_entry);
494                return $this->getTestArchive() . self::DIR_SEP . implode(self::DIR_SEP, $data_index_entry);
495            }
496        }
497
498        return null;
499    }
500
501    /**
502     * Returns the pass data directory.
503     *
504     * @param $active_fi integer ActiveFI of the participant.
505     * @param $pass	     integer Pass number of the test.
506     *
507     * @return string $pass_data_directory Path to the pass data directory.
508     */
509    protected function getPassDataDirectory($active_fi, $pass)
510    {
511        $passDataDir = $this->buildPassDataDirectory($active_fi, $pass);
512
513        if (!$passDataDir) {
514            if ($this->getParticipantData()) {
515                $usrData = $this->getParticipantData()->getUserDataByActiveId($active_fi);
516                $user = new ilObjUser();
517                $user->setFirstname($usrData['firstname']);
518                $user->setLastname($usrData['lastname']);
519                $user->setMatriculation($usrData['matriculation']);
520                $user->setFirstname($usrData['firstname']);
521            } else {
522                global $DIC;
523                $ilUser = $DIC['ilUser'];
524                $user = $ilUser;
525            }
526
527            $this->appendToArchiveDataIndex(
528                date(DATE_ISO8601),
529                $active_fi,
530                $pass,
531                $user->getFirstname(),
532                $user->getLastname(),
533                $user->getMatriculation()
534            );
535
536            $passDataDir = $this->buildPassDataDirectory($active_fi, $pass);
537        }
538
539        return $passDataDir;
540    }
541
542    /**
543     * Ensures the availability of the participant data directory.
544     *
545     * Checks if the directory exists and creates it if necessary.
546     *
547     * @param $active_fi	integer Active-FI of the test participant
548     * @param $pass			integer Pass-number of the actual test
549     *
550     * @return void
551     */
552    protected function ensurePassDataDirectoryIsAvailable($active_fi, $pass)
553    {
554        if (!$this->hasPassDataDirectory($active_fi, $pass)) {
555            $this->createPassDataDirectory($active_fi, $pass);
556        }
557        return;
558    }
559
560    #endregion
561
562    #region PassMaterialsDirectory
563
564    /**
565     * Returns if the pass materials directory exists for a given pass.
566     *
567     * @param $active_fi	integer ActiveFI for the participant.
568     * @param $pass			integer Pass number.
569     *
570     * @return bool			$hasPassmaterialsDirectory True, if the directory exists.
571     */
572    protected function hasPassMaterialsDirectory($active_fi, $pass)
573    {
574        /** @noinspection PhpUsageOfSilenceOperatorInspection */
575        if (@is_dir($this->getPassMaterialsDirectory($active_fi, $pass))) {
576            return true;
577        }
578        return false;
579    }
580
581    /**
582     * Creates pass materials directory.
583     *
584     * @param $active_fi	integer	ActiveFI of the participant.
585     * @param $pass			integer Pass number of the test.
586     *
587     * @return void
588     */
589    protected function createPassMaterialsDirectory($active_fi, $pass)
590    {
591        // Data are taken from the current user as the implementation expects the first interaction of the pass
592        // takes place from the usage/behaviour of the current user.
593
594        if ($this->getParticipantData()) {
595            $usrData = $this->getParticipantData()->getUserDataByActiveId($active_fi);
596            $user = new ilObjUser();
597            $user->setFirstname($usrData['firstname']);
598            $user->setLastname($usrData['lastname']);
599            $user->setMatriculation($usrData['matriculation']);
600            $user->setFirstname($usrData['firstname']);
601        } else {
602            global $DIC;
603            $ilUser = $DIC['ilUser'];
604            $user = $ilUser;
605        }
606
607        $this->appendToArchiveDataIndex(
608            date('Y'),
609            $active_fi,
610            $pass,
611            $user->getFirstname(),
612            $user->getLastname(),
613            $user->getMatriculation()
614        );
615        mkdir($this->getPassMaterialsDirectory($active_fi, $pass), 0777, true);
616    }
617
618    /**
619     * Returns the pass materials directory.
620     *
621     * @param $active_fi	integer ActiveFI of the participant.
622     * @param $pass			integer Pass number.
623     *
624     * @return string $pass_materials_directory Path to the pass materials directory.
625     */
626    protected function getPassMaterialsDirectory($active_fi, $pass)
627    {
628        $pass_data_directory = $this->getPassMaterialsDirectory($active_fi, $pass);
629        return $pass_data_directory . self::DIR_SEP . self::PASS_MATERIALS_PATH_COMPONENT;
630    }
631
632    /**
633     * Ensures the availability of the pass materials directory.
634     *
635     * Checks if the directory exists and creates it if necessary.
636     *
637     * @param $active_fi	integer Active-FI of the test participant
638     * @param $pass			integer Pass-number of the actual test
639     *
640     */
641    protected function ensurePassMaterialsDirectoryIsAvailable($active_fi, $pass)
642    {
643        if (!$this->hasPassMaterialsDirectory($active_fi, $pass)) {
644            $this->createPassMaterialsDirectory($active_fi, $pass);
645        }
646    }
647
648    #endregion
649
650    /**
651     * Reads the archive data index.
652     *
653     * @return array[array] $archive_data_index Archive data index.
654     */
655    protected function readArchiveDataIndex()
656    {
657        /**
658         * The Archive Data Index is a csv-file containing the following columns
659         * <active_fi>|<pass>|<yyyy>|<mm>|<dd>|<directory>
660         */
661        $data_index_file = $this->getTestArchive() . self::DIR_SEP . self::DATA_INDEX_FILENAME;
662
663        $contents = array();
664
665        /** @noinspection PhpUsageOfSilenceOperatorInspection */
666        if (@file_exists($data_index_file)) {
667            $lines = explode("\n", file_get_contents($data_index_file));
668            foreach ($lines as $line) {
669                $line_items = explode('|', $line);
670                $line_data['identifier'] = $line_items[0] . '|' . $line_items[1];
671                $line_data['yyyy'] = $line_items[2];
672                $line_data['mm'] = $line_items[3];
673                $line_data['dd'] = $line_items[4];
674                $line_data['directory'] = $line_items[5];
675                $contents[] = $line_data;
676            }
677        }
678        return $contents;
679    }
680
681    /**
682     * Appends a line to the archive data index.
683     *
684     * @param $date             string  Date for the directories path.
685     * @param $active_fi        integer ActiveFI of the participant.
686     * @param $pass             integer Pass number of the participant.
687     * @param $user_firstname	string	User firstname.
688     * @param $user_lastname	string  User lastname.
689     * @param $matriculation    string Matriculation number of the user.
690     *
691     * @return void
692     */
693    protected function appendToArchiveDataIndex($date, $active_fi, $pass, $user_firstname, $user_lastname, $matriculation)
694    {
695        $line = $this->determinePassDataPath($date, $active_fi, $pass, $user_firstname, $user_lastname, $matriculation);
696
697        $this->archive_data_index[] = $line;
698        $output_contents = '';
699
700        foreach ($this->archive_data_index as $line_data) {
701            if ($line_data['identifier'] == "|") {
702                continue;
703            }
704            $output_contents .= implode('|', $line_data) . "\n";
705        }
706
707        file_put_contents($this->getTestArchive() . self::DIR_SEP . self::DATA_INDEX_FILENAME, $output_contents);
708        $this->readArchiveDataIndex();
709        return;
710    }
711
712    /**
713     * Determines the pass data path.
714     *
715     * @param $date
716     * @param $active_fi
717     * @param $pass
718     * @param $user_firstname
719     * @param $user_lastname
720     * @param $matriculation
721     *
722     * @return array
723     */
724    protected function determinePassDataPath($date, $active_fi, $pass, $user_firstname, $user_lastname, $matriculation)
725    {
726        $date = date_create_from_format(DATE_ISO8601, $date);
727        $line = array(
728            'identifier' => $active_fi . '|' . $pass,
729            'yyyy' => date_format($date, 'Y'),
730            'mm' => date_format($date, 'm'),
731            'dd' => date_format($date, 'd'),
732            'directory' => $active_fi . '_' . $pass . '_' . $user_firstname . '_' . $user_lastname . '_' . $matriculation
733        );
734        return $line;
735    }
736
737    /**
738     * Logs to the archive log.
739     *
740     * @param $message string Complete log message.
741     *
742     * @return void
743     */
744    protected function logArchivingProcess($message)
745    {
746        $archive = $this->getTestArchive() . self::DIR_SEP . self::ARCHIVE_LOG;
747        if (file_exists($archive)) {
748            $content = file_get_contents($archive) . "\n" . $message;
749        } else {
750            $content = $message;
751        }
752
753        file_put_contents($archive, $content);
754    }
755
756    /**
757     * Returns the count of files in a directory, eventually matching the given, optional, pattern.
758     *
759     * @param      		  $directory
760     * @param null|string $pattern
761     *
762     * @return integer
763     */
764    protected function countFilesInDirectory($directory, $pattern = null)
765    {
766        $filecount = 0;
767
768        /** @noinspection PhpAssignmentInConditionInspection */
769        if ($handle = opendir($directory)) {
770            while (($file = readdir($handle)) !== false) {
771                if (!in_array($file, array( '.', '..' )) && !is_dir($directory . $file)) {
772                    if ($pattern && strpos($file, $pattern) === 0) {
773                        $filecount++;
774                    }
775                }
776            }
777        }
778        return $filecount;
779    }
780}
781