Browse Source

Implemenent text file parser

Update controller/forms.php to store data in the new DB structure

Update explorer/ to required user email and password to access their questionnaire

Implement template functions and files to most html pages

Implement input validator to check user provided data
Lucas de Souza 4 years ago
parent
commit
c5e27e23db

+ 86 - 0
app/create_form.php

@@ -0,0 +1,86 @@
+<?php
+require_once('../controller/validator.php');
+require_once('../controller/formparser.php');
+require_once('../controller/forms.php');
+require_once('../templates/templates.php');
+require_once('../controller/generateform.php');
+
+$KEY_SIZE = 15; // max. 32
+$FOLDER_SIZE = 10; // max. 32
+
+function generateKeys ($email, $title) {
+    global $KEY_SIZE;
+    global $FOLDER_SIZE;
+    $date = new DateTime();
+    $input = $date->format("c").$email.$title;
+    $sha = hash("sha256", $input);
+    $key = array();
+    $folder = array();
+    $i = 0;
+    for (; $i < $KEY_SIZE; $i++) {
+        $key[] = $sha[$i];
+    }
+    for ($j = 0;$j < $FOLDER_SIZE; $j++ ) {
+        $folder[] = $sha[$i+$j];
+    }
+    return [implode("", $key), implode("", $folder)];
+}
+
+if (empty($_POST)) {
+    generateCreateForm();
+} else {
+    proccessRequest($_POST);
+}
+
+function generateCreateForm () {
+    $template = getTemplate('create_form.html');
+    $context = array('post_url' => htmlspecialchars($_SERVER["PHP_SELF"]));
+    $template = parseTemplate($template, $context);
+    echo $template;
+}
+
+function proccessRequest ($data) {
+    try {
+        Validator::check(['email', 'title', 'description','thanks'], $data);
+        Validator::check(['formSource'], $_FILES);
+        $from = array();
+        $form['email'] = Validator::email($data['email']);
+        $form['title'] = Validator::str($data['title']);
+        $form['description'] = Validator::str($data['description']);
+        $form['thanks'] = Validator::str($data['thanks']);
+
+        $upload = Validator::file($_FILES['formSource'], 'txt');
+
+        $formSource = file_get_contents($upload['tmp_name']);
+        $reader = new SourceReader($formSource);
+        $parser = new Parser($reader);
+        $form['questions'] = $parser->parse();
+        $keys = generateKeys($form['email'], $form['title']);
+        $form['id'] = $keys[1];
+        $html = generateFormHTML($form);
+        storeNewForm($form, $keys, $formSource);
+        $path = saveHTML($form['title'], $keys[1], $html);
+        echo "Questionário gerado com sucesso. Acesse o <a href='../forms/$path'>link</a> para visualizar seu questionario!";
+
+    } catch (Exception $e) {
+        echo 'Os dados do formulários são inválidos! Certifique-se que todos os dados foram enviados e estão no formato correto: '.$e->getMessage().'<br/>';
+        return;
+    }
+}
+
+function cleanTitle ($title) {
+    $clearstring = filter_var($title, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH);
+    $clearstring = trim(preg_replace("/[^a-zA-Z0-9 ]/",'',$clearstring));
+    $clearstring = trim(preg_replace("/[[:space:]]+/",'_',$clearstring));
+    return strtolower($clearstring);
+}
+
+function saveHTML ($title, $folder,$html) {
+    $date = new DateTime();
+    $cleanTitle = cleanTitle($title);
+    $datePart = $date->format("Y-m-j");
+    $filePath = "$datePart-$cleanTitle-$folder.html";
+    file_put_contents(__DIR__.'/../forms/'.$filePath, $html);
+    return $filePath;
+}
+

+ 0 - 0
app/index.php


+ 40 - 17
app/post.php

@@ -3,13 +3,13 @@
 /**
  * Este arquivo é parte do software linequest
  * Ambiente de questionários para a coleta de dados
- * 
+ *
  * Laboratório de Informática na Educação - LInE
  * https://www.usp.br/line/
- * 
+ *
  * Utilize os atributos definidos abaixo para
  * configurar o ambiente de questionários.
- * 
+ *
  * @author Lucas Calion
  * @author Igor Félix
  */
