diff --git a/BrowseImages.php b/BrowseImages.php
index f6dd580..95c6ba1 100644
--- a/BrowseImages.php
+++ b/BrowseImages.php
@@ -1,7 +1,7 @@
-browseImages();
+browseImages();
diff --git a/CreateTemplate.php b/CreateTemplate.php
index fe04a71..5b57aae 100644
--- a/CreateTemplate.php
+++ b/CreateTemplate.php
@@ -1,17 +1,17 @@
-generateCreateEditTemplatePage();
-
-/**
- * Include REDCap footer.
- */
+generateCreateEditTemplatePage();
+
+/**
+ * Include REDCap footer.
+ */
require_once APP_PATH_DOCROOT . "ProjectGeneral/footer.php";
\ No newline at end of file
diff --git a/CustomTemplateEngine.php b/CustomTemplateEngine.php
index d98bbf3..fe6916a 100644
--- a/CustomTemplateEngine.php
+++ b/CustomTemplateEngine.php
@@ -1086,7 +1086,7 @@ public function saveTemplate()
*
* @since 3.1
*/
- public function createPDF($dompdf_obj, $header, $footer, $main)
+ public function createPDF($dompdf_obj, $header, $footer, $main, $fileOrTemplateName="")
{
$contents = $this->formatPDFContents($header, $footer, $main);
@@ -1098,7 +1098,8 @@ public function createPDF($dompdf_obj, $header, $footer, $main)
$dompdf_obj->set_option('isRemoteEnabled', TRUE);
// Setup the paper size and orientation
- $dompdf_obj->setPaper("letter", "portrait");
+ list($paperSize, $paperOrientation) = $this->getPaperSettings($fileOrTemplateName);
+ $dompdf_obj->setPaper($paperSize, $paperOrientation);
// Render the HTML as PDF
$dompdf_obj->render();
@@ -1130,7 +1131,7 @@ public function downloadTemplate()
if (isset($main) && !empty($main))
{
$dompdf = new Dompdf();
- $pdf_content = $this->createPDF($dompdf, $header, $footer, $main);
+ $pdf_content = $this->createPDF($dompdf, $header, $footer, $main, $filename);
if (!$this->getProjectSetting("save-report-to-repo"))
{
@@ -2847,4 +2848,30 @@ public function redcap_module_link_check_display($project_id, $link)
return $link;
}
}
-}
+
+ /**
+ * getPaperSettings($name)
+ * Look for module project setting with name corresponding to the tempalte or file name provided
+ * @param String $name Name of template or pdf document file name
+ * @return Array Array with two elements: 1. paper size e.g. "Letter", "A4"; 2: paper orientation "Portrait" or "Landscape""
+ */
+ protected function getPaperSettings($fileOrTemplateName)
+ {
+ $paperSize = "letter";
+ $paperOrientation = "portrait";
+
+ $templateSettings = $this->getSubSettings('template-options');
+
+ foreach ($templateSettings as $settings) {
+ // look for a template with name occurring within the file name of what's being generated
+ // (pretty horrid - will catch "MyTemplate" before "MyTemplate_New" - need better way of recording template names perhaps recording to project settings on create/save/delete and auto-generate template name for files system storage?)
+ if (strpos($fileOrTemplateName, $settings['template-name']) !== false) {
+ $paperSize = (empty($settings['option-paper-size'])) ? $paperSize : $settings['option-paper-size'];
+ $paperOrientation = ($settings['option-paper-orientation']) ? "landscape" : $paperOrientation;
+ break;
+ }
+ }
+
+ return array($paperSize, $paperOrientation);
+ }
+}
\ No newline at end of file
diff --git a/DeleteTemplate.php b/DeleteTemplate.php
index e981ab3..8968d7f 100644
--- a/DeleteTemplate.php
+++ b/DeleteTemplate.php
@@ -1,20 +1,20 @@
-deleteTemplate();
-
-/**
- * If TRUE, then template was deleted successfullly and redirect to index with param deleted = 1.
- * Else, then template wasn't deleted successfully and redirect to index with param deleted = 0.
- */
-if ($result === TRUE)
-{
- header("Location:" . $customTemplateEngine->getUrl("index.php") . "&deleted=1");
-}
-else
-{
- header("Location:" . $customTemplateEngine->getUrl("index.php") . "&deleted=0");
+deleteTemplate();
+
+/**
+ * If TRUE, then template was deleted successfullly and redirect to index with param deleted = 1.
+ * Else, then template wasn't deleted successfully and redirect to index with param deleted = 0.
+ */
+if ($result === TRUE)
+{
+ header("Location:" . $customTemplateEngine->getUrl("index.php") . "&deleted=1");
+}
+else
+{
+ header("Location:" . $customTemplateEngine->getUrl("index.php") . "&deleted=0");
}
\ No newline at end of file
diff --git a/DownloadFilledTemplate.php b/DownloadFilledTemplate.php
index dfb2b9d..ed8c67a 100644
--- a/DownloadFilledTemplate.php
+++ b/DownloadFilledTemplate.php
@@ -1,6 +1,6 @@
-downloadTemplate();
\ No newline at end of file
diff --git a/EditTemplate.php b/EditTemplate.php
index f611529..58aca0a 100644
--- a/EditTemplate.php
+++ b/EditTemplate.php
@@ -1,18 +1,18 @@
-generateCreateEditTemplatePage($template_filtered);
-
-/**
- * Include REDCap footer.
- */
-require_once APP_PATH_DOCROOT . "ProjectGeneral/footer.php";
+generateCreateEditTemplatePage($template_filtered);
+
+/**
+ * Include REDCap footer.
+ */
+require_once APP_PATH_DOCROOT . "ProjectGeneral/footer.php";
diff --git a/FillAndSave.php b/FillAndSave.php
new file mode 100644
index 0000000..dcb2a64
--- /dev/null
+++ b/FillAndSave.php
@@ -0,0 +1,8 @@
+fillAndSave());
diff --git a/FillTemplate.php b/FillTemplate.php
index ea88654..76bac2e 100644
--- a/FillTemplate.php
+++ b/FillTemplate.php
@@ -1,27 +1,27 @@
-generateFillTemplatePage();
-
- /**
- * Include REDCap footer.
- */
- require_once APP_PATH_DOCROOT . "ProjectGeneral/footer.php";
-}
-else
-{
- $customTemplateEngine->batchFillReports();
-}
+generateFillTemplatePage();
+
+ /**
+ * Include REDCap footer.
+ */
+ require_once APP_PATH_DOCROOT . "ProjectGeneral/footer.php";
+}
+else
+{
+ $customTemplateEngine->batchFillReports();
+}
diff --git a/README.md b/README.md
index ca3c346..34ef549 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ When switching from the old module to the rebranded module, you must disable the
## Project Configurations
- Save Filled Templates to File Repository: This is specific to downloading a filled template, and will save a copy of the PDF to the File Repository.
+- [Optional] Enter templates by name and specify an alternative paper size, e.g. A4 (default is Letter), and/or select landscape orientation.
## Permissions
@@ -68,5 +69,4 @@ WARNING: This module is not currently able to support REDCap instances using lo
* upgraded included Javascript libraries:
* ckeditor to latest v4.21
* dompdf to latest v2.0
- * smarty to latest v4.1
-
+ * smarty to latest v4.1
\ No newline at end of file
diff --git a/SaveFileToField.php b/SaveFileToField.php
index fcd1001..82e9e7a 100644
--- a/SaveFileToField.php
+++ b/SaveFileToField.php
@@ -63,7 +63,7 @@
}
$dompdf = new Dompdf();
- $pdf_content = $customTemplateEngine->createPDF($dompdf, $header, $footer, $main);
+ $pdf_content = $customTemplateEngine->createPDF($dompdf, $header, $footer, $main, $filename);
if (!$customTemplateEngine->saveFileToField($filename, $pdf_content, $field_name, $record, $event_id))
{
diff --git a/SaveTemplate.php b/SaveTemplate.php
index 5fda080..116b955 100644
--- a/SaveTemplate.php
+++ b/SaveTemplate.php
@@ -1,8 +1,8 @@
-saveTemplate();
+saveTemplate();
print json_encode($result);
\ No newline at end of file
diff --git a/Template.php b/Template.php
index 122b0b6..ceb2ba6 100644
--- a/Template.php
+++ b/Template.php
@@ -1,1274 +1,1274 @@
-dictionary = REDCap::getDataDictionary('array', false);
- $this->instruments = REDCap::getInstrumentNames();
- $this->smarty = new Smarty();
- $this->smarty->setTemplateDir($templates_dir);
- $this->smarty->setCompileDir($compiled_dir);
- $this->smarty->assign("showLabelAndRow", $this->show_label_and_row);
- }
-
- /**
- * Checks whether all the siblings that come before or after an html element are empty
- *
- * @access private
- * @param DOMNode $elem Root element.
- * @param String $whichSibling Which sibling/direction to check. Can be either "previous" or "next".
- * @return Bool True if the given element and all its siblings are empty, false otherwise.
- */
- private function areSiblingsEmpty($elem, $whichSibling)
- {
- if ($elem == null)
- {
- return true;
- }
- else if ($whichSibling == "previous")
- {
- return $this->areSiblingsEmpty($elem->previousSibling, "previous") && empty($elem->nodeValue);
- }
-
- return $this->areSiblingsEmpty($elem->nextSibling, "next") && empty($elem->nodeValue);
- }
-
- /**
- * Retrieves empty child nodes within given element.
- *
- * @access private
- * @see Template::areSiblingsEmpty() For checking whether a table row is empty of data.
- * @param DOMNode $elem Root element.
- * @return Array An array of all empty nodes.
- */
- private function getEmptyNodes($elem)
- {
- $empty_elems = array();
-
- $isEmptyOrWhitespace = ctype_space($elem->nodeValue) || str_replace(array(" ", "\xC2\xA0"), "", $elem->nodeValue) == "";
-
- if ($elem->hasChildNodes())
- {
- foreach($elem->childNodes as $child)
- {
- $empty_child_elems = $this->getEmptyNodes($child);
- $empty_elems = array_merge($empty_elems, $empty_child_elems);
- }
- }
- /**
- * Checks for special whitespace characters, and tags that don't contain children.
- */
- else if ($isEmptyOrWhitespace && $elem->tagName != "img" && $elem->tagName != "body" && $elem->tagName != "hr" && $elem->tagName != "br")
- {
- /**
- * Empty
and | elements may pad out other data in table.
- * Make sure the entire row is empty, before removing.
- */
- if ($elem->tagName == "td" || $elem->tagName == "th")
- {
- if ($this->areSiblingsEmpty($elem->previousSibling, "previous") && $this->areSiblingsEmpty($elem->nextSibling, "next") && empty($elem->nodeValue))
- {
- $empty_elems[] = $elem;
- }
- }
- else
- {
- $empty_elems[] = $elem;
- }
- }
-
- return $empty_elems;
- }
-
- /**
- * Parses event data into an assosiative array that will be used by Smarty to fill templates
- * with data.
- *
- * Builds the array used in filling template data. Values are parsed according to the user's
- * data rights. i.e. Identifiers removed, no unvalidated text fields, etc... Values are associated with
- * field names, and field names are associated with event names (if project is longitudinal).
- *
- * @access private
- * @param Array $event_data Event data for a REDcap record.
- * @return Array An associative array of fields mapped to values, or events mapped to an array of fields mapped to values (if longitudinal).
- */
- private function parseEventData($event_data)
- {
- $user = strtolower(USERID);
- $rights = REDCap::getUserRights($user);
- $rights_object = new ExportRights($rights); // populate a rights object with this user's instrument-level rights
- $external_fields = array();
- $this->instruments = REDCap::getInstrumentNames();
- foreach ($this->instruments as $unique_name => $label)
- {
- $external_fields[] = "{$unique_name}_complete";
- $external_fields[] = "{$unique_name}_timestamp";
- }
-
- $event_fields_and_vals = array();
- foreach($event_data as $field_name => $value)
- {
- $value = trim(strip_tags($value));
- if (in_array($field_name, $external_fields))
- {
- $event_fields_and_vals[$field_name] = $value;
- }
- // else if ($field_name !== "redcap_event_name")
- else if ($field_name !== "redcap_event_name"
- && $field_name !== "redcap_repeat_instrument"
- && $field_name !== "redcap_repeat_instance")
- {
- if ($this->dictionary[$field_name]["field_type"] === "checkbox") { // modify checkbox values
- /*
- * Check user's data rights
- * Value 0 = no rights
- * Value 1 = full rights
- * Value 2 = de-identified rights
- * Value 3 = remove all identifiers rights
- * De-identified rights: All unvalidated text fields & notes will be removed, as well as any date/time fields
- * Remove all identifiers rights: hide data from all tagged identifier fields.
- */
- $event_fields_and_vals[$field_name] = array();
-
- if ($rights_object->field_to_rights_value[$field_name] !== "1") { // check if data needs to be hidden
-
- if (($rights_object->field_to_rights_value[$field_name] === "3") && ($this->dictionary[$field_name]["identifier"] === "y")) { // remove all identifiers, and this is an identifier
-
- $event_fields_and_vals[$field_name]["allValues"] = $this->removed_replacement;
-
- } else if ($rights_object->field_to_rights_value[$field_name] === "2") { // de-identified rights, so remove marked identifiers, freetext and date/time fields
-
- $event_fields_and_vals[$field_name]["allValues"] = $this->de_identified_replacement;
-
- } else { // no rights, so remove everything
-
- $event_fields_and_vals[$field_name]["allValues"] = $this->no_rights_replacement;
-
- } // end else
-
- } else { // full rights, so treat this data normally
-
- $all_choices = explode("|", $this->dictionary[$field_name]["select_choices_or_calculations"]);
- $all_choices = array_map(function ($v) {
- $v = strip_tags($v);
- $first_comma = strpos($v, ",");
- return trim(substr($v, $first_comma + 1));
- }, $all_choices);
-
- foreach($all_choices as $choice)
- {
- if (strpos($value, $choice) !== FALSE)
- {
- $event_fields_and_vals[$field_name][] = $choice;
- }
- }
-
- $event_fields_and_vals[$field_name]["allValues"] = implode(", ", explode(",", $value));
-
- } // end else
-
- } else { // non-checkbox fields, so check more thorougly
- /*
- * Check user's data rights
- * Value 0 = no rights
- * Value 1 = full rights
- * Value 2 = de-identified rights
- * Value 3 = remove all identifiers rights
- * De-identified rights: All unvalidated text fields & notes will be removed, as well as any date/time fields
- * Remove all identifiers rights: hide data from all tagged identifier fields.
- */
- $event_fields_and_vals[$field_name] = $value;
-
- if ($rights_object->field_to_rights_value[$field_name] !== "1") { // check if value needs to be modified
-
- if ($rights_object->field_to_rights_value[$field_name] === "3") { // remove identifiers right, and this is an identifier
-
- if ($this->dictionary[$field_name]["identifier"] === "y") {
-
- $event_fields_and_vals[$field_name] = $this->removed_replacement;
-
- } // end if
-
- } else if ($rights_object->field_to_rights_value[$field_name] === "2") { // de-identified rights, so remove freetext fields, marked identifiers, and date/time fields
-
- if ((($this->dictionary[$field_name]["field_type"] === "text") && (in_array($this->dictionary[$field_name]["text_validation_type_or_show_slider_number"], $this->date_formats) ||
- empty($this->dictionary[$field_name]["text_validation_type_or_show_slider_number"]))) // non-validated text fields, meaning free short text, but not calc fields or decimal/date/int
- || ($this->dictionary[$field_name]["field_type"] === "notes")
- || ($this->dictionary[$field_name]["identifier"] === "y")) {
-
- $event_fields_and_vals[$field_name] = $this->de_identified_replacement;
-
- } // end if
-
- } else { // rights value of 0, so remove all data from these fields
-
- $event_fields_and_vals[$field_name] = $this->no_rights_replacement;
-
- } // end else
-
- } else { // treat the data normally
-
- if ($this->dictionary[$field_name]["field_type"] === "notes") { // make HTML entities
-
- $event_fields_and_vals[$field_name] = str_replace("\r\n", " ", htmlentities($value));
-
- } else { // return without modification
-
- $event_fields_and_vals[$field_name] = $value;
-
- } // end else
-
- } // end else
-
- } // end else
-
- } // end else if
-
- } // end foreach
-
- return $event_fields_and_vals;
-
- } // end function
-
- /**
- * Replaces given text with replacement.
- *
- * @access private
- * @param String $text The text to replace.
- * @param String $replacement The replacement text.
- * @return String A string with the replaced text.
- */
- private function replaceStrings($text, $replacement)
- {
- preg_match_all("/'/", $text, $quotes, PREG_OFFSET_CAPTURE);
- $quotes = $quotes[0];
- if (sizeof($quotes) % 2 === 0)
- {
- $i = 0;
- $to_replace = array();
- while ($i < sizeof($quotes))
- {
- $to_replace[] = substr($text, $quotes[$i][1], $quotes[$i + 1][1] - $quotes[$i][1] + 1);
- $i = $i + 2;
- }
-
- $text = str_replace($to_replace, $replacement, $text);
- }
- return $text;
- }
-
- /**
- * Parses a syntax string into blocks.
- *
- * @access private
- * @param String $syntax The syntax to parse.
- * @return Array An array of blocks that make up the syntax passed.
- */
- private function getSyntaxParts($syntax)
- {
- $syntax = str_replace(array("['", "']"), array("[", "]"), $syntax);
- $syntax = $this->replaceStrings(trim($syntax), "''"); //Replace strings with ''
-
- $parts = array();
- $previous = array();
-
- $i = 0;
- while($i < strlen($syntax))
- {
- $char = $syntax[$i];
- switch($char)
- {
- case ",":
- case "(":
- case ")":
- case "]":
- $part = trim(implode("", $previous));
- $previous = array();
- if ($part !== "")
- {
- $parts[] = $part;
- }
- $parts[] = $char;
- $i++;
- break;
- case "[":
- if ($syntax[$i-1] == " ")
- {
- $parts[] = " ";
- }
- $part = trim(implode("", $previous));
- if ($part !== "")
- {
- $parts[] = $part;
- }
- $parts[] = $char;
- $previous = array();
- $i++;
- break;
- case " ":
- $part = trim(implode("", $previous));
- $previous = array();
- if ($part !== "")
- {
- $parts[] = $part;
- }
- $i++;
- break;
- default:
- $previous[] = $char;
- if ($i == strlen($syntax) - 1)
- {
- $part = trim(implode("", $previous));
- if ($part !== "")
- {
- $parts[] = $part;
- }
- }
- $i++;
- break;
- }
- }
-
- return $parts;
- }
-
- /**
- * Checks whether fields and events exist within project
- *
- * @access private
- * @param String $var The field name/event name to check
- * @return Bool True if $var is a valid REDCap field or event on the project, false otherwise
- */
- private function isValidFieldOrEvent($var)
- {
- $var = trim($var, "'");
-
- $events = REDCap::getEventNames(true, true); // If there are no events (the project is classical), the method will return false
-
- /**
- * Get REDCap completion fields
- */
- $external_fields = array();
- foreach ($this->instruments as $unique_name => $label)
- {
- $external_fields[] = "{$unique_name}_complete";
- $external_fields[] = "{$unique_name}_timestamp";
- }
-
- if ($var !== "allValues" && !in_array($var, $external_fields))
- {
- $dictionary = $this->dictionary[$var];
- if (($events === FALSE && empty($dictionary)) ||
- ($events !== FALSE && !in_array($var, $events) && empty($dictionary)))
- {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Checks whether fields and events are being queried correctly.
- *
- * @access private
- * @param String $text The line of text to validate.
- * @param Integer $line_num The current line number in the template.
- * @return Array An array of errors, with the line number appended to indicate where it occured.
- */
- private function validateFieldQueries($text, $line_num)
- {
- $errors = array();
-
- // Get all occurences of an opening square bracket "["
- preg_match_all('/\[/', $text, $opening_brackets, PREG_OFFSET_CAPTURE);
-
- if (!empty($opening_brackets[0]))
- {
- // Get the event/field name between each opening and closing bracket, and check if it exists
- foreach($opening_brackets[0] as $bracket)
- {
- $start_pos = $bracket[1]+1;
-
- $closing_bracket = strpos($text, "]", $start_pos);
- if ($closing_bracket !== FALSE)
- {
- $var = substr($text, $start_pos, $closing_bracket - $start_pos);
- if (substr($var, 0, 1) !== "'" && substr($var, strlen($var)-1, 1) !== "'")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] '$var' must be enclosed with single quotes.";
- }
-
- $var = trim($var, "'\"");
-
- if ($var !== "allValues")
- {
- $dictionary = $this->dictionary[$var];
- if (!empty($dictionary))
- {
- if ($dictionary["field_type"] === "checkbox")
- {
- // Check if checkbox is queried via in_array()
- $in_array = false;
- $in_array_start_pos = 0;
- while (($in_array_start_pos = strpos($text, "in_array('", $in_array_start_pos)) !== FALSE)
- {
- $end_of_check_value = strpos($text, "', \$redcap", $in_array_start_pos+1);
- $in_array_end_pos = strpos($text, ")", $end_of_check_value+1);
- if (($in_array_start_pos < $start_pos) && ($in_array_end_pos > $start_pos))
- {
- $in_array = true;
- break;
- }
- $in_array_start_pos = $in_array_start_pos + 1;
- }
-
- // check if checkbox is queried via ['allValues']
- $all_values_str = substr($text, $closing_bracket+1, 13);
- if ($all_values_str !== "['allValues']" && !$in_array)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] '$var' is a checkbox and can only be queried using in_array or \$redcap['$var']['allValues']";
- }
- }
- }
- }
- }
- }
- }
- return $errors;
- }
-
- /**
- * Validate syntax.
- *
- * @access private
- * @see Template::validateFieldQueries() For checking whether fields and events are queried correctly.
- * @see Template::getSyntaxParts() For retreiving blocks of syntax from the given syntax string.
- * @param String $syntax The syntax to validate.
- * @param Integer $line_num The current line number in the template.
- * @return Array An array of errors, with the line number appended to indicate where it occured.
- */
- private function validateSyntax($syntax, $line_num)
- {
- if (!empty($syntax))
- {
- $errors = $this->validateFieldQueries($syntax, $line_num);
-
- if ((sizeof(explode("'", $syntax)) - 1) % 2 > 0)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Odd number of single quotes exist. You've either added an extra quote, forgotten to close one, or forgotten to escape one.";
- }
- else if ($syntax != strip_tags($syntax))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Report logic cannot have any HTML between {}";
- }
- else if (preg_match("/{/", $syntax) !== 0 || preg_match("/}/", $syntax) !== 0)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] There is either a '{' or '}' within {}. They are special characters and can only denote the beginning and end of syntax.";
- }
-
- // Check symmetry of ()
- if ($parts == null) { $parts = array();} // PHP8 compatability patch Dan Evans, 2023-06-09
- if (sizeof(array_keys($parts, "(")) != sizeof(array_keys($parts, ")")))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Odd number of parenthesis (. You've either added an extra parenthesis, or forgot to close one.";
- }
-
- // Check symmetry of []
- if (sizeof(array_keys($parts, "[")) != sizeof(array_keys($parts, "]")))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Odd number of square brackets [. You've either added an extra bracket, or forgot to close one.";
- }
-
- $parts = $this->getSyntaxParts($syntax);
-
- foreach($parts as $index => $part)
- {
- switch ($part) {
- case "if":
- case "elseif":
- // Must have either a ( or ) or $redcap or $showLabelAndRow or in_array after
- if ($index != sizeof($parts) - 1)
- {
- if ($index !== 0)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed $part condition. $part clause must be first part of syntax.";
- }
- else
- {
- $next_part = $parts[$index + 1];
-
- if (($next_part !== "("
- && ($next_part != "''" && $previous == "in_array")
- && $next_part !== ")"
- && $next_part !== "\$redcap"
- && $next_part !== "\$showLabelAndRow"
- && $next_part !== "in_array"
- && $next_part !== "''"))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
- }
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed $part condition. You cannot have an empty $part clause.";
- }
- break;
- case "(":
- $previous = $parts[$index - 1];
- $next_part = $parts[$index + 1];
-
- if (($next_part !== "("
- && ($next_part != "''" && $previous == "in_array")
- && $next_part !== ")"
- && $next_part !== "\$redcap"
- && $next_part !== "\$showLabelAndRow"
- && $next_part !== "in_array"
- && $next_part !== "''"))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after (.";
- }
- else if ($next_part == "(" && $previous == "in_array")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Malformed in_array() function.";
- }
- break;
- case ")":
- // Must have either a ) or logical operator after, if not the last part of syntax
- if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if ($next_part !== ")" && !in_array($next_part, $this->logical_operators))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after ).";
- }
- }
- break;
- case "eq":
- case "ne":
- case "neq":
- case "gt":
- case "ge":
- case "gte":
- case "lt":
- case "le":
- case "lte":
- // Must have either a ( or $redcap or $showLabelAndRow or in_array or string or not after
- // If there's another logical operator two spaces before, is illegal.
- if ($index == 0)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a comparison operator $part as the first part in syntax.";
- }
- else if ($index != sizeof($parts) - 1)
- {
- $previous = $parts[$index - 2];
- $next_part = $parts[$index + 1];
-
- if (in_array($previous, $this->logical_operators) && $previous !== "or" && $previous !== "and")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $part. You cannot chain comparison operators together, you must use an and or an or";
- }
- if (!empty($next_part)
- && $next_part !== "("
- && $next_part !== "\$redcap"
- && $next_part !== "\$showLabelAndRow"
- && $next_part !== "''"
- && $next_part !== "in_array"
- && $next_part !== "not"
- && !is_numeric($next_part))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a comparison operator $part as the last part in syntax.";
- }
- break;
- case "not":
- case "or":
- case "and":
- // Must have either a ( or $redcap or $showLabelAndRow or in_array or string or not after
- if ($index == 0)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a logical operator $part as the first part in syntax.";
- }
- else if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if (!empty($next_part)
- && $next_part !== "("
- && $next_part !== "\$redcap"
- && $next_part !== "\$showLabelAndRow"
- && $next_part !== "''"
- && $next_part !== "in_array"
- && $next_part !== "not"
- && !is_numeric($next_part))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a logical operator $part as the last part in syntax.";
- }
- break;
- case "''":
- // Must have either a logical operator or ) or , or ]
- if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if ($next_part !== ")"
- && $next_part != ","
- && $next_part != "]"
- && !in_array($next_part, $this->logical_operators))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after string value within ''.";
- }
- }
- break;
- case "\$redcap":
- // Must have a [
- if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if ($next_part !== "[")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid \$redcap field query.";
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid \$redcap field query at end of syntax.";
- }
- break;
- case "[":
- // Must have a ''
- if ($index != sizeof($parts) - 1)
- {
- $previous = $parts[$index - 1];
- $next_part = $parts[$index + 1];
-
- if ($previous !== "\$redcap" && $previous !== "]")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Each field and events query must be preceeded by \$redcap";
- }
-
- if ($next_part == "]")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have empty [] brackets";
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have opening [ as end of syntax.";
- }
- break;
- case "\$showLabelAndRow":
- // Must have either a logical operator or ) after, if not last item in syntax
- if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if ($next_part !== ")"
- && !in_array($next_part, $this->logical_operators))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
- }
- }
- break;
- case "]":
- // Must have either a logical operator or ) or [ after, if not last item in syntax
- if ($index != sizeof($parts) - 1)
- {
- $previous_2 = $parts[$index - 2];
- $next_part = $parts[$index + 1];
-
- if ($previous_2 !== "[")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Unclosed or empty ] bracket.";
- }
-
- if ($next_part !== ")"
- && $next_part !== "["
- && !in_array($next_part, $this->logical_operators))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid '$next_part' after $part.";
- }
- }
- break;
- case ",":
- // Must have a $redcap field query after, has to be part of in_array function call
- if ($index != sizeof($parts) - 1)
- {
- $previous = $parts[$index - 1];
- $previous_2 = $parts[$index - 2];
- $previous_3 = $parts[$index - 3];
-
- if ($previous_3 !== "in_array" || $previous_2 !== "(" || $previous !== "''")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Improper use of , in syntax. Are you trying to call in_array()?";
- }
-
- $next_part = $parts[$index + 1];
- if ($next_part !== "\$redcap")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num]. Invalid $next_part after ,.";
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] $part cannot be end of syntax";
- }
- break;
- case "in_array":
- // Must have a ( after
- if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if ($next_part !== "(")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Malformed in_array() function.";
- }
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Malformed in_array() function.";
- }
- break;
- case "else":
- case "/if":
- // Must be the only clause in syntax
- if ($index != sizeof($parts) - 1)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed $part clause. Must be of form {{$part}}.";
- }
- break;
- default:
- // If it's a number, must have ) or logical operator after, if not last item in syntax
- if (is_numeric($part))
- {
- if ($index != sizeof($parts) - 1)
- {
- $next_part = $parts[$index + 1];
- if (!empty($next_part) && $next_part !== ")" && !in_array($next_part, $this->logical_operators))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
- }
- }
- }
- // Check if it's a string
- else if (!empty($part) &&
- $part[0] != "'" &&
- $part[0] != "\"" &&
- $part[strlen($part) - 1] != "'" &&
- $part[strlen($part) - 1] != "\"" &&
- !$this->isValidFieldOrEvent($part))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] $part is not a valid event/field/syntax in this project";
- }
- break;
- }
- }
- }
- return $errors;
- }
-
- /**
- * Validate if statements, by checking the following:
- *
- * - If statements have matching opening and closing statements.
- * - Elseifs are associated with an if statement.
- * - Every if statement has at most one else clause.
- *
- * @access private
- * @param Array $lines An array of lines within template.
- * @return Array An array of errors, with their line numbers appended to indicate where it occured.
- */
- private function validateIfStatements($lines)
- {
- $errors = array();
-
- // Find all occurences of opening {if, {/if}
- $opening_ifs = array();
- $closing_ifs = array();
-
- foreach($lines as $index => $line)
- {
- // Could be multiple statements on same line
- $last_pos = 0;
- while(($last_pos = strpos($line, '{if', $last_pos)) !== FALSE)
- {
- $line_num = $index + 1;
- if ($line[$last_pos + 3] !== " ")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed if/elseif statement. Need a space after 'if'";
- }
-
- $opening_ifs[] = array(
- "line_num" => $line_num,
- "line_pos" => $last_pos
- );
- $last_pos++;
- }
-
- $last_pos = 0;
- while(($last_pos = strpos($line, '{/if}', $last_pos)) !== FALSE)
- {
- $closing_ifs[] = array(
- "line_num" => $index + 1,
- "line_pos" => $last_pos
- );
- $last_pos++;
- }
- }
-
- $num_opening_ifs = sizeof($opening_ifs);
- $num_closing_ifs = sizeof($closing_ifs);
-
- $opening_ifs_copy = array_reverse($opening_ifs);
- $closing_ifs_copy = array_reverse($closing_ifs);
-
- foreach($opening_ifs_copy as $index => $opening_if)
- {
- $key = null;
- for ($i = 0; $i < sizeof($closing_ifs_copy); $i++)
- {
- $closing_if = $closing_ifs_copy[$i];
-
- if (($opening_if["line_num"] < $closing_if["line_num"]) ||
- ($opening_if["line_num"] == $closing_if["line_num"] && $opening_if["line_pos"] < $closing_if["line_pos"]))
- {
- $key = $i;
- break;
- }
- }
-
- if (!is_null($key))
- {
- unset($closing_ifs_copy[$key]);
- $closing_ifs_copy = array_values($closing_ifs_copy);
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [" . $opening_if["line_num"] . "] Missing {/if}";
- }
- }
-
- foreach($closing_ifs_copy as $closing_if)
- {
- $errors[] = "ERROR [EDITOR] LINE [" . $closing_if["line_num"] . "] Extra {/if}";
- }
-
- if (empty($errors))
- {
- // Find all occurences of {elseif, {else}
- $elseifs = array();
- $elses = array();
-
- foreach($lines as $index => $line)
- {
- $last_pos = 0;
- while(($last_pos = strpos($line, '{elseif', $last_pos)) !== FALSE)
- {
- $line_num = $index + 1;
- if ($line[$last_pos + 7] !== " ")
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed if/elseif statement. Need a space after 'elseif'";
- }
-
- $elseifs[] = array(
- "line_num" => $line_num,
- "line_pos" => $last_pos
- );
- $last_pos++;
- }
-
- $last_pos = 0;
- while(($last_pos = strpos($line, '{else}', $last_pos)) !== FALSE)
- {
- $elses[] = array(
- "line_num" => $index + 1,
- "line_pos" => $last_pos
- );
- $last_pos++;
- }
- }
-
- $num_elses = sizeof($elses);
- $num_elseifs = sizeof($elseifs);
-
- // Check that every elseif clause is associated with an if statement.
- foreach($elseifs as $elseif)
- {
- $line_num = $elseif["line_num"];
- $line_pos = $elseif["line_pos"];
-
- if ($num_opening_ifs == 0)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Every {elseif} clause must be associated with an {if} statement";
- }
- else
- {
- for ($i = 0; $i < $num_opening_ifs; $i++)
- {
- $opening_if = $opening_ifs[$i];
- $closing_if = $closing_ifs[$i];
-
- // Check if there's an else clause between the elseif and the opening if
- $arr = array_filter($elses, function($v) use ($opening_if, $closing_if, $line_num, $line_pos) {
- $v_line_num = $v["line_num"];
- $v_line_pos = $v["line_pos"];
-
- if ($opening_if["line_num"] < $v_line_num && $v_line_num < $closing_if["line_num"] && $v_line_num < $line_num)
- {
- return true;
- }
- else if (($opening_if["line_num"] == $v_line_num && $v_line_num == $closing_if["line_num"] && $v_line_num == $line_num) ||
- ($opening_if["line_num"] == $v_line_num && $v_line_num < $closing_if["line_num"] && $v_line_num == $line_num) ||
- ($opening_if["line_num"] < $v_line_num && $v_line_num == $closing_if["line_num"] && $v_line_num == $line_num) ||
- ($opening_if["line_num"] < $v_line_num && $v_line_num < $closing_if["line_num"] && $v_line_num == $line_num))
- {
- return $v_line_pos < $line_pos;
- }
- });
-
- if (($opening_if["line_num"] < $line_num && $line_num < $closing_if["line_num"] && empty($arr)) ||
- ($opening_if["line_num"] == $line_num && $line_num == $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos && $line_pos < $closing_if["line_pos"] && empty($arr)) ||
- ($opening_if["line_num"] == $line_num && $line_num < $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos && empty($arr)) ||
- ($opening_if["line_num"] < $line_num && $line_num == $closing_if["line_num"] && $line_pos < $closing_if["line_pos"] && empty($arr)))
- {
- break;
- }
- else if ($i == $num_opening_ifs - 1)
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Every {elseif} clause must be associated with an {if} statement";
- }
- }
- }
- }
-
- // Check that every if statement has at most one else clause, and that every else clause is associated with an if statement.
- foreach($elses as $index => $else)
- {
- $key = null;
-
- $line_num = $else["line_num"];
- $line_pos = $else["line_pos"];
-
- for ($i = 0; $i < sizeof($opening_ifs); $i++)
- {
- $opening_if = $opening_ifs[$i];
- $closing_if = $closing_ifs[$i];
-
- if (($opening_if["line_num"] < $line_num && $line_num < $closing_if["line_num"]) ||
- ($opening_if["line_num"] == $line_num && $line_num == $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos && $line_pos < $closing_if["line_pos"]) ||
- ($opening_if["line_num"] == $line_num && $line_num < $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos) ||
- ($opening_if["line_num"] < $line_num && $line_num == $closing_if["line_num"] && $line_pos < $closing_if["line_pos"]))
- {
- $key = $i;
- break;
- }
- }
-
- if (!is_null($key))
- {
- unset($opening_ifs[$i]);
- unset($closing_ifs[$i]);
-
- $opening_ifs = array_values($opening_ifs);
- $closing_ifs = array_values($closing_ifs);
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Template either has more than one {else} clause within an {if} statement, or an {else} clause outside of an {if} statement.";
- }
- }
- }
-
- return $errors;
- }
-
- /**
- * Performs template validation.
- *
- * Checks that there are no unclosed curly brackets within the template, as they're special characters that denote
- * Smarty syntax. If validation passes, then retrieve all text enclosed within curly brackets, and validate them.
- *
- * @see Template::validateSyntax() For validating all syntax, escept if statements.
- * @see Template::validateIfStatements() For validating if statements.
- * @param String $template_data Template contents.
- * @return Array An array of errors, with their line numbers appended to indicate where it occured.
- */
- public function validateTemplate($template_data)
- {
- // Decode all HTML entities, and replace escaped quotes with a backtick.
- $template_data = preg_replace("/\\\\'/", "`", html_entity_decode($template_data, ENT_HTML5 | ENT_QUOTES));
-
- $errors = array();
-
- $lines = explode("\n", $template_data);
-
- foreach($lines as $index => $line)
- {
- $line_num = $index + 1;
-
- // Find all occurences of opening { and closing }
- preg_match_all('/{/', $line, $opening_brackets, PREG_OFFSET_CAPTURE);
- preg_match_all('/}/', $line, $closing_brackets, PREG_OFFSET_CAPTURE);
-
- if (sizeof($opening_brackets[0]) != sizeof($closing_brackets[0]))
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Has uneven number of '{' and '}' brackets. Please check there are no extra opening or unclosed brackets.";
- }
- else
- {
- // Get the smarty syntax between each pair of {}
- foreach($opening_brackets[0] as $bracket)
- {
- $start_pos = $bracket[1]+1;
- $closing_bracket = strpos($line, "}", $start_pos);
- if ($closing_bracket !== FALSE)
- {
- $text = substr($line, $start_pos, $closing_bracket - $start_pos);
- $errors = array_merge($errors, $this->validateSyntax($text, $line_num));
- }
- else
- {
- $errors[] = "ERROR [EDITOR] LINE [$line_num] Is missing a closing '}'.";
- }
- }
- }
- }
-
- return array_merge($errors, $this->validateIfStatements($lines));
- }
-
- /**
- * Fill a given template with REDCap record data.
- *
- * Retrieves REDCap data, and parses it into an array used by Smarty to fill the
- * template with data. After using Smarty to fill the template, empty nodes are
- * found and deleted.
- *
- * @see Template::parseEventData() For parsing event data into Array used in Smarty.
- * @see Template::getEmptyNodes() For retrieving empty nodes in HTML.
- * @param String $template_name The Template name.
- * @param Integer $record A REDCap record ID.
- * @return String The template filled with REDCap record data.
- */
- public function fillTemplate($template_name, $record)
- {
- $filled_template = "";
-
- $user = strtolower(USERID);
- //$rights = REDCap::getUserRights($user);
-
- $template = REDCap::getData("json", $record, null, null, null, TRUE, FALSE, TRUE, null, TRUE);
-
- $json = json_decode($template, true);
-
- $repeatable_instruments_parsed = array();
-
- if (REDCap::isLongitudinal())
- {
- $this->redcap = array();
-
- $event_ids = array_values(REDCap::getEventNames(TRUE));
- $event_labels = array_values(REDCap::getEventNames(FALSE, TRUE));
-
- $events = array();
- for ($i = 0 ; $i <= count($event_labels)-1; $i++)
- {
- $events[$event_labels[$i]] = $event_ids[$i];
- }
-
- $first_event = $event_ids[0];
-
- foreach($json as $index => $event_data)
- {
- $data = array();
- $event = $events[$event_data["redcap_event_name"]];
- if (!empty($event_data["redcap_repeat_instance"]))
- {
- // Repeatable instrument
- if (!empty($event_data["redcap_repeat_instrument"]) && !in_array($event_data["redcap_repeat_instrument"], $repeatable_instruments_parsed))
- {
- // Get latest instance of repeatable instrument.
- // Retrieve all repeatable instances of event.
- $repeatable_instrument_instances = array_filter($json, function($value) use($event_data){
- return $value["redcap_repeat_instrument"] == $event_data["redcap_repeat_instrument"];
- });
- $repeatable_instrument_instances = array_values($repeatable_instrument_instances);
- // Get the latest instance and parse it.
- $repeat_instances = array_column($repeatable_instrument_instances, "redcap_repeat_instance");
- $latest_instance = max($repeat_instances);
- $key = array_search($latest_instance, $repeat_instances);
-
- if (empty($this->redcap[$event]))
- {
- $data = $this->parseEventData($repeatable_instrument_instances[$key]);
- }
- else
- {
- // Merges repeatable instrument data with non-repeatable instrument data in same event.
- $data = $this->redcap[$event];
- $repeatable_instrument_data = $this->parseEventData($repeatable_instrument_instances[$key]);
-
- foreach($repeatable_instrument_data as $field => $value)
- {
- if (empty($data[$field]))
- $data[$field] = $value;
- }
- }
-
- $repeatable_instruments_parsed[] = $event_data["redcap_repeat_instrument"];
- }
- // Repeatable event
- else if (empty($this->redcap[$event]))
- {
- // Retrieve all repeatable instances of event.
- $repeatable_event_instances = array_filter($json, function($value) use($event_data){
- return $value["redcap_event_name"] == $event_data["redcap_event_name"];
- });
- $repeatable_event_instances = array_values($repeatable_event_instances);
-
- // Get the latest instance and parse it.
- $repeat_instances = array_column($repeatable_event_instances, "redcap_repeat_instance");
- $latest_instance = max($repeat_instances);
- $key = array_search($latest_instance, $repeat_instances);
- $data = $this->parseEventData($repeatable_event_instances[$key]);
- }
- }
- else
- {
- if (empty($this->redcap[$event]))
- {
- $data = $this->parseEventData($event_data);
- }
- else
- {
- $data = array_merge($this->redcap[$event], $this->parseEventData($event_data));
- }
- }
-
- if (!empty($data))
- {
- if ($first_event == $event)
- {
- $this->redcap = array_merge($this->redcap, $data);
- }
- $this->redcap[$event] = $data;
- }
- }
- }
- else
- {
- foreach($json as $index => $event_data)
- {
- // Repeatable instrument
- if (!empty($event_data["redcap_repeat_instance"]))
- {
- // Repeatable instrument
- if (!empty($event_data["redcap_repeat_instrument"]) && !in_array($event_data["redcap_repeat_instrument"], $repeatable_instruments_parsed))
- {
- // Get latest instance of repeatable instrument.
- // Retrieve all repeatable instances of event.
- $repeatable_instrument_instances = array_filter($json, function($value) use($event_data){
- return $value["redcap_repeat_instrument"] == $event_data["redcap_repeat_instrument"];
- });
- $repeatable_instrument_instances = array_values($repeatable_instrument_instances);
-
- // Get the latest instance and parse it.
- $repeat_instances = array_column($repeatable_instrument_instances, "redcap_repeat_instance");
- $latest_instance = max($repeat_instances);
- $key = array_search($latest_instance, $repeat_instances);
-
- // Merges repeatable instrument data with non-repeatable instrument data in same event.
- $repeatable_instrument_data = $this->parseEventData($repeatable_instrument_instances[$key]);
- foreach($repeatable_instrument_data as $field => $value)
- {
- if (empty($this->redcap[$field]))
- $this->redcap[$field] = $value;
- }
-
- $repeatable_instruments_parsed[] = $event_data["redcap_repeat_instrument"];
- }
- }
- else
- {
- $this->redcap = $this->parseEventData($event_data);
- // parseEventData() is stripping out the comments field!
- }
- }
- }
-
- try
- {
- $this->smarty->assign("redcap", $this->redcap);
- $filled_template = $this->smarty->fetch($template_name);
-
- // Remove empty nodes
- $doc = new DOMDocument();
- $doc->loadHTML($filled_template);
- $body = $doc->getElementsByTagName("body")->item(0);
-
- $empty_elems = $this->getEmptyNodes($body);
- while (!empty($empty_elems))
- {
- foreach($empty_elems as $elem)
- {
- $elem->parentNode->removeChild($elem);
- }
- $empty_elems = $this->getEmptyNodes($body); // There may be new empty nodes after the previous ones were removed.
- }
- $filled_template = $doc->saveHTML();
- }
- catch (Exception $e)
- {
- throw new Exception("Error on line " . $e->getLine() . ": " . $e->getMessage());
- }
-
- return $filled_template;
- }
-}
+dictionary = REDCap::getDataDictionary('array', false);
+ $this->instruments = REDCap::getInstrumentNames();
+ $this->smarty = new Smarty();
+ $this->smarty->setTemplateDir($templates_dir);
+ $this->smarty->setCompileDir($compiled_dir);
+ $this->smarty->assign("showLabelAndRow", $this->show_label_and_row);
+ }
+
+ /**
+ * Checks whether all the siblings that come before or after an html element are empty
+ *
+ * @access private
+ * @param DOMNode $elem Root element.
+ * @param String $whichSibling Which sibling/direction to check. Can be either "previous" or "next".
+ * @return Bool True if the given element and all its siblings are empty, false otherwise.
+ */
+ private function areSiblingsEmpty($elem, $whichSibling)
+ {
+ if ($elem == null)
+ {
+ return true;
+ }
+ else if ($whichSibling == "previous")
+ {
+ return $this->areSiblingsEmpty($elem->previousSibling, "previous") && empty($elem->nodeValue);
+ }
+
+ return $this->areSiblingsEmpty($elem->nextSibling, "next") && empty($elem->nodeValue);
+ }
+
+ /**
+ * Retrieves empty child nodes within given element.
+ *
+ * @access private
+ * @see Template::areSiblingsEmpty() For checking whether a table row is empty of data.
+ * @param DOMNode $elem Root element.
+ * @return Array An array of all empty nodes.
+ */
+ private function getEmptyNodes($elem)
+ {
+ $empty_elems = array();
+
+ $isEmptyOrWhitespace = ctype_space($elem->nodeValue) || str_replace(array(" ", "\xC2\xA0"), "", $elem->nodeValue) == "";
+
+ if ($elem->hasChildNodes())
+ {
+ foreach($elem->childNodes as $child)
+ {
+ $empty_child_elems = $this->getEmptyNodes($child);
+ $empty_elems = array_merge($empty_elems, $empty_child_elems);
+ }
+ }
+ /**
+ * Checks for special whitespace characters, and tags that don't contain children.
+ */
+ else if ($isEmptyOrWhitespace && $elem->tagName != "img" && $elem->tagName != "body" && $elem->tagName != "hr" && $elem->tagName != "br")
+ {
+ /**
+ * Empty | and | elements may pad out other data in table.
+ * Make sure the entire row is empty, before removing.
+ */
+ if ($elem->tagName == "td" || $elem->tagName == "th")
+ {
+ if ($this->areSiblingsEmpty($elem->previousSibling, "previous") && $this->areSiblingsEmpty($elem->nextSibling, "next") && empty($elem->nodeValue))
+ {
+ $empty_elems[] = $elem;
+ }
+ }
+ else
+ {
+ $empty_elems[] = $elem;
+ }
+ }
+
+ return $empty_elems;
+ }
+
+ /**
+ * Parses event data into an assosiative array that will be used by Smarty to fill templates
+ * with data.
+ *
+ * Builds the array used in filling template data. Values are parsed according to the user's
+ * data rights. i.e. Identifiers removed, no unvalidated text fields, etc... Values are associated with
+ * field names, and field names are associated with event names (if project is longitudinal).
+ *
+ * @access private
+ * @param Array $event_data Event data for a REDcap record.
+ * @return Array An associative array of fields mapped to values, or events mapped to an array of fields mapped to values (if longitudinal).
+ */
+ private function parseEventData($event_data)
+ {
+ $user = strtolower(USERID);
+ $rights = REDCap::getUserRights($user);
+ $rights_object = new ExportRights($rights); // populate a rights object with this user's instrument-level rights
+ $external_fields = array();
+ $this->instruments = REDCap::getInstrumentNames();
+ foreach ($this->instruments as $unique_name => $label)
+ {
+ $external_fields[] = "{$unique_name}_complete";
+ $external_fields[] = "{$unique_name}_timestamp";
+ }
+
+ $event_fields_and_vals = array();
+ foreach($event_data as $field_name => $value)
+ {
+ $value = trim(strip_tags($value));
+ if (in_array($field_name, $external_fields))
+ {
+ $event_fields_and_vals[$field_name] = $value;
+ }
+ // else if ($field_name !== "redcap_event_name")
+ else if ($field_name !== "redcap_event_name"
+ && $field_name !== "redcap_repeat_instrument"
+ && $field_name !== "redcap_repeat_instance")
+ {
+ if ($this->dictionary[$field_name]["field_type"] === "checkbox") { // modify checkbox values
+ /*
+ * Check user's data rights
+ * Value 0 = no rights
+ * Value 1 = full rights
+ * Value 2 = de-identified rights
+ * Value 3 = remove all identifiers rights
+ * De-identified rights: All unvalidated text fields & notes will be removed, as well as any date/time fields
+ * Remove all identifiers rights: hide data from all tagged identifier fields.
+ */
+ $event_fields_and_vals[$field_name] = array();
+
+ if ($rights_object->field_to_rights_value[$field_name] !== "1") { // check if data needs to be hidden
+
+ if (($rights_object->field_to_rights_value[$field_name] === "3") && ($this->dictionary[$field_name]["identifier"] === "y")) { // remove all identifiers, and this is an identifier
+
+ $event_fields_and_vals[$field_name]["allValues"] = $this->removed_replacement;
+
+ } else if ($rights_object->field_to_rights_value[$field_name] === "2") { // de-identified rights, so remove marked identifiers, freetext and date/time fields
+
+ $event_fields_and_vals[$field_name]["allValues"] = $this->de_identified_replacement;
+
+ } else { // no rights, so remove everything
+
+ $event_fields_and_vals[$field_name]["allValues"] = $this->no_rights_replacement;
+
+ } // end else
+
+ } else { // full rights, so treat this data normally
+
+ $all_choices = explode("|", $this->dictionary[$field_name]["select_choices_or_calculations"]);
+ $all_choices = array_map(function ($v) {
+ $v = strip_tags($v);
+ $first_comma = strpos($v, ",");
+ return trim(substr($v, $first_comma + 1));
+ }, $all_choices);
+
+ foreach($all_choices as $choice)
+ {
+ if (strpos($value, $choice) !== FALSE)
+ {
+ $event_fields_and_vals[$field_name][] = $choice;
+ }
+ }
+
+ $event_fields_and_vals[$field_name]["allValues"] = implode(", ", explode(",", $value));
+
+ } // end else
+
+ } else { // non-checkbox fields, so check more thorougly
+ /*
+ * Check user's data rights
+ * Value 0 = no rights
+ * Value 1 = full rights
+ * Value 2 = de-identified rights
+ * Value 3 = remove all identifiers rights
+ * De-identified rights: All unvalidated text fields & notes will be removed, as well as any date/time fields
+ * Remove all identifiers rights: hide data from all tagged identifier fields.
+ */
+ $event_fields_and_vals[$field_name] = $value;
+
+ if ($rights_object->field_to_rights_value[$field_name] !== "1") { // check if value needs to be modified
+
+ if ($rights_object->field_to_rights_value[$field_name] === "3") { // remove identifiers right, and this is an identifier
+
+ if ($this->dictionary[$field_name]["identifier"] === "y") {
+
+ $event_fields_and_vals[$field_name] = $this->removed_replacement;
+
+ } // end if
+
+ } else if ($rights_object->field_to_rights_value[$field_name] === "2") { // de-identified rights, so remove freetext fields, marked identifiers, and date/time fields
+
+ if ((($this->dictionary[$field_name]["field_type"] === "text") && (in_array($this->dictionary[$field_name]["text_validation_type_or_show_slider_number"], $this->date_formats) ||
+ empty($this->dictionary[$field_name]["text_validation_type_or_show_slider_number"]))) // non-validated text fields, meaning free short text, but not calc fields or decimal/date/int
+ || ($this->dictionary[$field_name]["field_type"] === "notes")
+ || ($this->dictionary[$field_name]["identifier"] === "y")) {
+
+ $event_fields_and_vals[$field_name] = $this->de_identified_replacement;
+
+ } // end if
+
+ } else { // rights value of 0, so remove all data from these fields
+
+ $event_fields_and_vals[$field_name] = $this->no_rights_replacement;
+
+ } // end else
+
+ } else { // treat the data normally
+
+ if ($this->dictionary[$field_name]["field_type"] === "notes") { // make HTML entities
+
+ $event_fields_and_vals[$field_name] = str_replace("\r\n", " ", htmlentities($value));
+
+ } else { // return without modification
+
+ $event_fields_and_vals[$field_name] = $value;
+
+ } // end else
+
+ } // end else
+
+ } // end else
+
+ } // end else if
+
+ } // end foreach
+
+ return $event_fields_and_vals;
+
+ } // end function
+
+ /**
+ * Replaces given text with replacement.
+ *
+ * @access private
+ * @param String $text The text to replace.
+ * @param String $replacement The replacement text.
+ * @return String A string with the replaced text.
+ */
+ private function replaceStrings($text, $replacement)
+ {
+ preg_match_all("/'/", $text, $quotes, PREG_OFFSET_CAPTURE);
+ $quotes = $quotes[0];
+ if (sizeof($quotes) % 2 === 0)
+ {
+ $i = 0;
+ $to_replace = array();
+ while ($i < sizeof($quotes))
+ {
+ $to_replace[] = substr($text, $quotes[$i][1], $quotes[$i + 1][1] - $quotes[$i][1] + 1);
+ $i = $i + 2;
+ }
+
+ $text = str_replace($to_replace, $replacement, $text);
+ }
+ return $text;
+ }
+
+ /**
+ * Parses a syntax string into blocks.
+ *
+ * @access private
+ * @param String $syntax The syntax to parse.
+ * @return Array An array of blocks that make up the syntax passed.
+ */
+ private function getSyntaxParts($syntax)
+ {
+ $syntax = str_replace(array("['", "']"), array("[", "]"), $syntax);
+ $syntax = $this->replaceStrings(trim($syntax), "''"); //Replace strings with ''
+
+ $parts = array();
+ $previous = array();
+
+ $i = 0;
+ while($i < strlen($syntax))
+ {
+ $char = $syntax[$i];
+ switch($char)
+ {
+ case ",":
+ case "(":
+ case ")":
+ case "]":
+ $part = trim(implode("", $previous));
+ $previous = array();
+ if ($part !== "")
+ {
+ $parts[] = $part;
+ }
+ $parts[] = $char;
+ $i++;
+ break;
+ case "[":
+ if ($syntax[$i-1] == " ")
+ {
+ $parts[] = " ";
+ }
+ $part = trim(implode("", $previous));
+ if ($part !== "")
+ {
+ $parts[] = $part;
+ }
+ $parts[] = $char;
+ $previous = array();
+ $i++;
+ break;
+ case " ":
+ $part = trim(implode("", $previous));
+ $previous = array();
+ if ($part !== "")
+ {
+ $parts[] = $part;
+ }
+ $i++;
+ break;
+ default:
+ $previous[] = $char;
+ if ($i == strlen($syntax) - 1)
+ {
+ $part = trim(implode("", $previous));
+ if ($part !== "")
+ {
+ $parts[] = $part;
+ }
+ }
+ $i++;
+ break;
+ }
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Checks whether fields and events exist within project
+ *
+ * @access private
+ * @param String $var The field name/event name to check
+ * @return Bool True if $var is a valid REDCap field or event on the project, false otherwise
+ */
+ private function isValidFieldOrEvent($var)
+ {
+ $var = trim($var, "'");
+
+ $events = REDCap::getEventNames(true, true); // If there are no events (the project is classical), the method will return false
+
+ /**
+ * Get REDCap completion fields
+ */
+ $external_fields = array();
+ foreach ($this->instruments as $unique_name => $label)
+ {
+ $external_fields[] = "{$unique_name}_complete";
+ $external_fields[] = "{$unique_name}_timestamp";
+ }
+
+ if ($var !== "allValues" && !in_array($var, $external_fields))
+ {
+ $dictionary = $this->dictionary[$var];
+ if (($events === FALSE && empty($dictionary)) ||
+ ($events !== FALSE && !in_array($var, $events) && empty($dictionary)))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether fields and events are being queried correctly.
+ *
+ * @access private
+ * @param String $text The line of text to validate.
+ * @param Integer $line_num The current line number in the template.
+ * @return Array An array of errors, with the line number appended to indicate where it occured.
+ */
+ private function validateFieldQueries($text, $line_num)
+ {
+ $errors = array();
+
+ // Get all occurences of an opening square bracket "["
+ preg_match_all('/\[/', $text, $opening_brackets, PREG_OFFSET_CAPTURE);
+
+ if (!empty($opening_brackets[0]))
+ {
+ // Get the event/field name between each opening and closing bracket, and check if it exists
+ foreach($opening_brackets[0] as $bracket)
+ {
+ $start_pos = $bracket[1]+1;
+
+ $closing_bracket = strpos($text, "]", $start_pos);
+ if ($closing_bracket !== FALSE)
+ {
+ $var = substr($text, $start_pos, $closing_bracket - $start_pos);
+ if (substr($var, 0, 1) !== "'" && substr($var, strlen($var)-1, 1) !== "'")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] '$var' must be enclosed with single quotes.";
+ }
+
+ $var = trim($var, "'\"");
+
+ if ($var !== "allValues")
+ {
+ $dictionary = $this->dictionary[$var];
+ if (!empty($dictionary))
+ {
+ if ($dictionary["field_type"] === "checkbox")
+ {
+ // Check if checkbox is queried via in_array()
+ $in_array = false;
+ $in_array_start_pos = 0;
+ while (($in_array_start_pos = strpos($text, "in_array('", $in_array_start_pos)) !== FALSE)
+ {
+ $end_of_check_value = strpos($text, "', \$redcap", $in_array_start_pos+1);
+ $in_array_end_pos = strpos($text, ")", $end_of_check_value+1);
+ if (($in_array_start_pos < $start_pos) && ($in_array_end_pos > $start_pos))
+ {
+ $in_array = true;
+ break;
+ }
+ $in_array_start_pos = $in_array_start_pos + 1;
+ }
+
+ // check if checkbox is queried via ['allValues']
+ $all_values_str = substr($text, $closing_bracket+1, 13);
+ if ($all_values_str !== "['allValues']" && !$in_array)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] '$var' is a checkbox and can only be queried using in_array or \$redcap['$var']['allValues']";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return $errors;
+ }
+
+ /**
+ * Validate syntax.
+ *
+ * @access private
+ * @see Template::validateFieldQueries() For checking whether fields and events are queried correctly.
+ * @see Template::getSyntaxParts() For retreiving blocks of syntax from the given syntax string.
+ * @param String $syntax The syntax to validate.
+ * @param Integer $line_num The current line number in the template.
+ * @return Array An array of errors, with the line number appended to indicate where it occured.
+ */
+ private function validateSyntax($syntax, $line_num)
+ {
+ if (!empty($syntax))
+ {
+ $errors = $this->validateFieldQueries($syntax, $line_num);
+
+ if ((sizeof(explode("'", $syntax)) - 1) % 2 > 0)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Odd number of single quotes exist. You've either added an extra quote, forgotten to close one, or forgotten to escape one.";
+ }
+ else if ($syntax != strip_tags($syntax))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Report logic cannot have any HTML between {}";
+ }
+ else if (preg_match("/{/", $syntax) !== 0 || preg_match("/}/", $syntax) !== 0)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] There is either a '{' or '}' within {}. They are special characters and can only denote the beginning and end of syntax.";
+ }
+
+ // Check symmetry of ()
+ if ($parts == null) { $parts = array();} // PHP8 compatability patch Dan Evans, 2023-06-09
+ if (sizeof(array_keys($parts, "(")) != sizeof(array_keys($parts, ")")))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Odd number of parenthesis (. You've either added an extra parenthesis, or forgot to close one.";
+ }
+
+ // Check symmetry of []
+ if (sizeof(array_keys($parts, "[")) != sizeof(array_keys($parts, "]")))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Odd number of square brackets [. You've either added an extra bracket, or forgot to close one.";
+ }
+
+ $parts = $this->getSyntaxParts($syntax);
+
+ foreach($parts as $index => $part)
+ {
+ switch ($part) {
+ case "if":
+ case "elseif":
+ // Must have either a ( or ) or $redcap or $showLabelAndRow or in_array after
+ if ($index != sizeof($parts) - 1)
+ {
+ if ($index !== 0)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed $part condition. $part clause must be first part of syntax.";
+ }
+ else
+ {
+ $next_part = $parts[$index + 1];
+
+ if (($next_part !== "("
+ && ($next_part != "''" && $previous == "in_array")
+ && $next_part !== ")"
+ && $next_part !== "\$redcap"
+ && $next_part !== "\$showLabelAndRow"
+ && $next_part !== "in_array"
+ && $next_part !== "''"))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
+ }
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed $part condition. You cannot have an empty $part clause.";
+ }
+ break;
+ case "(":
+ $previous = $parts[$index - 1];
+ $next_part = $parts[$index + 1];
+
+ if (($next_part !== "("
+ && ($next_part != "''" && $previous == "in_array")
+ && $next_part !== ")"
+ && $next_part !== "\$redcap"
+ && $next_part !== "\$showLabelAndRow"
+ && $next_part !== "in_array"
+ && $next_part !== "''"))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after (.";
+ }
+ else if ($next_part == "(" && $previous == "in_array")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Malformed in_array() function.";
+ }
+ break;
+ case ")":
+ // Must have either a ) or logical operator after, if not the last part of syntax
+ if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if ($next_part !== ")" && !in_array($next_part, $this->logical_operators))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after ).";
+ }
+ }
+ break;
+ case "eq":
+ case "ne":
+ case "neq":
+ case "gt":
+ case "ge":
+ case "gte":
+ case "lt":
+ case "le":
+ case "lte":
+ // Must have either a ( or $redcap or $showLabelAndRow or in_array or string or not after
+ // If there's another logical operator two spaces before, is illegal.
+ if ($index == 0)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a comparison operator $part as the first part in syntax.";
+ }
+ else if ($index != sizeof($parts) - 1)
+ {
+ $previous = $parts[$index - 2];
+ $next_part = $parts[$index + 1];
+
+ if (in_array($previous, $this->logical_operators) && $previous !== "or" && $previous !== "and")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $part. You cannot chain comparison operators together, you must use an and or an or";
+ }
+ if (!empty($next_part)
+ && $next_part !== "("
+ && $next_part !== "\$redcap"
+ && $next_part !== "\$showLabelAndRow"
+ && $next_part !== "''"
+ && $next_part !== "in_array"
+ && $next_part !== "not"
+ && !is_numeric($next_part))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a comparison operator $part as the last part in syntax.";
+ }
+ break;
+ case "not":
+ case "or":
+ case "and":
+ // Must have either a ( or $redcap or $showLabelAndRow or in_array or string or not after
+ if ($index == 0)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a logical operator $part as the first part in syntax.";
+ }
+ else if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if (!empty($next_part)
+ && $next_part !== "("
+ && $next_part !== "\$redcap"
+ && $next_part !== "\$showLabelAndRow"
+ && $next_part !== "''"
+ && $next_part !== "in_array"
+ && $next_part !== "not"
+ && !is_numeric($next_part))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have a logical operator $part as the last part in syntax.";
+ }
+ break;
+ case "''":
+ // Must have either a logical operator or ) or , or ]
+ if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if ($next_part !== ")"
+ && $next_part != ","
+ && $next_part != "]"
+ && !in_array($next_part, $this->logical_operators))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after string value within ''.";
+ }
+ }
+ break;
+ case "\$redcap":
+ // Must have a [
+ if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if ($next_part !== "[")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid \$redcap field query.";
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid \$redcap field query at end of syntax.";
+ }
+ break;
+ case "[":
+ // Must have a ''
+ if ($index != sizeof($parts) - 1)
+ {
+ $previous = $parts[$index - 1];
+ $next_part = $parts[$index + 1];
+
+ if ($previous !== "\$redcap" && $previous !== "]")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Each field and events query must be preceeded by \$redcap";
+ }
+
+ if ($next_part == "]")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have empty [] brackets";
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Cannot have opening [ as end of syntax.";
+ }
+ break;
+ case "\$showLabelAndRow":
+ // Must have either a logical operator or ) after, if not last item in syntax
+ if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if ($next_part !== ")"
+ && !in_array($next_part, $this->logical_operators))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
+ }
+ }
+ break;
+ case "]":
+ // Must have either a logical operator or ) or [ after, if not last item in syntax
+ if ($index != sizeof($parts) - 1)
+ {
+ $previous_2 = $parts[$index - 2];
+ $next_part = $parts[$index + 1];
+
+ if ($previous_2 !== "[")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Unclosed or empty ] bracket.";
+ }
+
+ if ($next_part !== ")"
+ && $next_part !== "["
+ && !in_array($next_part, $this->logical_operators))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid '$next_part' after $part.";
+ }
+ }
+ break;
+ case ",":
+ // Must have a $redcap field query after, has to be part of in_array function call
+ if ($index != sizeof($parts) - 1)
+ {
+ $previous = $parts[$index - 1];
+ $previous_2 = $parts[$index - 2];
+ $previous_3 = $parts[$index - 3];
+
+ if ($previous_3 !== "in_array" || $previous_2 !== "(" || $previous !== "''")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Improper use of , in syntax. Are you trying to call in_array()?";
+ }
+
+ $next_part = $parts[$index + 1];
+ if ($next_part !== "\$redcap")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num]. Invalid $next_part after ,.";
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] $part cannot be end of syntax";
+ }
+ break;
+ case "in_array":
+ // Must have a ( after
+ if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if ($next_part !== "(")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Malformed in_array() function.";
+ }
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Malformed in_array() function.";
+ }
+ break;
+ case "else":
+ case "/if":
+ // Must be the only clause in syntax
+ if ($index != sizeof($parts) - 1)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed $part clause. Must be of form {{$part}}.";
+ }
+ break;
+ default:
+ // If it's a number, must have ) or logical operator after, if not last item in syntax
+ if (is_numeric($part))
+ {
+ if ($index != sizeof($parts) - 1)
+ {
+ $next_part = $parts[$index + 1];
+ if (!empty($next_part) && $next_part !== ")" && !in_array($next_part, $this->logical_operators))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Invalid $next_part after $part.";
+ }
+ }
+ }
+ // Check if it's a string
+ else if (!empty($part) &&
+ $part[0] != "'" &&
+ $part[0] != "\"" &&
+ $part[strlen($part) - 1] != "'" &&
+ $part[strlen($part) - 1] != "\"" &&
+ !$this->isValidFieldOrEvent($part))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] $part is not a valid event/field/syntax in this project";
+ }
+ break;
+ }
+ }
+ }
+ return $errors;
+ }
+
+ /**
+ * Validate if statements, by checking the following:
+ *
+ * - If statements have matching opening and closing statements.
+ * - Elseifs are associated with an if statement.
+ * - Every if statement has at most one else clause.
+ *
+ * @access private
+ * @param Array $lines An array of lines within template.
+ * @return Array An array of errors, with their line numbers appended to indicate where it occured.
+ */
+ private function validateIfStatements($lines)
+ {
+ $errors = array();
+
+ // Find all occurences of opening {if, {/if}
+ $opening_ifs = array();
+ $closing_ifs = array();
+
+ foreach($lines as $index => $line)
+ {
+ // Could be multiple statements on same line
+ $last_pos = 0;
+ while(($last_pos = strpos($line, '{if', $last_pos)) !== FALSE)
+ {
+ $line_num = $index + 1;
+ if ($line[$last_pos + 3] !== " ")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed if/elseif statement. Need a space after 'if'";
+ }
+
+ $opening_ifs[] = array(
+ "line_num" => $line_num,
+ "line_pos" => $last_pos
+ );
+ $last_pos++;
+ }
+
+ $last_pos = 0;
+ while(($last_pos = strpos($line, '{/if}', $last_pos)) !== FALSE)
+ {
+ $closing_ifs[] = array(
+ "line_num" => $index + 1,
+ "line_pos" => $last_pos
+ );
+ $last_pos++;
+ }
+ }
+
+ $num_opening_ifs = sizeof($opening_ifs);
+ $num_closing_ifs = sizeof($closing_ifs);
+
+ $opening_ifs_copy = array_reverse($opening_ifs);
+ $closing_ifs_copy = array_reverse($closing_ifs);
+
+ foreach($opening_ifs_copy as $index => $opening_if)
+ {
+ $key = null;
+ for ($i = 0; $i < sizeof($closing_ifs_copy); $i++)
+ {
+ $closing_if = $closing_ifs_copy[$i];
+
+ if (($opening_if["line_num"] < $closing_if["line_num"]) ||
+ ($opening_if["line_num"] == $closing_if["line_num"] && $opening_if["line_pos"] < $closing_if["line_pos"]))
+ {
+ $key = $i;
+ break;
+ }
+ }
+
+ if (!is_null($key))
+ {
+ unset($closing_ifs_copy[$key]);
+ $closing_ifs_copy = array_values($closing_ifs_copy);
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [" . $opening_if["line_num"] . "] Missing {/if}";
+ }
+ }
+
+ foreach($closing_ifs_copy as $closing_if)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [" . $closing_if["line_num"] . "] Extra {/if}";
+ }
+
+ if (empty($errors))
+ {
+ // Find all occurences of {elseif, {else}
+ $elseifs = array();
+ $elses = array();
+
+ foreach($lines as $index => $line)
+ {
+ $last_pos = 0;
+ while(($last_pos = strpos($line, '{elseif', $last_pos)) !== FALSE)
+ {
+ $line_num = $index + 1;
+ if ($line[$last_pos + 7] !== " ")
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Mal-formed if/elseif statement. Need a space after 'elseif'";
+ }
+
+ $elseifs[] = array(
+ "line_num" => $line_num,
+ "line_pos" => $last_pos
+ );
+ $last_pos++;
+ }
+
+ $last_pos = 0;
+ while(($last_pos = strpos($line, '{else}', $last_pos)) !== FALSE)
+ {
+ $elses[] = array(
+ "line_num" => $index + 1,
+ "line_pos" => $last_pos
+ );
+ $last_pos++;
+ }
+ }
+
+ $num_elses = sizeof($elses);
+ $num_elseifs = sizeof($elseifs);
+
+ // Check that every elseif clause is associated with an if statement.
+ foreach($elseifs as $elseif)
+ {
+ $line_num = $elseif["line_num"];
+ $line_pos = $elseif["line_pos"];
+
+ if ($num_opening_ifs == 0)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Every {elseif} clause must be associated with an {if} statement";
+ }
+ else
+ {
+ for ($i = 0; $i < $num_opening_ifs; $i++)
+ {
+ $opening_if = $opening_ifs[$i];
+ $closing_if = $closing_ifs[$i];
+
+ // Check if there's an else clause between the elseif and the opening if
+ $arr = array_filter($elses, function($v) use ($opening_if, $closing_if, $line_num, $line_pos) {
+ $v_line_num = $v["line_num"];
+ $v_line_pos = $v["line_pos"];
+
+ if ($opening_if["line_num"] < $v_line_num && $v_line_num < $closing_if["line_num"] && $v_line_num < $line_num)
+ {
+ return true;
+ }
+ else if (($opening_if["line_num"] == $v_line_num && $v_line_num == $closing_if["line_num"] && $v_line_num == $line_num) ||
+ ($opening_if["line_num"] == $v_line_num && $v_line_num < $closing_if["line_num"] && $v_line_num == $line_num) ||
+ ($opening_if["line_num"] < $v_line_num && $v_line_num == $closing_if["line_num"] && $v_line_num == $line_num) ||
+ ($opening_if["line_num"] < $v_line_num && $v_line_num < $closing_if["line_num"] && $v_line_num == $line_num))
+ {
+ return $v_line_pos < $line_pos;
+ }
+ });
+
+ if (($opening_if["line_num"] < $line_num && $line_num < $closing_if["line_num"] && empty($arr)) ||
+ ($opening_if["line_num"] == $line_num && $line_num == $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos && $line_pos < $closing_if["line_pos"] && empty($arr)) ||
+ ($opening_if["line_num"] == $line_num && $line_num < $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos && empty($arr)) ||
+ ($opening_if["line_num"] < $line_num && $line_num == $closing_if["line_num"] && $line_pos < $closing_if["line_pos"] && empty($arr)))
+ {
+ break;
+ }
+ else if ($i == $num_opening_ifs - 1)
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Every {elseif} clause must be associated with an {if} statement";
+ }
+ }
+ }
+ }
+
+ // Check that every if statement has at most one else clause, and that every else clause is associated with an if statement.
+ foreach($elses as $index => $else)
+ {
+ $key = null;
+
+ $line_num = $else["line_num"];
+ $line_pos = $else["line_pos"];
+
+ for ($i = 0; $i < sizeof($opening_ifs); $i++)
+ {
+ $opening_if = $opening_ifs[$i];
+ $closing_if = $closing_ifs[$i];
+
+ if (($opening_if["line_num"] < $line_num && $line_num < $closing_if["line_num"]) ||
+ ($opening_if["line_num"] == $line_num && $line_num == $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos && $line_pos < $closing_if["line_pos"]) ||
+ ($opening_if["line_num"] == $line_num && $line_num < $closing_if["line_num"] && $opening_if["line_pos"] < $line_pos) ||
+ ($opening_if["line_num"] < $line_num && $line_num == $closing_if["line_num"] && $line_pos < $closing_if["line_pos"]))
+ {
+ $key = $i;
+ break;
+ }
+ }
+
+ if (!is_null($key))
+ {
+ unset($opening_ifs[$i]);
+ unset($closing_ifs[$i]);
+
+ $opening_ifs = array_values($opening_ifs);
+ $closing_ifs = array_values($closing_ifs);
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Template either has more than one {else} clause within an {if} statement, or an {else} clause outside of an {if} statement.";
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Performs template validation.
+ *
+ * Checks that there are no unclosed curly brackets within the template, as they're special characters that denote
+ * Smarty syntax. If validation passes, then retrieve all text enclosed within curly brackets, and validate them.
+ *
+ * @see Template::validateSyntax() For validating all syntax, escept if statements.
+ * @see Template::validateIfStatements() For validating if statements.
+ * @param String $template_data Template contents.
+ * @return Array An array of errors, with their line numbers appended to indicate where it occured.
+ */
+ public function validateTemplate($template_data)
+ {
+ // Decode all HTML entities, and replace escaped quotes with a backtick.
+ $template_data = preg_replace("/\\\\'/", "`", html_entity_decode($template_data, ENT_HTML5 | ENT_QUOTES));
+
+ $errors = array();
+
+ $lines = explode("\n", $template_data);
+
+ foreach($lines as $index => $line)
+ {
+ $line_num = $index + 1;
+
+ // Find all occurences of opening { and closing }
+ preg_match_all('/{/', $line, $opening_brackets, PREG_OFFSET_CAPTURE);
+ preg_match_all('/}/', $line, $closing_brackets, PREG_OFFSET_CAPTURE);
+
+ if (sizeof($opening_brackets[0]) != sizeof($closing_brackets[0]))
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Has uneven number of '{' and '}' brackets. Please check there are no extra opening or unclosed brackets.";
+ }
+ else
+ {
+ // Get the smarty syntax between each pair of {}
+ foreach($opening_brackets[0] as $bracket)
+ {
+ $start_pos = $bracket[1]+1;
+ $closing_bracket = strpos($line, "}", $start_pos);
+ if ($closing_bracket !== FALSE)
+ {
+ $text = substr($line, $start_pos, $closing_bracket - $start_pos);
+ $errors = array_merge($errors, $this->validateSyntax($text, $line_num));
+ }
+ else
+ {
+ $errors[] = "ERROR [EDITOR] LINE [$line_num] Is missing a closing '}'.";
+ }
+ }
+ }
+ }
+
+ return array_merge($errors, $this->validateIfStatements($lines));
+ }
+
+ /**
+ * Fill a given template with REDCap record data.
+ *
+ * Retrieves REDCap data, and parses it into an array used by Smarty to fill the
+ * template with data. After using Smarty to fill the template, empty nodes are
+ * found and deleted.
+ *
+ * @see Template::parseEventData() For parsing event data into Array used in Smarty.
+ * @see Template::getEmptyNodes() For retrieving empty nodes in HTML.
+ * @param String $template_name The Template name.
+ * @param Integer $record A REDCap record ID.
+ * @return String The template filled with REDCap record data.
+ */
+ public function fillTemplate($template_name, $record)
+ {
+ $filled_template = "";
+
+ $user = strtolower(USERID);
+ //$rights = REDCap::getUserRights($user);
+
+ $template = REDCap::getData("json", $record, null, null, null, TRUE, FALSE, TRUE, null, TRUE);
+
+ $json = json_decode($template, true);
+
+ $repeatable_instruments_parsed = array();
+
+ if (REDCap::isLongitudinal())
+ {
+ $this->redcap = array();
+
+ $event_ids = array_values(REDCap::getEventNames(TRUE));
+ $event_labels = array_values(REDCap::getEventNames(FALSE, TRUE));
+
+ $events = array();
+ for ($i = 0 ; $i <= count($event_labels)-1; $i++)
+ {
+ $events[$event_labels[$i]] = $event_ids[$i];
+ }
+
+ $first_event = $event_ids[0];
+
+ foreach($json as $index => $event_data)
+ {
+ $data = array();
+ $event = $events[$event_data["redcap_event_name"]];
+ if (!empty($event_data["redcap_repeat_instance"]))
+ {
+ // Repeatable instrument
+ if (!empty($event_data["redcap_repeat_instrument"]) && !in_array($event_data["redcap_repeat_instrument"], $repeatable_instruments_parsed))
+ {
+ // Get latest instance of repeatable instrument.
+ // Retrieve all repeatable instances of event.
+ $repeatable_instrument_instances = array_filter($json, function($value) use($event_data){
+ return $value["redcap_repeat_instrument"] == $event_data["redcap_repeat_instrument"];
+ });
+ $repeatable_instrument_instances = array_values($repeatable_instrument_instances);
+ // Get the latest instance and parse it.
+ $repeat_instances = array_column($repeatable_instrument_instances, "redcap_repeat_instance");
+ $latest_instance = max($repeat_instances);
+ $key = array_search($latest_instance, $repeat_instances);
+
+ if (empty($this->redcap[$event]))
+ {
+ $data = $this->parseEventData($repeatable_instrument_instances[$key]);
+ }
+ else
+ {
+ // Merges repeatable instrument data with non-repeatable instrument data in same event.
+ $data = $this->redcap[$event];
+ $repeatable_instrument_data = $this->parseEventData($repeatable_instrument_instances[$key]);
+
+ foreach($repeatable_instrument_data as $field => $value)
+ {
+ if (empty($data[$field]))
+ $data[$field] = $value;
+ }
+ }
+
+ $repeatable_instruments_parsed[] = $event_data["redcap_repeat_instrument"];
+ }
+ // Repeatable event
+ else if (empty($this->redcap[$event]))
+ {
+ // Retrieve all repeatable instances of event.
+ $repeatable_event_instances = array_filter($json, function($value) use($event_data){
+ return $value["redcap_event_name"] == $event_data["redcap_event_name"];
+ });
+ $repeatable_event_instances = array_values($repeatable_event_instances);
+
+ // Get the latest instance and parse it.
+ $repeat_instances = array_column($repeatable_event_instances, "redcap_repeat_instance");
+ $latest_instance = max($repeat_instances);
+ $key = array_search($latest_instance, $repeat_instances);
+ $data = $this->parseEventData($repeatable_event_instances[$key]);
+ }
+ }
+ else
+ {
+ if (empty($this->redcap[$event]))
+ {
+ $data = $this->parseEventData($event_data);
+ }
+ else
+ {
+ $data = array_merge($this->redcap[$event], $this->parseEventData($event_data));
+ }
+ }
+
+ if (!empty($data))
+ {
+ if ($first_event == $event)
+ {
+ $this->redcap = array_merge($this->redcap, $data);
+ }
+ $this->redcap[$event] = $data;
+ }
+ }
+ }
+ else
+ {
+ foreach($json as $index => $event_data)
+ {
+ // Repeatable instrument
+ if (!empty($event_data["redcap_repeat_instance"]))
+ {
+ // Repeatable instrument
+ if (!empty($event_data["redcap_repeat_instrument"]) && !in_array($event_data["redcap_repeat_instrument"], $repeatable_instruments_parsed))
+ {
+ // Get latest instance of repeatable instrument.
+ // Retrieve all repeatable instances of event.
+ $repeatable_instrument_instances = array_filter($json, function($value) use($event_data){
+ return $value["redcap_repeat_instrument"] == $event_data["redcap_repeat_instrument"];
+ });
+ $repeatable_instrument_instances = array_values($repeatable_instrument_instances);
+
+ // Get the latest instance and parse it.
+ $repeat_instances = array_column($repeatable_instrument_instances, "redcap_repeat_instance");
+ $latest_instance = max($repeat_instances);
+ $key = array_search($latest_instance, $repeat_instances);
+
+ // Merges repeatable instrument data with non-repeatable instrument data in same event.
+ $repeatable_instrument_data = $this->parseEventData($repeatable_instrument_instances[$key]);
+ foreach($repeatable_instrument_data as $field => $value)
+ {
+ if (empty($this->redcap[$field]))
+ $this->redcap[$field] = $value;
+ }
+
+ $repeatable_instruments_parsed[] = $event_data["redcap_repeat_instrument"];
+ }
+ }
+ else
+ {
+ $this->redcap = $this->parseEventData($event_data);
+ // parseEventData() is stripping out the comments field!
+ }
+ }
+ }
+
+ try
+ {
+ $this->smarty->assign("redcap", $this->redcap);
+ $filled_template = $this->smarty->fetch($template_name);
+
+ // Remove empty nodes
+ $doc = new DOMDocument();
+ $doc->loadHTML($filled_template);
+ $body = $doc->getElementsByTagName("body")->item(0);
+
+ $empty_elems = $this->getEmptyNodes($body);
+ while (!empty($empty_elems))
+ {
+ foreach($empty_elems as $elem)
+ {
+ $elem->parentNode->removeChild($elem);
+ }
+ $empty_elems = $this->getEmptyNodes($body); // There may be new empty nodes after the previous ones were removed.
+ }
+ $filled_template = $doc->saveHTML();
+ }
+ catch (Exception $e)
+ {
+ throw new Exception("Error on line " . $e->getLine() . ": " . $e->getMessage());
+ }
+
+ return $filled_template;
+ }
+}
diff --git a/UploadImages.php b/UploadImages.php
index b126d2a..0ec972b 100644
--- a/UploadImages.php
+++ b/UploadImages.php
@@ -1,7 +1,7 @@
-uploadImages();
\ No newline at end of file
diff --git a/config.json b/config.json
index 0ea58ef..61d1fa7 100644
--- a/config.json
+++ b/config.json
@@ -67,7 +67,34 @@
"type": "checkbox",
"repeatable": false,
"super-users-only": true
+ },
+ {
+ "key": "template-options",
+ "name": "Template configuration options",
+ "required": false,
+ "type": "sub_settings",
+ "repeatable": true,
+ "sub_settings": [
+ {
+ "key": "template-name",
+ "name": "Template name",
+ "required": true,
+ "type": "text"
+ },
+ {
+ "key": "option-paper-size",
+ "name": "Paper size (Optional - specify if different to Letter e.g. A4)",
+ "required": false,
+ "type": "text"
+ },
+ {
+ "key": "option-paper-orientation",
+ "name": "Use landscape orientation instead of portrait?",
+ "required": false,
+ "type": "checkbox"
+ }
+ ]
}
],
"framework-version": 9
-}
+}
\ No newline at end of file
diff --git a/index.php b/index.php
index 250a5a0..c1f781d 100644
--- a/index.php
+++ b/index.php
@@ -1,17 +1,17 @@
-generateIndexPage();
-
-/**
- * Include REDCap footer.
- */
+generateIndexPage();
+
+/**
+ * Include REDCap footer.
+ */
require_once APP_PATH_DOCROOT . "ProjectGeneral/footer.php";
\ No newline at end of file
|