@@ -27,7 +27,6 @@
      * @param $data - representa o conteúdo de $_POST ou $_GET
      */
     function execute ($data) {
-
         if (count($data) < 1) {
             echo print_error('empty_data');
             exit;
@@ -37,25 +36,49 @@
             echo print_error('key_missing');
             exit;
         }
-        
-        $data["ip"] = get_user_ip();
-        $data["timestamp"] = time();
+
+        if (!isset($data['uuid'])) {
+            echo print_error('key_missing');
+            exit;
+        }
 
         require_once('../controller/forms.php');
-        try {
-            if (isset($data['uuid'])) {
-                store(json_encode($data, JSON_UNESCAPED_UNICODE), $data['uuid']);
-            } else {
-                store(json_encode($data, JSON_UNESCAPED_UNICODE));
+
+        $hash = $data['form'];
+
+        // load question mapping
+        $info = getQuestionaireInfo($hash);
+        $mapping = loadQuestionsMapping($info['qid']);
+        $texts = loadQuestionsText($info['qid']);
+
+        foreach ($mapping as $map) {
+            $id = "q$map->number";
+            if(isset($data[$id]) && $data[$id] === $map->value) {
+                $data[$id] = $map->text;
             }
+        }
+
+        foreach ($texts as $question) {
+            $id = "q$question->number";
+            $data[$question->text] = $data[$id];
+            unset($data[$id]);
+        }
+
+        $data['form'] = $info['title'].'-'.$data['form'];
+
+        // $data["ip"] = get_user_ip();
+        // $data["timestamp"] = time();
+
+        try {
+            storeUserSubmission(json_encode($data, JSON_UNESCAPED_UNICODE), $info['qid'], $data['uuid']);
         } catch (Exception $e) {
             echo print_error($e->getMessage());
             exit;
         }
-        
-        header('Location: thanks.html');
+
+        header("Location: thanks.php?id=$hash");
     }
-    
+
     /**
      * Imprime as mensagens de erro padrão:
      * @param $code - identifica o erro a ser retornado
@@ -64,7 +87,7 @@
         switch($code) {
             case 'key_missing':
                 return "Error: The data could not be stored. <br>Reason: The 'form' parameter was not sent in the request. This parameter is required, it identifies which form this data is associated.";
-            
+
             case 'empty_data':
                 return "Error: The data could not be stored. <br>Reason: None parameter was sent in the request.<br>";
         }
@@ -81,4 +104,4 @@
         }
         return $ip;
     }
-?>
+?>

+ 19 - 0
app/thanks.php

@@ -0,0 +1,19 @@
+<?php
+require_once('../templates/templates.php');
+require_once('../controller/forms.php');
+require_once('../controller/validator.php');
+
+if (!empty($_GET)) {
+    Validator::check(['id'], $_GET);
+    $id = Validator::str($_GET['id']);
+    try {
+        $context = getFormThanks($id);
+        $template = getTemplate('thanks.html');
+        echo parseTemplate($template,$context);
+    } catch (Exception $e) {
+        http_response_code(404);
+        exit;
+    }
+} else {
+    header('Location: /');
+}

+ 0 - 0
assets/css/bootstrap.min.css


File diff suppressed because it is too large
+ 2 - 0
assets/foundation.min.css


+ 0 - 0
assets/img/logo.png


+ 0 - 0
assets/img/request.png


+ 0 - 0
assets/img/structure.png


+ 0 - 0
assets/js/bootstrap.min.js


+ 0 - 0
assets/js/jquery-3.3.1.slim.min.js


+ 0 - 0
assets/js/popper.min.js


File diff suppressed because it is too large
+ 1 - 0
assets/populatejs.min.js


+ 0 - 0
config/index.php


+ 302 - 0
controller/formparser.php

@@ -0,0 +1,302 @@
+<?php
+
+class SourceReader {
+
+   private $source;
+   private $pos = 0;
+   private $size;
+
+   function __construct ($source) {
+      $this->source = str_replace(array("\r\n","\r"), "\n", $source);
+      $this->size = strlen($this->source);
+   }
+
+
+   function remaining () {
+      return $this->size - $this->pos;
+   }
+
+   function consume ($count = 1) {
+      $val = '';
+      for($i = 0; $i < $count; $i++) {
+         if ($this->eof()) throw new Exception('End of file reached during read.', 801);
+         $val = $val . $this->source[$this->pos];
+         $this->pos++;
+      }
+      return $val;
+   }
+
+   function consumeUntil ($char, $inclusive = false, $stopAtEOF = false) {
+      $skipped = '';
+      while($this->source[$this->pos] !== $char) {
+         if ($this->eof()) {
+            if ($stopAtEOF) return $skipped;
+            throw new Exception('End of file reached during read.', 801);
+         }
+         $skipped = $skipped . $this->source[$this->pos];
+         $this->pos++;
+      }
+      if ($inclusive) {
+         $skipped = $skipped . $char;
+         $this->pos++;
+      }
+      return $skipped;
+   }
+
+   function consumeWhile ($char) {
+      $skipped = '';
+      while($this->source[$this->pos] === $char) {
+         if ($this->eof()) throw new Exception('End of file reached during read.', 801);
+         $skipped = $skipped . $this->source[$this->pos];
+         $this->pos++;
+      }
+      return $skipped;
+   }
+
+   function eof () {
+      return $this->pos >= $this->size;
+   }
+
+   function getChar () {
+      return $this->source[$this->pos];
+   }
+
+   function lookAhead ($steps) {
+      if (($this->pos + $steps) > $this->size) {
+         throw new Exception('End of file reached during read.', 801);
+      }
+      $result = "";
+      $i = $this->pos;
+      for (; $i < ($this->pos + $steps); $i++) {
+         $result = $result . $this->source[$i];
+      }
+      return $result;
+   }
+
+   function lookFoward ($pos) {
+      if ($pos + $this->pos > $this->size) {
+         throw new Exception('End of file reached during read.', 801);
+      }
+      return $this->source[$pos + $this->pos];
+   }
+
+
+   function lookBack ($steps) {
+      if (($this->pos - $steps) < 0) {
+         throw new Exception('End of file reached during read.', 801);
+      }
+      $result = "";
+      $i = $this->pos;
+      for (; $i > ($this->pos - $steps); $i--) {
+         $result = $result . $this->source[$i];
+      }
+      return $result;
+   }
+
+}
+
+class Parser {
+   private $reader;
+   private $line = 1;
+
+   function __construct ($reader) {
+      $this->reader =  $reader;
+   }
+
+   function skipBlanklines () {
+      while (in_array($this->reader->getChar(), array(" ", "\n"))) {
+         $this->reader->consumeWhile(' ');
+         if ($this->reader->getChar() === "\n") {
+            $this->line++;
+            $this->reader->consume();
+         }
+      }
+   }
+
+   function parse () {
+      $numbers = array();
+      $questions = array();
+      try {
+         while (!$this->reader->eof()) {
+            $this->skipBlanklines();
+            $char = $this->reader->getChar();
+            if ($char === '#') {
+               // skip comment
+               $this->reader->consumeUntil("\n", true);
+               $this->line++;
+               continue;
+            }
+            $char = $this->reader->consume();
+
+            if ($char === ':') {
+               $foward = $this->reader->getChar();
+               if($foward !== ':') {
+                  $actual = $char.$foward;
+                  throw new Exception("Error na linha $this->line. ${actual} não é uma expressão válida, talvez você quisesse '::' (quebra de página )?", 800);
+               }
+               $this->reader->consumeUntil("\n", true);
+               $this->line++;
+               $pagebreak = new stdClass;
+               $pagebreak->type = 'PAGEBREAK';
+               $questions[] = $pagebreak;
+               continue;
+            }
+
+            if (is_numeric($char)) {
+               $foward = $this->reader->getChar();
+               if (is_numeric($foward)) {
+                  // two digits
+                  $char = $char . $foward;
+                  $this->reader->consume();
+                  $foward = $this->reader->getChar();
+               }
+
+               if ($foward !== '.') {
+                  throw new Exception("Error na linha $this->line. O número da questão deve ser seguido de um .(ponto).", 800);
+               }
+
+               if (in_array($char, $numbers)) {
+                  throw new Exception('Error na linha '.($this->line).'. O número '.$char.' já foi utilizado anteriormente.', 802);
+               }
+               $numbers[] = $char;
+               // read '.'
+               $this->reader->consume();
+               $questions[] = $this->parseQuestion($char);
+            } else {
+               throw new Exception('Error na linha '.($this->line).'. Você deve informar uma questão nessa linha', 800);
+            }
+         }
+      } catch (Exception $e) {
+         $message = $e->getMessage();
+         // echo $e->getTraceAsString();
+         throw new Exception ("Erro de processamento: $message Total de linhas processadas: $this->line", 800);
+      }
+      if (count($questions) == 0) {
+         throw new Exception('Error na linha '.($this->line).'. Questionário deve ter ao menos uma questão.', 806);
+      }
+      return $questions;
+   }
+
+   function parseQuestion ($number) {
+      $question = new stdClass();
+      $question->number = 'q'.$number;
+      // params
+      $this->reader->consumeWhile(' ');
+      if ($this->reader->getChar() !== '[') {
+         throw new Exception('Error na linha '.($this->line).'. Toda questão deve ter seus parâmetros (M, D, S, I e *) definidos entre [ ].', 800);
+      }
+      $this->reader->consume();
+      $params = $this->reader->consumeUntil("]");
+      $params = str_replace(' ', '', $params);
+      $validTypes = ['M','D','S', 'I'];
+      if (strlen($params) === 0) {
+         throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803);
+      }
+      $found = false;
+      foreach ($validTypes as $type) {
+         if (strpos($params, $type) !== FALSE) {
+            if ($found) {
+               throw new Exception('Error na linha '.($this->line).'. Questões devem ter apenas um parâmetro de tipo: M, D, I ou S.', 803);
+            }
+            $question->type = $type;
+            $found = true;
+         }
+      }
+
+      if (!$found) {
+         throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803);
+      }
+
+      $params = str_replace($validTypes, '', $params);
+      if(strlen($params) > 0) {
+         if (strpos($params, '*') !== FALSE) {
+            $question->required = true;
+            $params = str_replace('*', '', $params);
+         } else {
+            throw new Exception('Error na linha '.($this->line).'. Parâmetro desconhecido:'.$params.'. Os parâmetros válidos são: M, D, S, I e *.', 800);
+         }
+      }
+      if(strlen($params) > 0) {
+         throw new Exception('Error na linha '.($this->line).'. Parâmetro desconhecido:'.$params.'. Os parâmetros válidos são: M, D, S, I e *.', 800);
+      }
+      $this->reader->consume();
+
+      $this->reader->consumeWhile(' ');
+      $question->text = trim($this->reader->consumeUntil("\n"));
+      if (strlen($question->text) === 0) {
+         throw new Exception('Error na linha '.($this->line).'. Questões devem ter um texto associado!', 800);
+      }
+      $this->reader->consume();
+      $this->line++;
+
+      // echo $question->type;
+      // echo '<br/>';
+      if (in_array($question->type, array('M','S'))) {
+         $this->parseChoices($question);
+         if (count($question->choices) < 2) {
+            throw new Exception('Error na linha '.($this->line).'. A questão '.$question->number.' é de múltipla escolha e deve ter pelo menos duas alternativas.', 805);
+         }
+      }
+      return $question;
+   }
+
+   function parseChoices ($question) {
+      $question->choices = array();
+      $letters = array();
+      while (!$this->reader->eof()) {
+         $this->skipBlanklines();
+         if($this->reader->eof()){
+            // blank lines at the end of file
+            continue;
+         }
+         $char = $this->reader->getChar();
+         $isPoint = $this->reader->lookFoward(1);
+         if ($char === '#') {
+            $this->reader->consumeUntil("\n", true);
+            $this->line++;
+            continue;
+         } else if ($char === ':' || is_numeric($char)) {
+            break;
+         } else if ($isPoint !== '.') {
+            // must be CONDICAO
+            $cond = "CONDICAO:";
+            $actual = $this->reader->lookAhead(strlen($cond));
+            if(strcmp($cond, $actual) !== 0) {
+               throw new Exception("Error na linha ".($this->line).". A expressão ".$actual." é desconhecida, o único valor válido nesse contexto seria a expressão 'CONDICAO:'.", 800);
+            }
+
+            // TODO parse conditionals
+            $this->reader->consumeUntil("\n", true);
+            $this->line++;
+            break;
+
+         }
+
+         // echo 'Choice:'.$char;
+         // echo '<br/>';
+         if (in_array($char, $letters)) {
+            throw new Exception('Error na linha '.($this->line).'. A letra '.$char.' já foi utilizada anteriormente nesta mesma questão.', 804);
+         } else if (!preg_match('/[A-Z]/', $char)) {
+            throw new Exception('Error na linha '.($this->line).'. As alternativas das questões devem ser letras maiúsculas A-Z.', 800);
+         } else if ($isPoint !== '.') {
+            throw new Exception('Error na linha '.($this->line).'. A letra da alternativa deve ser seguida de um .(ponto)!', 800);
+         }
+         $choice = new stdClass();
+         $choice->value = $char;
+         $this->reader->consumeUntil(".", true);
+         $this->reader->consumeWhile(' ');
+
+         // text can be last line of valid text
+         $choice->text = trim($this->reader->consumeUntil("\n", false, true));
+         if (strlen($choice->text) === 0) {
+            throw new Exception('Error na linha '.($this->line).'. Alternativas devem ter um texto associado!', 800);
+         }
+         if(!$this->reader->eof()) {
+            $this->reader->consume();
+            $this->line++;
+         }
+         $question->choices[] = $choice;
+      }
+   }
+
+}

+ 322 - 80
controller/forms.php

@@ -3,13 +3,13 @@
 /**
  * Este arquivo é parte do software linequest
  * Ambiente de questionários para a coleta de dados
- * 
+ *
  * Laboratório de Informática na Educação - LInE
  * https://www.usp.br/line/
- * 
+ *
  * Utilize os atributos definidos abaixo para
  * configurar o ambiente de questionários.
- * 
+ *
  * @author Lucas Calion
  * @author Igor Félix
  */
@@ -28,140 +28,382 @@
  function get_id_by_uuid ($uuid) {
     global $DB; connect();
 
-    $sql = "SELECT * FROM records";
+    $sql = "SELECT id FROM answers WHERE uuid = ?";
 
     if (!($stmt = $DB->prepare($sql))) {
-        echo "Prepare failed: (" . $DB->errno . ") " . $DB->error;
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
     }
-    
+
+    $stmt->bind_param('s', $uuid);
     $stmt->execute();
-    
-    $res_data = mysqli_stmt_get_result($stmt);
-    
-    $all_data = array();
-    
-    while($row = mysqli_fetch_array($res_data)){
-        $data = json_decode($row['form']);
-        if (isset($data->uuid) && ($data->uuid == $uuid)) {
-            return $row['id'];
-        }
+    $stmt->store_result();
+    $stmt->bind_result($userID);
+    if ($stmt->fetch()) {
+       $stmt->free_result();
+       $stmt->close();
+       $DB->close();
+       return $userID;
     }
+    $stmt->free_result();
+    $stmt->close();
+    $DB->close();
     return -1;
  }
 
- function store ($data, $uuid = null) {
+ function getQuestionaireInfo ($hash) {
+    global $DB; connect();
+
+    $sql = "SELECT id,title FROM questionnaire WHERE view_hash = ?";
+
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    $stmt->bind_param('s', $hash);
+    $stmt->execute();
+    $stmt->store_result();
+    $stmt->bind_result($qid, $title);
+    if ($stmt->fetch()) {
+       $stmt->free_result();
+       $stmt->close();
+       $DB->close();
+       return ['qid' => $qid, 'title' => $title];
+    }
+    throw new Exception("User attempted to submit data to a non-existent questionnaire.");
+ }
+
+ function getQuestionaireInfoBySecret ($hash) {
     global $DB; connect();
 
-    $sql = "";
+    $sql = "SELECT id,title FROM questionnaire WHERE edit_hash = ?";
 
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    $stmt->bind_param('s', $hash);
+    $stmt->execute();
+    $stmt->store_result();
+    $stmt->bind_result($qid, $title);
+    if ($stmt->fetch()) {
+       $stmt->free_result();
+       $stmt->close();
+       $DB->close();
+       return ['qid' => $qid, 'title' => $title];
+    }
+    throw new Exception("User attempted to submit data to a non-existent questionnaire.");
+ }
+
+ function storeUserSubmission ($data, $qid, $uuid) {
     $id_p = get_id_by_uuid($uuid);
     if ($id_p >= 0) {
-        $sql =  "UPDATE records SET form = ?
-                WHERE id = " . $id_p;
-    } else {
-        $sql =  "INSERT INTO records (id, form)
-                VALUES (null, ?)";
+       updateUserRecord($id_p, $qid, $data);
+       return;
     }
-    
+    $sql =  "INSERT INTO answers (q_id, uuid, json)
+       VALUES (?, ?, ?)";
+
+
+    global $DB; connect();
+
     if (!($stmt = $DB->prepare($sql))) {
-        echo "Prepare failed: (" . $DB->errno . ") " . $DB->error;
+        die("Prepare failed: ($DB->errno) $DB->error");
     }
-    
-    if (!$stmt->bind_param("s", $data)) {
-        echo "Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error;
+
+    if (!$stmt->bind_param("iss", $qid, $uuid, $data)) {
+        die("Binding parameters failed: ($stmt->errno) $stmt->error");
     }
 
     if (!$stmt->execute()) {
-        echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error;
+        die("Execute failed: ($stmt->errno) $stmt->error");
     }
 
+    $stmt->close();
     $DB->close();
  }
 
- function get_forms () {
+ function updateUserRecord ($id, $qid, $data) {
     global $DB; connect();
+    $sql =  "UPDATE answers SET json = ?
+       WHERE id = ?";
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: ($DB->errno) $DB->error");
+    }
 
-    $sql = "SELECT form FROM records";
+    if (!$stmt->bind_param("si", $data, $id)) {
+        die("Binding parameters failed: ($stmt->errno) $stmt->error");
+    }
+
+    if (!$stmt->execute()) {
+        die("Execute failed: ($stmt->errno) $stmt->error");
+    }
 
+    $stmt->close();
+    $DB->close();
+ }
+
+ function storeNewForm ($form, $keys, $source) {
+    global $DB; connect();
+    $sql = "INSERT INTO questionnaire(email, title, description, thanks, view_hash, edit_hash, source) VALUES (?, ?, ?, ?, ?, ?, ?)";
     if (!($stmt = $DB->prepare($sql))) {
-        echo "Prepare failed: (" . $DB->errno . ") " . $DB->error;
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+    if (!$stmt->bind_param("sssssss", $form['email'], $form['title'], $form['description'], $form['thanks'], $keys[1], $keys[0], $source)) {
+        die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
     }
 
-    $stmt->execute();
+    if (!$stmt->execute()) {
+        die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
 
-    $res_data = mysqli_stmt_get_result($stmt);
+    $qid = $stmt->insert_id;
+    $stmt->close();
+    $DB->close();
+    $questions = $form['questions'];
+    foreach ($questions as $question) {
+       if ($question->type === 'PAGEBREAK') continue;
+       storeQuestionText($qid, $question);
+       if (in_array($question->type, ['M','S'])) {
+          storeQuestionMapping($qid, $question);
+       }
+    }
 
-    $all_forms = array();
+ }
 
-    while($row = mysqli_fetch_array($res_data)){
-        $data = json_decode($row['form']);
-        if(!in_array($data->form, $all_forms)){
-            $all_forms[] = $data->form;
-        }
+ function storeQuestionText($qid, $question) {
+    global $DB; connect();
+    $sql = "INSERT INTO question_text(q_id, question, text) VALUES (?, ?, ?)";
+    $number = substr($question->number, 1);
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: ($DB->errno) $DB->error");
     }
+    if (!$stmt->bind_param("iis", $qid, $number, $question->text)) {
+       die("Binding parameters failed: ($stmt->errno) $stmt->error");
+    }
+
+    if (!$stmt->execute()) {
+       die("Execute failed: ($stmt->errno) $stmt->error");
+    }
+    $stmt->close();
+    $DB->close();
+ }
 
-    sort($all_forms);
+ function storeQuestionMapping ($qid, $question) {
+    global $DB; connect();
+    $sql = "INSERT INTO question_mapping(q_id, question, value, text) VALUES (?, ?, ?, ?)";
+    $number = substr($question->number, 1);
+    if (!($stmt = $DB->prepare($sql))) {
+      die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+    foreach ($question->choices as $choice) {
+       if (!$stmt->bind_param("iiss", $qid, $number, $choice->value, $choice->text)) {
+         die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
+       }
 
-    return $all_forms;
+       if (!$stmt->execute()) {
+          die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+       }
+    }
+    $stmt->close();
+    $DB->close();
  }
 
- function get_total_rows ($form_id) {
+ function getFormThanks ($hash) {
     global $DB; connect();
-    
-    $sql = "SELECT form FROM records";
+
+    $sql = "SELECT title, thanks FROM questionnaire WHERE view_hash = ?";
 
     if (!($stmt = $DB->prepare($sql))) {
-        echo "Prepare failed: (" . $DB->errno . ") " . $DB->error;
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
     }
-    
-    $stmt->execute();
 
-    $res_data = mysqli_stmt_get_result($stmt);
+    if (!$stmt->bind_param("s", $hash)) {
+       die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
 
-    $count = 0;
+    if (!$stmt->execute()) {
+       die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
 
-    while($row = mysqli_fetch_array($res_data)){
-        $data = json_decode($row['form']);
-        if ($data->form == $form_id) {
-            $count ++;
-        }
+    $stmt->store_result();
+    $stmt->bind_result($title, $thanks);
+    if ($stmt->fetch()) {
+       $result = ['title'=>$title, 'thanks'=>$thanks];
+       $stmt->free_result();
+       $stmt->close();
+       $DB->close();
+       return $result;
     }
+    throw new Exception("User attempted to access data of a non-existent questionnaire.");
+ }
+
+ function checkFormAccess ($email, $secret) {
+    global $DB; connect();
+
+    $sql = "SELECT id FROM questionnaire WHERE edit_hash = ? AND email = ?";
+
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    if (!$stmt->bind_param("ss", $secret, $email)) {
+       die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    if (!$stmt->execute()) {
+       die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    $stmt->store_result();
+    $stmt->bind_result($qid);
+    $found = false;
+    if ($stmt->fetch()) {
+       $found = true;
+    }
+    $stmt->free_result();
+    $stmt->close();
+    $DB->close();
+    return $found;
+ }
+
+ function get_user_form ($secret) {
+    global $DB; connect();
+
+    $sql = "SELECT id FROM questionnaire WHERE edit_hash = ?";
+
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    if (!$stmt->bind_param("s", $secret)) {
+       die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    if (!$stmt->execute()) {
+       die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    $stmt->store_result();
+    $stmt->bind_result($qid);
+    if ($stmt->fetch()) {
+       $stmt->free_result();
+       $stmt->close();
+       $DB->close();
+       return $qid;
+    }
+    die('User attempted to acess data from an unknown questionnaire!');
+ }
+
+ function get_total_rows ($form_id) {
+    global $DB; connect();
+
+    $sql = "SELECT count(id) FROM answers WHERE q_id = ?";
+
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    if (!$stmt->bind_param("i", $form_id)) {
+       die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    if (!$stmt->execute()) {
+       die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    $stmt->store_result();
+    $stmt->bind_result($count);
+    $stmt->fetch();
+    $stmt->free_result();
+    $stmt->close();
+    $DB->close();
 
     return $count;
  }
 
- function get_records_pagination($form_id, $offset, $no_of_records_per_page) {
+ function get_records_pagination($form_id, $page, $no_of_records_per_page) {
     global $DB; connect();
 
-    $sql = "SELECT * FROM records";
+    $sql = "SELECT id, json FROM answers WHERE q_id = ? LIMIT ?, ?";
 
     if (!($stmt = $DB->prepare($sql))) {
-        echo "Prepare failed: (" . $DB->errno . ") " . $DB->error;
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
     }
-    
-    $stmt->execute();
-    
-    $res_data = mysqli_stmt_get_result($stmt);
-    
-    $all_data = array();
 
-    $count = 0;
-    $included = 0;
-    
-    while($row = mysqli_fetch_array($res_data)){
-        $data = json_decode($row['form']);
-        if ($data->form == $form_id) {
-            $data->id = $row['id'];
-            $count++;
-            if ($count-1 >= $offset) {
-                $included ++;
-                if ($included-1 == $no_of_records_per_page) break;
-                $all_data[] = $data;
-            }
-            
-        }
+    if (!$stmt->bind_param("iii", $form_id, $page,$no_of_records_per_page)) {
+       die("Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error);
     }
+
+    if (!$stmt->execute()) {
+       die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
+    }
+
+    $stmt->store_result();
+    $stmt->bind_result($id, $json);
+    $all_data = array();
+    while ($stmt->fetch()) {
+       $data = json_decode($json);
+       $data->id = $id;
+       $all_data[] = $data;
+    }
+    $stmt->free_result();
+    $stmt->close();
+    $DB->close();
+
     return $all_data;
  }
 
- ?>
+ function loadQuestionsMapping ($qid) {
+    global $DB; connect();
+
+    $sql = "SELECT question, value, text FROM question_mapping WHERE q_id = ?";
+
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    $stmt->bind_param('i', $qid);
+    $stmt->execute();
+    $stmt->store_result();
+    $result = array();
+    $stmt->bind_result($number, $value, $text);
+    while ($stmt->fetch()){
+       $question = new stdClass;
+       $question->number = $number;
+       $question->value = $value;
+       $question->text = $text;
+       $result[] = $question;
+    }
+    $stmt->free_result();
+    $stmt->close();
+    $DB->close();
+    return $result;
+ }
+
+ function loadQuestionsText ($qid) {
+    global $DB; connect();
+
+    $sql = "SELECT question, text FROM question_text WHERE q_id = ?";
+
+    if (!($stmt = $DB->prepare($sql))) {
+        die("Prepare failed: (" . $DB->errno . ") " . $DB->error);
+    }
+
+    $stmt->bind_param('i', $qid);
+    $stmt->execute();
+    $stmt->store_result();
+    $result = array();
+    $stmt->bind_result($number, $text);
+    while ($stmt->fetch()){
+       $question = new stdClass;
+       $question->number = $number;
+       $question->text = $text;
+       $result[] = $question;
+    }
+    $stmt->free_result();
+    $stmt->close();
+    $DB->close();
+    return $result;
+ }
+
+ ?>

+ 71 - 0
controller/generateform.php

@@ -0,0 +1,71 @@
+<?php
+require_once('../templates/templates.php');
+
+$cacheQuestions = array();
+$cacheChoices = array();
+
+function loadQuestionTemplate ($type) {
+   global $cacheQuestions;
+   if (!isset($cacheQuestions[$type])) {
+      $cacheQuestions[$type] = getTemplate(strtolower("question_$type.html"));
+   }
+   return $cacheQuestions[$type];
+}
+
+function loadChoicesTemplate ($type) {
+   global $cacheChoices;
+   if (!isset($cacheChoices[$type])) {
+      $cacheChoices[$type] = getTemplate(strtolower("choice_$type.html"));
+   }
+   return $cacheChoices[$type];
+}
+
+function generateFormHTML ($form) {
+   $pageCount = 1;
+   $formText = "";
+   $content = "";
+   $questions = $form['questions'];
+   $pageTemplate = getTemplate('form_page.html');
+   foreach ($questions as $question) {
+      if ($question->type === 'PAGEBREAK') {
+         if (strlen($content) > 0) {
+            $context = array('page_number'=>$pageCount,'questions'=>$content);
+            $formText = $formText . parseTemplate($pageTemplate, $context);
+            $pageCount++;
+            $content = "";
+         }
+         continue;
+      }
+      $questionTemplate = loadQuestionTemplate($question->type);
+      $required = "";
+      if (property_exists($question,'required')) {
+         $required = "required";
+      }
+      $context = array('id'=>$question->number,'text'=>$question->text, 'required'=>$required);
+      if (in_array($question->type, array('S','M'))) {
+         $context['choices'] = generateChoices($question);
+      }
+      $content = $content . parseTemplate($questionTemplate, $context);
+   }
+
+   if (strlen($content) > 0) {
+      $context = array('page_number'=>$pageCount,'questions'=>$content);
+      $formText = $formText . parseTemplate($pageTemplate, $context);
+   }
+
+   $formTemplate = getTemplate('form.html');
+   $context = ['title'=>$form['title'], 'description'=>$form['description'], 'pages'=>$formText, 'form_id'=>$form['id']];
+   return parseTemplate($formTemplate, $context);
+}
+
+function generateChoices ($question) {
+   $choices = $question->choices;
+   $content = "";
+   $template = loadChoicesTemplate($question->type);
+   for ($i = 0; $i < count($choices); $i++) {
+      $choice = $choices[$i];
+      $context = ['id' => $question->number, 'count' => $i, 'value'=>$choice->value, 'text'=>$choice->text];
+      $content = $content . parseTemplate($template, $context);
+   }
+   return $content;
+}

+ 0 - 0
controller/index.php


+ 76 - 0
controller/validator.php

@@ -0,0 +1,76 @@
+<?php
+
+class  Validator {
+	static $errors = true;
+
+	static function check($arr, $on = false) {
+		if ($on === false) {
+			$on = $_REQUEST;
+		}
+		foreach ($arr as $value) {
+			if (empty($on[$value])) {
+				self::throwError('Data is missing', 900);
+			}
+		}
+	}
+
+	static function int($val) {
+		$val = filter_var($val, FILTER_VALIDATE_INT);
+		if ($val === false) {
+			self::throwError('Invalid Integer', 901);
+		}
+		return $val;
+	}
+
+	static function str($val, $empty=false) {
+		if (!is_string($val)) {
+			self::throwError('Invalid String', 902);
+		}
+		$val = trim(htmlspecialchars($val));
+		if(strlen($val) === 0 && !$empty) self::throwError('Empty String', 907);
+		return $val;
+	}
+
+	static function bool($val) {
+		$val = filter_var($val, FILTER_VALIDATE_BOOLEAN);
+		return $val;
+	}
+
+	static function email($val) {
+		$val = filter_var($val, FILTER_VALIDATE_EMAIL);
+		if ($val === false) {
+			self::throwError('Invalid Email', 903);
+		}
+		return $val;
+	}
+
+	static function url($val) {
+		$val = filter_var($val, FILTER_VALIDATE_URL);
+		if ($val === false) {
+			self::throwError('Invalid URL', 904);
+		}
+		return $val;
+	}
+
+	static function file($val, $ext = false) {
+		if (($val['error'] !== UPLOAD_ERR_OK) || !is_uploaded_file($val['tmp_name'])) {
+			self::throwError('File upload error',905);
+		}
+		if ($ext) {
+			$fileName = $val['name'];
+			// $fileSize = $val['size'];
+			$fileNameCmps = explode(".", $fileName);
+			$fileExtension = strtolower(end($fileNameCmps));
+			if (strcmp($ext, $fileExtension) !== 0) {
+				self::throwError('Invalid file extension',906);
+			}
+		}
+		return $val;
+	}
+
+	static function throwError($error = 'Error In Processing', $errorCode = 0) {
+		if (self::$errors === true) {
+			throw new Exception($error, $errorCode);
+		}
+	}
+}

+ 9 - 11
explorer/export.php

@@ -4,16 +4,14 @@
         header('Location: login.php');
         exit;
     }
-?>  
-
-<?php
-
-    $form_id = $_GET['formulario'];
+    require_once('../controller/forms.php');
+    $secret = $_SESSION['hash_user'];
+    $info = getQuestionaireInfoBySecret($secret);
+    $form_id = $info['qid'];
 
     header('Content-Type: text/csv; charset=utf-8');
-    header('Content-Disposition: attachment; filename='.$form_id.'.csv');
+    header('Content-Disposition: attachment; filename='.$info['title'].'.csv');
 
-    require_once('../controller/forms.php');
 
     $total_rows = get_total_rows($form_id);
 
@@ -21,7 +19,7 @@
 
     $output = fopen('php://output', 'w');
 
-    
+
     $all_fields = array('id');
 
     foreach($res_data as $form) {
@@ -42,7 +40,7 @@
         foreach($all_fields as $field) {
 
             if ($field == 'id') continue;
-            
+
             if ($field === 'timestamp') {
                 $prepared[] = date("H:i:s d/m/Y", $form->$field);
                 continue;
@@ -50,9 +48,9 @@
 
             if (isset($form->$field)) $prepared[] = $form->$field;
             else $prepared[] = '';
-            
+
         }
         fputcsv($output, $prepared);
     }
 
-?>
+?>

+ 12 - 31
explorer/index.php

@@ -1,11 +1,10 @@
-
 <?php
     session_start();
     if (!isset($_SESSION['hash_user'])) {
         header('Location: login.php');
         exit;
     }
-?>  
+?>
 
 <html>
 <head>
@@ -29,7 +28,7 @@
         table tr:nth-child(even){background-color: #f2f2f2;}
 
         table tr:hover {background-color: #ddd;}
-        
+
         table tr:hover td {border: 1px solid white;}
 
         table th {
@@ -85,31 +84,14 @@
 </head>
 <body>
 
-    <?php 
-        require_once('../controller/forms.php');
-        $all_forms = get_forms();
+    <?php
+    require_once('../controller/forms.php');
+    $secret = $_SESSION['hash_user'];
+    $form_id = get_user_form($secret);
     ?>
 
-    <form>
-        Selecione o identificador do formulário: 
-            <select name="formulario" onchange="this.form.submit()">
-                <option></option>
-                <?php
-                    foreach($all_forms as $form) {
-                        if ((isset($_GET['formulario'])
-                            && $_GET['formulario'] == $form)) {
-                            print "<option selected>$form</option>";
-                        } else {
-                            print "<option>$form</option>";
-                        }
-                    }
-                ?>
-            </select>
-    </form>
-        
     <table>
     <?php
-        if ((!isset($_GET['formulario']))) exit;
 
         if (isset($_GET['pageno'])) {
             $pageno = $_GET['pageno'];
@@ -119,13 +101,12 @@
 
         $no_of_records_per_page = 10;
         $offset = ($pageno-1) * $no_of_records_per_page;
-        $form_id = $_GET['formulario'];
 
         $total_rows = get_total_rows($form_id);
         $total_pages = ceil($total_rows / $no_of_records_per_page);
 
         $res_data = get_records_pagination($form_id, $offset, $no_of_records_per_page);
-        
+
         $all_fields = array();
 
         foreach($res_data as $form) {
@@ -156,28 +137,28 @@
                 }
                 if (isset($form->$field)) print $form->$field.'</td>';
                 else print '</td>';
-            
+
             }
             print '</tr>';
         }
-        
+
     ?>
     </table>
     <div class="total">
         Total de registros: <b><?= $total_rows ?></b>
     </div>
     <div class="export">
-        Exportar como: <a href="export.php?formulario=<?= $form_id ?>&format=csv">CSV</a>
+        Exportar como: <a href="export.php?format=csv">CSV</a>
     </div>
     <center>
         <div class="pagination">
             <a class="<?php if($pageno <= 1){ echo 'disabled'; } ?>" href="?formulario=<?= $form_id ?>&pageno=1">Primeira</a>
 
             <a class="<?php if($pageno <= 1){ echo 'disabled'; } ?>" href="<?php if($pageno <= 1){ echo '#'; } else { echo "?formulario=$form_id&pageno=".($pageno - 1); } ?>">Anterior</a>
-            
+
             <a class="<?php if($pageno >= $total_pages){ echo 'disabled'; } ?>" href="<?php if($pageno >= $total_pages){ echo '#'; } else { echo "?formulario=$form_id&pageno=".($pageno + 1); } ?>">Próxima</a>
             <a class="<?php if($pageno >= $total_pages){ echo 'disabled'; } ?>" href="?formulario=<?= $form_id ?>&pageno=<?php echo $total_pages; ?>">Última</a>
         </div>
     </center>
 </body>
-</html>
+</html>

+ 30 - 20
explorer/login.php

@@ -1,22 +1,32 @@
+<?php
+require_once('../templates/templates.php');
+$template = getTemplate('explorer_login.html');
+$context = ['errors' => '','post_url' => htmlspecialchars($_SERVER["PHP_SELF"])];
 
-    <form method="post">
-        Informe a senha: 
-        <input type="password" name="senha">
-        <input type="submit" value="Enviar">
-        
-        <?php
-            if (isset($_POST['senha'])) {
-                require_once ('../config/linequest.php');
+function errorMessage () {
+    global $context;
+    $errorAlert = getTemplate('error_alert.html');
+    $context['errors'] = parseTemplate($errorAlert, ['message'=>'Email ou senha incorretos. Certifique-se que os dados informados estão corretos']);
+}
 
-                global $CFG;
-                if ($CFG->viewpass == $_POST['senha']) {
-                    session_start();
-                    $_SESSION['hash_user'] = md5($_POST['senha'].time());
-                    header('Location: index.php');
-                } else {
-                    echo "<b style='color: red'>Senha incorreta!</b>";
-                }
-            }
-            
-        ?>  
-    </form>
+if (!empty($_POST)) {
+    require_once('../controller/forms.php');
+    require_once('../controller/validator.php');
+
+    Validator::check(['email','senha'], $_POST);
+    $email = Validator::email($_POST['email']);
+    $password = Validator::str($_POST['senha']);
+    try {
+        $valid = checkFormAccess($email, $password);
+        if ($valid) {
+            session_start();
+            $_SESSION['hash_user'] = $password;
+            header('Location: index.php');
+            exit;
+        }
+        errorMessage();
+    } catch (Exception $e) {
+        errorMessage();
+    }
+}
+echo parseTemplate($template, $context);

+ 0 - 0
forms/index.php


+ 0 - 0
forms/teste.html


+ 0 - 0
index.html


+ 4 - 0
templates/choice_m.html

@@ -0,0 +1,4 @@
+<label class="radio-inline" for="{id}-{count}">
+  <input type="radio" name="{id}" id="{id}-{count}" value="{value}" >
+  {text}
+</label>

+ 1 - 0
templates/choice_s.html

@@ -0,0 +1 @@
+<option value="{value}">{text}</option>

+ 112 - 0
templates/create_form.html

@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html lang="pt-br">
+  <head>
+    <!-- Meta tags Obrigatórias -->
+    <meta charset="utf-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1, shrink-to-fit=no"
+    />
+
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="/assets/css/bootstrap.min.css" />
+
+    <title>LInE Quest</title>
+
+    <style>
+      body {
+        background-color: #e9ecef;
+      }
+      .page-header {
+        margin-top: 1em;
+        border-top: 1px solid rgb(155, 155, 155);
+        padding-top: 0.5em;
+      }
+      .badge {
+        font-size: 100%;
+      }
+      .form-div {
+        margin: 2rem;
+        width: 60vw;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="jumbotron" style="padding-bottom: 0;">
+      <p style="float: right; color: #333; font-weight: 300;">
+        Free Education, Private Data (FEPD)
+      </p>
+      <img src="/assets/img/logo.png" width="250px" />
+      <p style="font-size: 21px; color: #333; font-weight: 300;">
+        Plataforma para questionários
+      </p>
+      <hr/>
+    </div>
+    <div class="form-div">
+      <h3 class="">Criar questionário</h3>
+      <form enctype="multipart/form-data" action="{post_url}" method="POST">
+        <div class="form-group">
+          <label for="email">Email</label>
+          <input
+            type="email"
+            class="form-control"
+            id="email"
+            name="email"
+          />
+          <small id="emailHelp" class="form-text text-muted"
+            >Email que receberá as informações de acesso ao questionário.</small
+          >
+        </div>
+        <div class="form-group">
+          <label for="title">Título</label>
+          <input
+            id="title"
+            name="title"
+            type="text"
+            class="form-control"
+          />
+          <small id="titleHelp" class="form-text text-muted"
+            >Título do seu questionário</small
+          >
+        </div>
+        <div class="form-group">
+          <label for="description">Apresentação</label>
+          <textarea
+            id="description"
+            name="description"
+            cols="15"
+            rows="5"
+            class="form-control"
+          ></textarea>
+          <small id="introHelp" class="form-text text-muted"
+            >Essa apresentação será aprensetado no início do seu
+            questionário.</small
+          >
+        </div>
+        <div class="form-group">
+          <label for="thanks">Agradecimento</label>
+          <textarea
+            id="thanks"
+            name="thanks"
+            cols="15"
+            rows="5"
+            class="form-control"
+          ></textarea>
+          <small id="thanksHelp" class="form-text text-muted"
+            >Essa mensagem será aprensetada no final do seu questionário.</small
+          >
+        </div>
+        <div class="form-group">
+          <label for="formSource">Arquivo do questionário</label>
+          <input
+            type="file"
+            class="form-control-file"
+            id="formSource"
+            name="formSource"
+          />
+        </div>
+        <button class="btn btn-primary" type="submit">Enviar</button>
+      </form>
+    </div>
+  </body>
+</html>

+ 3 - 0
templates/error_alert.html

@@ -0,0 +1,3 @@
+<div class="alert alert-danger" role="alert">
+  {message}
+</div>

+ 77 - 0
templates/explorer_login.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="pt-br">
+  <head>
+    <!-- Meta tags Obrigatórias -->
+    <meta charset="utf-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1, shrink-to-fit=no"
+    />
+
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="/assets/css/bootstrap.min.css" />
+
+    <title>LInE Quest</title>
+
+    <style>
+      body {
+        background-color: #e9ecef;
+      }
+      .page-header {
+        margin-top: 1em;
+        border-top: 1px solid rgb(155, 155, 155);
+        padding-top: 0.5em;
+      }
+      .badge {
+        font-size: 100%;
+      }
+      .form-div {
+        margin: 2rem;
+        width: 60vw;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="jumbotron" style="padding-bottom: 0;">
+      <p style="float: right; color: #333; font-weight: 300;">
+        Free Education, Private Data (FEPD)
+      </p>
+      <img src="/assets/img/logo.png" width="250px" />
+      <p style="font-size: 21px; color: #333; font-weight: 300;">
+        Plataforma para questionários
+      </p>
+      <hr/>
+    </div>
+    <div class="form-div">
+      <h3 class="">Acessar os dados do seu questionário</h3>
+      <form action="{post_url}" method="POST">
+        <div class="form-group">
+          <label for="email">Email</label>
+          <input
+            type="email"
+            class="form-control"
+            id="email"
+            name="email"
+          />
+          <small id="emailHelp" class="form-text text-muted"
+            >Email utilizado durante a criação do questionário.</small
+          >
+        </div>
+        <div class="form-group">
+          <label for="senha">Senha</label>
+          <input
+            id="senha"
+            name="senha"
+            type="password"
+            class="form-control"
+          />
+          <small id="senhaHelp" class="form-text text-muted"
+            >Código de acesso enviado ao seu email após a criação do questionário</small
+          >
+        </div>
+        <button class="btn btn-primary" type="submit">Enviar</button>
+      </form>
+    </div>
+    {errors}
+  </body>
+</html>

+ 289 - 0
templates/form.html

@@ -0,0 +1,289 @@
+<!DOCTYPE HTML>
+<html lang="pt-BR">
+  <head>
+    <title>{title}</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta name="author" content="LInE" />
+    <link rel="stylesheet" href="/assets/foundation.min.css">
+    <style>
+      body {
+        margin-left: 2rem;
+        margin-right: 2rem;
+        margin-top:1.5rem;
+      }
+      .rotulo {
+        font-weight: bold;
+        background-color: #e4f1fa;
+        margin: 2px;
+      }
+      .required::before {
+        content: '* ';
+        color: red;
+      }
+      .condicional {
+        visibility: hidden;
+      }
+      .page {
+        display: none;
+      }
+      #btn_submit {
+        display: none;
+      }
+      legend, #current_page, #max_page {
+        font-weight: bold;
+      }
+      #current_page {
+        font-family: monospace;
+        font-size: 1rem;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="callout" >
+    <p class="paragrafo lead text-justify">
+      <span style="font-weight: bold;">{title}</span></br>{description}</br>
+<span class="required" style="color:red">Obrigatório</span>
+    </p>
+    </div>
+    <div class="formulario">
+      <form class="form" method="POST" action="../app/post.php">
+        <input type="hidden" name="form" value="{form_id}" />
+        <input type="hidden" name="uuid" value="" />
+        <input type="hidden" name="page" value="1" />
+        <input type="hidden" name="status" value="" />
+        <div class="grid-container">
+          <div class="grid-x grid-padding-x">
+            <div class="large-5">
+              Página <span id="current_page"></span> de <span id="max_page"></span>
+            </div>
+
+            {pages}
+
+            <fieldset class="fieldset float-center">
+              <button id="btn_back" class="button primary">Anterior</button>
+              <button id="btn_next" class="button primary">Próximo</button>
+              <button id="btn_submit" class="button success" type="submit">Finalizar</button>
+            </fieldset>
+          </div>
+        </div>
+      </form>
+    </div>
+    <script src="/assets/populatejs.min.js"></script>
+    <script>
+      (function () {
+        const form = document.forms[0];
+        const MAX_PAGE = document.querySelectorAll("fieldset.page").length;
+        const KEY_NAME = form['form'].value;
+        const STATUS = {
+          PARTIAL: "PARTIAL",
+          COMPLETED: "COMPLETED"
+        }
+        const btnBack = document.querySelector("#btn_back");
+        const header = document.querySelector('.callout > p.paragrafo');
+        const btnNext = document.querySelector("#btn_next");
+        const btnSubmit = document.querySelector("#btn_submit");
+        const currentPageSpan = document.querySelector("#current_page");
+        const maxPageSpan = document.querySelector("#max_page");
+        let page = 1;
+        let isValid = false;
+
+        const getLocalStorage = function () {
+          const test = 'test';
+          try {
+            localStorage.setItem(test, test);
+            localStorage.removeItem(test);
+            return localStorage;
+          } catch(e) {
+            return {
+              setItem: () => null,
+              removeItem: () => null,
+              getItem: () => null
+            };
+          }
+        }
+
+
+        const storePartialForm = function (formData) {
+          const obj = {};
+          formData.forEach((value, key) => {
+            obj[key] = value;
+          });
+          getLocalStorage().setItem(KEY_NAME, JSON.stringify(obj));
+        }
+
+        const loadFromStorage = function () {
+          const storage = getLocalStorage();
+          const oldForm = storage.getItem(KEY_NAME);
+          if (oldForm != null) {
+            const formDict = JSON.parse(oldForm);
+            populatejs(formDict);
+            page = +formDict['page'];
+          }
+        }
+
+        const sendPartialForm = function (formData) {
+          const xhr = new XMLHttpRequest();
+          xhr.open("POST", form.action);
+          formData.set("status", STATUS.PARTIAL);
+          xhr.send(formData);
+        }
+
+        const updateCurrentPageSpan = function () {
+          currentPageSpan.innerHTML = page;
+          form['page'].value = page;
+        }
+
+        const getPage = function (pageID) {
+          const page = document.querySelector(`.page[data-page='${pageID}']`);
+          return page;
+        }
+        const showPage = function (pageID) {
+          const page = getPage(pageID);
+          page.style.display = 'block';
+          header.scrollIntoView({behavior: 'smooth', inline:'start'})
+        }
+
+        const hidePage = function (pageID) {
+          const page = getPage(pageID);
+          page.style.display = 'none';
+        }
+
+        const nextPage = function () {
+          let next = page + 1;
+          next = next > MAX_PAGE ? MAX_PAGE : next;
+          hidePage(page);
+          showPage(next);
+          page = next;
+          updateCurrentPageSpan()
+        }
+
+        const prevPage = function () {
+          let last = page - 1;
+          last = last < 1 ? 1 : last;
+          hidePage(page);
+          showPage(last);
+          page = last;
+          updateCurrentPageSpan()
+        }
+
+        // Manage back button
+        btnBack.disabled = true;
+        btnBack.addEventListener('click', function (event) {
+          event.preventDefault();
+          if (page === 1) {
+            return;
+          }
+          prevPage();
+          validateConditionals();
+          validateFormPage();
+          btnSubmit.style.display = 'none';
+          btnNext.style.display = 'inline-block';
+          if (page === 1)
+            btnBack.disabled = true;
+        })
+
+        // Manage next button
+        btnNext.disabled = true;
+        btnNext.addEventListener('click', function (event) {
+          event.preventDefault();
+          if (!isValid) {
+            return;
+          } else if(page === MAX_PAGE) {
+            btnNext.style.display = 'none';
+            return;
+          }
+
+          nextPage();
+          validateConditionals();
+          validateFormPage();
+          const formData = new FormData(form);
+          sendPartialForm(formData);
+          storePartialForm(formData);
+          btnBack.disabled = false;
+          if(page === MAX_PAGE) {
+            btnSubmit.style.display = 'inline-block';
+            btnNext.style.display = 'none';
+          }
+        });
+
+        // validate form
+        const validateFormPage = function () {
+          const pageElement = getPage(page);
+          const requiredList = Array.prototype.slice.call(pageElement.querySelectorAll('label.required'))
+            .filter(e => {
+              const style = window.getComputedStyle(e);
+              return e.offsetParent != null && style.visibility != 'hidden';
+            })
+            .map(e => e.attributes.for.value);
+          //requiredList.forEach(x => console.log(form[x]));
+          isValid = !requiredList.some( field => {
+            const value = form[field].value;
+            return value == "" || value == null;
+          });
+          btnNext.disabled = !isValid;
+          btnSubmit.disabled = !isValid;
+          btnBack.disabled = page === 1;
+
+        }
+
+        // validate conditional fields
+        const validateConditionals = function () {
+          const pageElement = getPage(page);
+          const conditionals = Array.prototype.slice.call(pageElement.querySelectorAll('.condicional'))
+            .filter(e => {
+              const style = window.getComputedStyle(e);
+              return e.offsetParent != null;
+            });
+          //console.log(conditionals);
+          conditionals.forEach(cond => {
+            const field = cond.dataset.cond;
+            const valuesList = cond.dataset.values.split("|");
+            const value = form[field].value;
+            const shouldEnable = valuesList.some( v => v == value);
+            cond.style.visibility = shouldEnable ? 'visible' : 'hidden';
+          });
+
+        }
+
+        // Hook document change event to validate the form and conditions
+        document.addEventListener('change', () => {
+          validateConditionals();
+          validateFormPage();
+        });
+
+        // Intercept form submit
+        form.addEventListener('submit' ,() => {
+          getLocalStorage().removeItem(KEY_NAME);
+          form["status"].value = STATUS.COMPLETED;
+        });
+
+        // set uuid
+        const createUUID = function () {
+          return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+            const r = Math.random() * 16 | 0;
+            const v = c === 'x' ? r : (r & 0x3 | 0x8);
+            return v.toString(16);
+          });
+        }
+        form['uuid'].value = createUUID();
+
+        // set max page span
+        maxPageSpan.innerHTML = MAX_PAGE;
+
+
+        //load from stogage if available
+        loadFromStorage();
+        if(page === MAX_PAGE) {
+          btnSubmit.style.display = 'inline-block';
+          btnNext.style.display = 'none';
+        }
+
+        // Always show page 1
+        showPage(page);
+        updateCurrentPageSpan();
+        validateConditionals();
+        validateFormPage();
+      })()
+    </script>
+  </body>
+</html>

+ 3 - 0
templates/form_page.html

@@ -0,0 +1,3 @@
+<fieldset class="cell fieldset page" data-page="{page_number}">
+  {questions}
+</fieldset>

+ 6 - 0
templates/question_d.html

@@ -0,0 +1,6 @@
+<div>
+  <label class="rotulo {required}" for="{id}">{text}</label>
+  <div class="col-md-4">
+    <textarea class="form-control" id="{id}" name="{id}"></textarea>
+  </div>
+</div>

+ 6 - 0
templates/question_i.html

@@ -0,0 +1,6 @@
+<div>
+  <label class="rotulo {required}" for="{id}">{text}</label>
+  <div>
+    <input id="{id}" name="{id}" type="text">
+  </div>
+</div>

+ 6 - 0
templates/question_m.html

@@ -0,0 +1,6 @@
+<div>
+  <label class="rotulo {required}" for="{id}">{text}</label>
+  <div class="col-md-4">
+    {choices}   
+  </div>
+</div>

+ 9 - 0
templates/question_s.html

@@ -0,0 +1,9 @@
+<div>
+  <label class="rotulo {required}" for="{id}">{text}</label>
+  <div>
+    <select id="{id}" name="{id}">
+      <option value="">Selecione uma opção</option>
+      {choices}
+    </select>
+  </div>
+</div>

+ 20 - 0
templates/templates.php

@@ -0,0 +1,20 @@
+<?php
+
+function getTemplate ($template, $folder=__DIR__."/") {
+  $file = $folder.$template;
+  $content = "";
+  if (is_file($file)) {
+    $content = file_get_contents($file);
+  } else {
+    error_log('Not file....'.$file);
+  }
+  return $content;
+}
+
+function parseTemplate ($template, $context) {
+  $final = $template;
+  foreach ($context as $key => $value){
+    $final = str_replace('{'.$key.'}',$value, $final);
+  }
+  return $final;
+}

+ 5 - 4
app/thanks.html

@@ -1,10 +1,10 @@
 <!DOCTYPE HTML>
 <html lang="pt-BR">
   <head>
-    <title>Questionário: Pesquisa sobre mudanças na Educação provocadas pela pandemia da covid-19</title>
+    <title>{title}</title>
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <meta name="author" content="LInE" />
-    <link rel="stylesheet" href="../forms/foundation.min.css">
+    <link rel="stylesheet" href="/assets/foundation.min.css">
     <style>
       .rotulo {
         font-weight: bold;
@@ -32,8 +32,9 @@
   <body>
     <div class="callout">
         <p class="paragrafo lead text-justify">
-        Suas respostas foram gravadas com sucesso! O Laboratório de Informática na Educação agradece a sua participação!
+        Suas respostas foram gravadas com sucesso!<br/>
+        {thanks}
         </p>
     </div>
    </body>
-</html>
+</html>