2nd pass at importer for dokuwiki's into yioop

Chris Pollett [2024-06-19 16:Jun:th]
2nd pass at importer for dokuwiki's into yioop
Filename
src/configs/GroupWikiTool.php
src/configs/TokenTool.php
src/controllers/Controller.php
src/controllers/SearchController.php
src/controllers/StaticController.php
src/controllers/components/CrawlComponent.php
src/controllers/components/SocialComponent.php
src/library/VersionManager.php
src/library/WikiParser.php
src/models/GroupModel.php
diff --git a/src/configs/GroupWikiTool.php b/src/configs/GroupWikiTool.php
index 7e102cb05..cf783bf04 100644
--- a/src/configs/GroupWikiTool.php
+++ b/src/configs/GroupWikiTool.php
@@ -122,11 +122,15 @@ switch ($argv[1]) {
             echo $usage;
             exit();
         }
-        list(,, $type, $group_name, $locale_tag, $wiki_path,) = $argv;
+        if (empty($argv[6])) {
+            $argv[6] = "";
+        }
+        list(,, $type, $group_name, $locale_tag, $wiki_path,
+            $from_base_url) = $argv;
         if ($type != "dokuwiki") {
             echo "Only importing from Dokuwiki's currently supported!\n";
         }
-        importWiki($type, $group_name, $locale_tag, $wiki_path);
+        importWiki($type, $group_name, $locale_tag, $wiki_path, $from_base_url);
         break;
     case "path":
         if (empty($argv[4])) {
@@ -179,7 +183,8 @@ switch ($argv[1]) {
 /**
  *
  */
-function importWiki($type, $group_name, $locale_tag, $wiki_path)
+function importWiki($type, $group_name, $locale_tag, $wiki_path,
+    $from_base_url= "")
 {
     if ($type != 'dokuwiki') {
         echo "$type is an unknown wiki type\n";
@@ -207,18 +212,23 @@ function importWiki($type, $group_name, $locale_tag, $wiki_path)
         ["/(\A|\s)\_\_([$class_or_id]+)\_\_/su", '$1<u>$2<u>'],
         ["/(\A|\n)\s*\-/su", "$1#"],
         ["/(\A|\n)\s*\*/u", "$1*"],
-        ["/\[([^\|]+)\s+\|([^\]]+)\]/su", "[$1|$2]"],
+        ["/\[(\s*[^\|]+)\s+\|([^\]]+)\]/su", "[$1|$2]"],
         ["/\[([^\|]+)\|\s+([^\]]+)\]/su", "[$1|$2]"],
-        ["/\{\{\:?docs\:([^\|]+)\|([^\}\{]+)\}\}/su",
-            '((resource:$1|$2))'],
-        ["/\/\/$/u", "<br>\n"],
+        ["/\{\{\s*\:?([a-zA-z]*)(\:)([^\|]+)\|([^\}\{]+)\}\}/su",
+            '((resource-link:media:$3|$1|$4))'],
+        ['/\/\/$/', "<br>\n"],
+        ['/(\{|\[)\s+/', "$1"],
+        ['/\s+(\}|\])/', "$1"],
+        ['/(\A|\n)\s(\{|\[)/s', "$1$2"],
+        ['/(\A|\n)(\*+)\s+/s', "$1$2"],
+        ['/(\A|\n)(\#+)\s+/s', "$1$2"],
     ];
     $internal_to_yioops = [
-        ["/ZZH1ZZ\s*([$class_or_id]+)\s*ZZH1ZZ/su", '=$1='],
-        ["/ZZH2ZZ\s*([$class_or_id]+)\s*ZZH2ZZ/su", '==$1=='],
-        ["/ZZH3ZZ\s*([$class_or_id]+)\s*ZZH3ZZ/su", '===$1==='],
-        ["/ZZH4ZZ\s*([$class_or_id]+)\s*ZZH4ZZ/su", '====$1===='],
-        ["/ZZH5ZZ\s*([$class_or_id]+)\s*ZZH5ZZ/su", '=====$1====='],
+        ["/ZZH1ZZ\s*([$class_or_id]+)\s*ZZH1ZZ\s*/su", '\n=$1=\n'],
+        ["/ZZH2ZZ\s*([$class_or_id]+)\s*ZZH2ZZ\s*/su", '\n==$1==\n'],
+        ["/ZZH3ZZ\s*([$class_or_id]+)\s*ZZH3ZZ\s*/su", '\n===$1===\n'],
+        ["/ZZH4ZZ\s*([$class_or_id]+)\s*ZZH4ZZ\s*/su", '\n====$1====\n'],
+        ["/ZZH5ZZ\s*([$class_or_id]+)\s*ZZH5ZZ\s*/su", '\n=====$1=====\n'],
     ];
     $doku_matches = [];
     $doku_replaces = [];
@@ -231,33 +241,75 @@ function importWiki($type, $group_name, $locale_tag, $wiki_path)
         list($internal_matches[], $internal_replaces[]) = $internal_to_yioop;
     }
     $documents = glob("$wiki_path/attic/*.txt.gz");
-    $has_main_page = false;
-    foreach ($documents as $pre_doc) {
-        if (preg_match('/\/Main\.([^\/\.]+)\.txt\.gz$/', $pre_doc)) {
-            $has_main_page = true;
-            break;
-        }
-    }
+    importWikiMedia(C\ROOT_ID, $group_id, $locale_tag, $wiki_path);
+    $first_start = true;
+    $old_original_page_name = "";
+    $meta_info = [];
     foreach ($documents as $pre_doc) {
         $document = gzdecode(file_get_contents($pre_doc));
         $document = preg_replace($doku_matches, $doku_replaces, $document);
         $document = preg_replace($internal_matches, $internal_replaces,
             $document);
+        if (!empty($from_base_url)) {
+            $quoted_url = preg_quote($from_base_url);
+            $document = preg_replace(
+                "@\[\[$quoted_url([^\:\|\]]+)id=([^\|\]]+)@", "[[$2",
+                $document);
+            $document = preg_replace("@\[\[$quoted_url([^\:\|\]]+)fetch\.php\?".
+                "media\=([a-zA-Z]+)\:([^\:\|\]]+)\:([^\|\]]+)\|([^\]]+)\]\]@",
+                '((resource-link:media:$4|$2/$3|$5))',
+                $document);
+        }
         $document = str_replace('\\n',"\n", $document);
-        $document = str_replace('\\\'',"'", $document);
+        $document = str_replace("'''","&#039;&#039;&#039;", $document);
+        $document = str_replace("''","&#039;&#039;", $document);
         if (preg_match('/\/([^\/]+)\.([^\/\.]+)\.txt\.gz$/', $pre_doc,
             $matches)) {
-            list(, $page_name, $timestamp) = $matches;
-            if ($page_name == 'start' && !$has_main_page) {
+            list(, $original_page_name, $timestamp) = $matches;
+            if ($original_page_name != $old_original_page_name) {
+                $meta_path = "$wiki_path/meta/$original_page_name.changes";
+                if (file_exists($meta_path)) {
+                    $pre_meta_info = file($meta_path);
+                    foreach ($pre_meta_info as $pre_meta_item) {
+                        list($meta_timestamp, $meta_edit_reason) =
+                            explode("\t", $pre_meta_item, 2);
+                        $meta_edit_reason = " Dokuwiki import " .
+                            preg_replace("/\s+/", " ", trim($meta_edit_reason));
+                        $meta_info[trim($meta_timestamp)] = $meta_edit_reason;
+                    }
+                }
+                $old_original_page_name = $original_page_name;
+            }
+            $page_name = preg_replace('/^Main(\d*)$/', "DokuWikiMain$1",
+                $original_page_name);
+            $page_name = str_replace(" ", "_", $page_name);
+            if ($page_name == 'start') {
                 $page_name = 'Main';
+                if ($first_start) {
+                    $first_start = false;
+                    /* Main page is created by CreateDB with a too new timestamp
+                       as compared to some doku wiki pages might be improting
+                     */
+                    $page_id = $group_model->getPageId($group_id, $page_name,
+                        $locale_tag);
+                    if (intval($page_id) > 0) {
+                        $db = $group_model->db;
+                        $db->execute("DELETE FROM GROUP_PAGE
+                            WHERE ID=$page_id");
+                        $db->execute("DELETE FROM GROUP_PAGE_HISTORY
+                            WHERE PAGE_ID=$page_id");
+                    }
+                }
             }
+            $edit_reason = (empty($meta_info[$timestamp])) ?
+                "[Wiki Import on " . time(). " ]" : $meta_info[$timestamp];
             $timestamp = intval($timestamp);
             if (!empty($timestamp) && !empty($page_name)) {
                 echo "Inserting $page_name revision from ". date("Y-m-d H:i:s",
                     $timestamp) ."\n!";
                 $group_model->setPageName(C\ROOT_ID,
                     $group_id, $page_name, $document,
-                    $locale_tag, "[Wiki Import on " . time(). " ]",
+                    $locale_tag, $edit_reason,
                     L\tl('social_component_page_created', $page_name),
                     L\tl('social_component_page_discuss_here'),
                     pubdate: $timestamp);
@@ -268,3 +320,41 @@ function importWiki($type, $group_name, $locale_tag, $wiki_path)
         }
     }
 }
+/**
+ *
+ */
+function importWikiMedia($user_id, $group_id, $locale_tag, $wiki_path,
+    $sub_path = "")
+{
+    $base_prefix = "$wiki_path/media/";
+    $media_prefix = "$base_prefix$sub_path/";
+    $media_paths = glob("$media_prefix*");
+    $len_prefix = strlen($media_prefix);
+    $group_model = new GroupModel();
+    if (!($page_id = $group_model->getPageId($group_id, "media",
+        $locale_tag))) {
+        $media_page = L\WikiParser::makeWikiPageHead(
+            ['page_type' => 'media_list']) . L\WikiParser::END_HEAD_VARS .
+            "media";
+        $page_id = $group_model->setPageName($user_id, $group_id, "media",
+            $media_page, $locale_tag, "create",
+            L\tl('social_component_page_created', $media_page),
+            L\tl('social_component_page_discuss_here'));
+    }
+    foreach ($media_paths as $media_path) {
+        if (is_dir($media_path)) {
+            $sub_subpath = substr($media_path, strlen($base_prefix));
+            $folders = $group_model->getGroupPageResourcesFolders($group_id,
+                $page_id, $sub_subpath, true);
+            echo "Importing $sub_subpath\n";
+            importWikiMedia($user_id, $group_id, $locale_tag, $wiki_path,
+                $sub_subpath);
+        } else {
+            $file_name = substr($media_path, $len_prefix);
+            $mime_type = L\mimeType($file_name, true);
+            $data = file_get_contents($media_path);
+            $group_model->copyFileToGroupPageResource($media_path, $file_name,
+                $mime_type, $group_id, $page_id, $sub_path, $data);
+        }
+    }
+}
diff --git a/src/configs/TokenTool.php b/src/configs/TokenTool.php
index b2eafaec5..bb3eb9c3e 100644
--- a/src/configs/TokenTool.php
+++ b/src/configs/TokenTool.php
@@ -960,7 +960,7 @@ function translateLocale($locale_tag, $with_wiki_pages = 0, $batch_size = 20,
                 }
                 //\r\n line endings
                 $group_page = preg_replace('/\r/u', '', $group_page);
-                $parsed_page = $controller->parsePageHeadVars($group_page,
+                $parsed_page = WikiParser::parsePageHeadVars($group_page,
                     true);
                 if (empty($parsed_page[0])) {
                     $parsed_page[0] = [];
@@ -1087,7 +1087,7 @@ function wikiHeaderPageToString($wiki_header, $wiki_page_data)
     }
     if (!empty($wiki_page_data) || (!empty($wiki_header['page_type']) &&
         $wiki_header['page_type'] != 'standard')) {
-        $page = $head_string . "END_HEAD_VARS" . $wiki_page_data;
+        $page = $head_string . L\WikiParser::END_HEAD_VARS . $wiki_page_data;
     }
     return $page;
 }
diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php
index 0c3572666..7c36d05d4 100755
--- a/src/controllers/Controller.php
+++ b/src/controllers/Controller.php
@@ -1063,53 +1063,7 @@ abstract class Controller
     public function parsePageHeadVarsView($view, $page_name, $page_data)
     {
         list($view->head_objects[$page_name], $view->page_objects[$page_name])=
-            $this->parsePageHeadVars($page_data, true);
-    }
-    /**
-     * Used to parse head meta variables out of a data string provided either
-     * from a wiki page or a static page. Meta data is stored in lines
-     * before the first occurrence of END_HEAD_VARS. Head variables
-     * are name=value pairs. An example of head
-     * variable might be:
-     * title = This web page's title
-     * Anything after a semi-colon on a line in the head section is treated as
-     * a comment
-     *
-     * @param string $page_data this is the actual content of a wiki or
-     *      static page
-     * @param bool whether to output just an array of head variables or
-     *      if output a pair [head vars, page body]
-     * @return array the associative array of head variables or pair
-     *      [head vars, page body]
-     */
-    public function parsePageHeadVars($page_data, $with_body = false)
-    {
-        $page_parts = explode("END_HEAD_VARS", $page_data);
-        $head_object = [];
-        if (count($page_parts) > 1) {
-            $head_lines = preg_split("/\n\n/", array_shift($page_parts));
-            $page_data = implode("END_HEAD_VARS", $page_parts);
-            foreach ($head_lines as $line) {
-                $semi_pos =  (strpos($line, ";")) ? strpos($line, ";"):
-                    strlen($line);
-                $line = substr($line, 0, $semi_pos);
-                $line_parts = explode("=", $line);
-                if (count($line_parts) == 2) {
-                    $key = trim(urldecode($line_parts[0]));
-                    $value = urldecode(trim($line_parts[1]));
-                    if ($key == 'page_alias') {
-                        $value = str_replace(" ", "_", $value);
-                    }
-                    $head_object[$key] = $value;
-                }
-            }
-        } else {
-            $page_data = $page_parts[0];
-        }
-        if ($with_body) {
-            return [$head_object, $page_data];
-        }
-        return $head_object;
+            L\WikiParser::parsePageHeadVars($page_data, true);
     }
     /**
      * If external source advertisements are present in the output of this
diff --git a/src/controllers/SearchController.php b/src/controllers/SearchController.php
index 3403e4401..809e6ab89 100755
--- a/src/controllers/SearchController.php
+++ b/src/controllers/SearchController.php
@@ -38,6 +38,7 @@ use seekquarry\yioop\library\FetchUrl;
 use seekquarry\yioop\library\FileCache;
 use seekquarry\yioop\library\PhraseParser;
 use seekquarry\yioop\library\UrlParser;
+use seekquarry\yioop\library\WikiParser;

 /**
  * Controller used to handle search requests to SeekQuarry
@@ -1387,7 +1388,7 @@ EOD;
                     $data['SEARCH_CALLOUT'] = "";
                     if (!empty($callout_info)) {
                         list( , $callout) =
-                            $this->parsePageHeadVars(
+                            WikiParser::parsePageHeadVars(
                                 $callout_info['PAGE'], true);
                         $data['SEARCH_CALLOUT'] = $callout;
                     }
diff --git a/src/controllers/StaticController.php b/src/controllers/StaticController.php
index c149c7bc8..79446e6f1 100644
--- a/src/controllers/StaticController.php
+++ b/src/controllers/StaticController.php
@@ -123,7 +123,7 @@ class StaticController extends Controller
             $page = "404";
             $page_string = $this->getPage($page);
         }
-        $page_parts = explode("END_HEAD_VARS", $page_string);
+        $page_parts = explode(L\WikiParser::END_HEAD_VARS, $page_string);
         $data['PAGE'] = $page_parts[1] ?? $page_parts[0];
         if (!isset($data["INCLUDE_SCRIPTS"])) {
             $data["INCLUDE_SCRIPTS"] = [];
@@ -215,7 +215,7 @@ EOD;
             }
             if (isset($page_header['PAGE'])) {
                 $header_parts =
-                    explode("END_HEAD_VARS", $page_header['PAGE']);
+                    explode(L\WikiParser::END_HEAD_VARS, $page_header['PAGE']);
             }
             $page_header['PAGE'] = $page_header['PAGE'] ?? "";
             $data["PAGE_HEADER"] = (isset($header_parts[1])) ?
@@ -230,7 +230,7 @@ EOD;
             }
             if (isset($page_footer['PAGE'])) {
                 $footer_parts =
-                    explode("END_HEAD_VARS", $page_footer['PAGE']);
+                    explode(L\WikiParser::END_HEAD_VARS, $page_footer['PAGE']);
             }
             $page_footer['PAGE'] = $page_footer['PAGE'] ?? "";
             $data['PAGE_FOOTER'] = (isset($footer_parts[1])) ?
diff --git a/src/controllers/components/CrawlComponent.php b/src/controllers/components/CrawlComponent.php
index ab418eed6..343368d57 100644
--- a/src/controllers/components/CrawlComponent.php
+++ b/src/controllers/components/CrawlComponent.php
@@ -39,6 +39,7 @@ use seekquarry\yioop\library\FetchUrl;
 use seekquarry\yioop\library\PageRuleParser;
 use seekquarry\yioop\library\PhraseParser;
 use seekquarry\yioop\library\UrlParser;
+use seekquarry\yioop\library\WikiParser;
 use seekquarry\yioop\library\media_jobs as M;
 use seekquarry\yioop\library\processors as P;
 use seekquarry\yioop\library\processors\PageProcessor;
@@ -2334,7 +2335,7 @@ class CrawlComponent extends Component implements CrawlConstants
                         $_REQUEST["ID"] = $kwiki["ID"];
                         $_REQUEST["QUERY"] = $query;
                         list( , $kwiki['PAGE']) =
-                            $parent->parsePageHeadVars(
+                            WikiParser::parsePageHeadVars(
                                 $kwiki['PAGE'], true);
                         $_REQUEST["KWIKI_PAGE"] = $kwiki['PAGE'];
                         return $parent->redirectWithMessage(
@@ -2428,7 +2429,7 @@ class CrawlComponent extends Component implements CrawlConstants
                     }
                     $_REQUEST["KWIKI_PAGE"] = $kwiki_page;
                     if (!empty($kwiki_page)) {
-                        $kwiki_page = $head_string . "END_HEAD_VARS" .
+                        $kwiki_page = $head_string . WikiParser::END_HEAD_VARS .
                             $kwiki_page;
                     }
                     $_REQUEST["ID"] = $verticals_model->setPageName(C\ROOT_ID,
diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php
index 675ae96b0..b9c2ec264 100644
--- a/src/controllers/components/SocialComponent.php
+++ b/src/controllers/components/SocialComponent.php
@@ -3289,7 +3289,7 @@ class SocialComponent extends Component implements CrawlConstants
                 "admin" : "group";
         $base_url = C\SHORT_BASE_URL;
         list($data, $sub_path, $additional_substitutions, $clean_array,
-            $strings_array, $page_defaults) = $this->initCommonWikiArrays(
+            $strings_array) = $this->initCommonWikiArrays(
                 $controller_name, $base_url);
         $group_model = $parent->model("group");
         if (isset($_SESSION['USER_ID'])) {
@@ -3423,7 +3423,7 @@ class SocialComponent extends Component implements CrawlConstants
                     $page = isset($page) ? $page : null;
                     $edit_reason = isset($edit_reason) ? $edit_reason: null;
                     $this->editWiki($data, $user_id, $group_id, $group,
-                        $page_id, $page_name, $page, $page_defaults, $sub_path,
+                        $page_id, $page_name, $page, $sub_path,
                         $edit_reason, $missing_fields, $read_address,
                         $additional_substitutions);
                     break;
@@ -3780,7 +3780,7 @@ class SocialComponent extends Component implements CrawlConstants
                 $this->initializeReadMode($data, $user_id, $group_id,
                     $sub_path);
             } else if (in_array($data['MODE'], ['edit', 'source'])) {
-                foreach ($page_defaults as $key => $default) {
+                foreach (WikiParser::PAGE_DEFAULTS as $key => $default) {
                     $data[$key] = $default;
                     if (isset($data["HEAD"][$key])) {
                         $data[$key] = $data["HEAD"][$key];
@@ -3857,7 +3857,7 @@ class SocialComponent extends Component implements CrawlConstants
                         $template_info = $group_model->
                             getPageInfoByName($group_id, $template_name,
                             $data['CURRENT_LOCALE_TAG'], "read");
-                        list( ,$tmp_page) = $parent->parsePageHeadVars(
+                        list(, $tmp_page) = WikiParser::parsePageHeadVars(
                             $template_info['PAGE'], true);
                         $tmp_page = preg_replace("/{{text\|(.+?)\|(.+?)}}/",
                             "<input type='text' class='narrow-field'" .
@@ -4034,7 +4034,7 @@ class SocialComponent extends Component implements CrawlConstants
                 $data['CURRENT_LOCALE_TAG'], $data["MODE"]);
             if (isset($page_header['PAGE'])) {
                 $header_parts =
-                    explode("END_HEAD_VARS", $page_header['PAGE']);
+                    explode(WikiParser::END_HEAD_VARS, $page_header['PAGE']);
             }
             $data["PAGE_HEADER"] = (isset($header_parts[1])) ?
                 $header_parts[1] : ($page_header['PAGE'] ?? "");
@@ -4062,7 +4062,7 @@ class SocialComponent extends Component implements CrawlConstants
                 $data["MODE"]);
             if (isset($page_footer['PAGE'])) {
                 $footer_parts =
-                    explode("END_HEAD_VARS", $page_footer['PAGE']);
+                    explode(WikiParser::END_HEAD_VARS, $page_footer['PAGE']);
             }
             $data['PAGE_FOOTER'] = (isset($footer_parts[1])) ?
                 $footer_parts[1] : ($page_footer['PAGE'] ?? "");
@@ -4139,7 +4139,7 @@ EOD;
                 $template_info = $group_model->
                     getPageInfoByName($group_id, $template_name,
                     $data['CURRENT_LOCALE_TAG'], "read");
-                list( ,$tmp_page) = $parent->parsePageHeadVars(
+                list( ,$tmp_page) = WikiParser::parsePageHeadVars(
                     $template_info['PAGE'], true);
                 $tmp_page = preg_replace("/{{(area|text)\|(.+?)\|(.+?)}}/",
                     "{{field|$2}}", $tmp_page);
@@ -4459,8 +4459,6 @@ EOD;
      * @param int $page_id if of wiki page being edited
      * @param string $page_name string name of wiki page being edited
      * @param string $page cleaned wiki page that came from $_REQUEST, if any
-     * @param array $page_defaults associative array system-wide defaults
-     *  for page settings of any wiki page
      * @param string $sub_path sub resource folder being edited of wiki page, if
      *  any
      * @param string $edit_reason reason for performing update on wiki page
@@ -4473,7 +4471,7 @@ EOD;
      *  substitutions to make in going from wiki page to html
      */
     private function editWiki(&$data, $user_id, $group_id, $group, $page_id,
-        $page_name, $page, $page_defaults, $sub_path, $edit_reason,
+        $page_name, $page, $sub_path, $edit_reason,
         $missing_fields, $read_address, $additional_substitutions)
     {
         if (empty($data["CAN_EDIT"])) {
@@ -4561,7 +4559,7 @@ EOD;
             $data['PAGE_NAME'] = $page_name;
             $data['RESOURCE_NAME'] = $file_name;
         } else {
-            list($head_object, $page_data) = $parent->parsePageHeadVars(
+            list($head_object, $page_data) = WikiParser::parsePageHeadVars(
                 $page_info['PAGE'] ?? "", true);
             $is_currently_template = (!empty($head_object["page_type"]) &&
                 $head_object["page_type"][0] == 't');
@@ -4588,7 +4586,7 @@ EOD;
                 $page_types = array_keys($data['page_types']);
                 $page_borders = array_keys($data['page_borders']);
                 $set_path = false;
-                foreach ($page_defaults as $key => $default) {
+                foreach (WikiParser::PAGE_DEFAULTS as $key => $default) {
                     $head_vars[$key] = (isset($head_object[$key])) ?
                         $head_object[$key] : $default;
                     if (isset($_REQUEST[$key])) {
@@ -4626,8 +4624,8 @@ EOD;
                                     ['name', 'size', 'modified'])) {
                                     if (isset($page_info['PAGE'])) {
                                         if (!isset($page)) {
-                                            $page_parts =
-                                                explode("END_HEAD_VARS",
+                                            $page_parts = explode(
+                                                WikiParser::END_HEAD_VARS,
                                                 $page_info['PAGE']);
                                             $page = isset($page_parts[1]) ?
                                                 $page_parts[1] : $page_parts[0];
@@ -4688,17 +4686,13 @@ EOD;
                             isset($_REQUEST['update_description']);
                     }
                 }
-                $head_string = "";
-                foreach ($page_defaults as $key => $default) {
-                    $head_string .= urlencode($key) . "=" .
-                        urlencode($head_vars[$key]) . "\n\n";
-                }
+                $head_string = WikiParser::makeWikiPageHead($head_vars);
                 if (is_array($page)) { //template case
                     $page = base64_encode(serialize($page));
                 }
                 if (!empty($page) || (!empty($head_vars['page_type']) &&
                     $head_vars['page_type'] != 'standard')) {
-                    $page = $head_string . "END_HEAD_VARS" . $page;
+                    $page = $head_string . WikiParser::END_HEAD_VARS . $page;
                 }
                 $page_info = (empty($page_info)) ? [] : $page_info;
                 $page_info['ID'] = $group_model->setPageName($user_id,
@@ -4950,7 +4944,7 @@ EOD;
         $data['PAGE_NAME'] = htmlentities($page_info['PAGE_NAME'] ?? "");
         $page_info = $group_model->getPageInfoByName($group_id,
             $page_info['PAGE_NAME'] ?? "", $data['CURRENT_LOCALE_TAG'], 'edit');
-        $data['HEAD'] = $parent->parsePageHeadVars($page_info['PAGE'] ?? "");
+        $data['HEAD'] = WikiParser::parsePageHeadVars($page_info['PAGE'] ?? "");
         $resources_info = $group_model->getGroupPageResourceUrls(
             $group_id, $page_id, $sub_path);
         $data['ORIGINAL_URL_PREFIX'] = $resources_info['url_prefix'];
@@ -5040,7 +5034,7 @@ EOD;
             $page_info['PAGE_NAME'] ?? "", $data['CURRENT_LOCALE_TAG'], 'edit');
         $data['RESOURCES_INFO'] = $group_model->getGroupPageResourceUrls(
             $group_id, $page_id, $sub_path);
-        $data['HEAD'] = $parent->parsePageHeadVars($page_info['PAGE'] ?? "");
+        $data['HEAD'] = WikiParser::parsePageHeadVars($page_info['PAGE'] ?? "");
         $this->initUserResourcePreferences($data);
         $resources = $data['RESOURCES_INFO']['resources'] ?? "";
         $num_resources = (is_array($resources)) ? count($resources) : 0;
@@ -5363,25 +5357,6 @@ EOD;
             "edit_reason" => C\SHORT_TITLE_LEN,
             "filter" => C\SHORT_TITLE_LEN,
             "resource_filter" => C\SHORT_TITLE_LEN];
-        $page_defaults = [
-            'alternative_path' => '',
-            'author' => '',
-            'default_sort' => 'aname',
-            'description' => '',
-            'page_alias' => '',
-            'page_border' => 'solid',
-            'page_header' => '',
-            'page_footer' => '',
-            'page_theme' => '',
-            'page_type' => 'standard',
-            'properties' => '',
-            'robots' => '',
-            'share_expires' => C\FOREVER,
-            'title' => '',
-            'toc' => true,
-            'url_shortener' => '',
-            'update_description' => false
-        ];
        /* Check if back params need to be set. Set them if required.
           the back params are usually sent when the wiki action is initiated
           from within an open help article.
@@ -5405,7 +5380,7 @@ EOD;
             $data['BACK_URL'] = http_build_query($back_params_cleaned);
         }
         return [$data, $sub_path, $additional_substitutions, $clean_array,
-            $strings_array, $page_defaults];
+            $strings_array];
     }
     /**
      * Used to create Javascript used to toggle a wiki page's settings control
diff --git a/src/library/VersionManager.php b/src/library/VersionManager.php
index 808acebd5..1b3fc1f98 100644
--- a/src/library/VersionManager.php
+++ b/src/library/VersionManager.php
@@ -281,7 +281,7 @@ class VersionManager
      *      carrying out the operation
      * @return int success code
      */
-    public function headPutContents($file, $data, $lock = true)
+    public function headPutContents($file, $data, $lock = true, $timestamp = 0)
     {
         if (empty($this->managed_folder)) {
             return self::PUT_CONTENTS_FAILED;
@@ -303,7 +303,7 @@ class VersionManager
             }
             return self::PUT_CONTENTS_FAILED;
         }
-        $this->createVersion($file, "", 0, false);
+        $this->createVersion($file, "", $timestamp, false);
         if ($lock) {
             unlink($lock_file);
         }
diff --git a/src/library/WikiParser.php b/src/library/WikiParser.php
index 5c3dda05f..72c8b41f9 100644
--- a/src/library/WikiParser.php
+++ b/src/library/WikiParser.php
@@ -43,12 +43,39 @@ require_once __DIR__."/../configs/Config.php";
  */
 class WikiParser implements CrawlConstants
 {
+    /**
+     * String used to separate the head variables of a Yioop wiki page from
+     * the page contents.
+     */
+    const END_HEAD_VARS = "END_HEAD_VARS";
     /**
      * Escape string to try to prevent incorrect nesting of div for some of the
      * substitutions;
-     * @var string
      */
-    public $esc=",[}";
+    const ESC=",[}";
+    /**
+     * Wiki page header field used for Yioop wiki pages and the default
+     * values (field => value pairs).
+     */
+    const PAGE_DEFAULTS = [
+        'alternative_path' => '',
+        'author' => '',
+        'default_sort' => 'aname',
+        'description' => '',
+        'page_alias' => '',
+        'page_border' => 'solid',
+        'page_header' => '',
+        'page_footer' => '',
+        'page_theme' => '',
+        'page_type' => 'standard',
+        'properties' => '',
+        'robots' => '',
+        'share_expires' => C\FOREVER,
+        'title' => '',
+        'toc' => true,
+        'url_shortener' => '',
+        'update_description' => false
+    ];
     /**
      * Whether the parser should be configured only to do minimal substitutions
      * or all available (minimal might be used for posts in discussion groups)
@@ -104,7 +131,7 @@ class WikiParser implements CrawlConstants
     public function __construct($base_address = "", $add_substitutions = [],
         $minimal = false)
     {
-        $esc = $this->esc;
+        $esc = self::ESC;
         $not_braces = '(?:[^\}]|[^\}]\})*';
         $not_paragraph = '(?:\A|[^\n]|[^\n]\n)';
         $class_or_id = '0-9a-zA-Z\_\-\s';
@@ -445,7 +472,7 @@ class WikiParser implements CrawlConstants
         $head_vars = [];
         $draw_toc = true;
         if ($parse_head_vars && !$this->minimal) {
-            $document_parts = explode("END_HEAD_VARS", $document);
+            $document_parts = explode(self::END_HEAD_VARS, $document);
             if (count($document_parts) > 1) {
                 $head = $document_parts[0];
                 $document = $document_parts[1];
@@ -525,7 +552,7 @@ class WikiParser implements CrawlConstants
             "/&lt;nowiki&gt;(.+?)&lt;\/nowiki&gt;/s",
             C\NS_LIB . "base64DecodeCallback", $document);
         if ($head != "" && $parse_head_vars) {
-            $document = $head . "END_HEAD_VARS" . $document;
+            $document = $head . self::END_HEAD_VARS . $document;
         }
         if (!$handle_big_files &&
             strlen($document) > 0.9 * C\MAX_GROUP_PAGE_LEN) {
@@ -587,7 +614,7 @@ class WikiParser implements CrawlConstants
      */
     public function cleanLinksAndParagraphs($document)
     {
-        $esc = $this->esc;
+        $esc = self::ESC;
         $document = preg_replace_callback("/((href=)\"([^\"]+)\")/",
             C\NS_LIB . "fixLinksCallback", $document);
         $document = preg_replace_callback(
@@ -747,19 +774,6 @@ class WikiParser implements CrawlConstants
                         $ref_data['title'] = "<a href=\"{$ref_data['url']}\">".
                             "{$ref_data['title']}</a>";
                     }
-                    if (isset($ref_data['quote'])) {
-                        $references .= '"'.$ref_data['quote'].'". ';
-                    }
-                    if (isset($ref_data['author'])) {
-                        $references .= $ref_data['author'].". ";
-                    }
-                    if (isset($ref_data['title'])) {
-                        $references .= '"'.$ref_data['title'].'". ';
-                    }
-                    if (isset($ref_data['accessdate']) &&
-                        !isset($ref_data['archivedate'])) {
-                        $references .= '('.$ref_data['accessdate'].') ';
-                    }
                     if (isset($ref_data['archivedate'])) {
                         if (isset($ref_data['archiveurl'])) {
                             $ref_data['archivedate'] = "<a href=\"".
@@ -768,45 +782,24 @@ class WikiParser implements CrawlConstants
                         }
                         $references .= '('.$ref_data['archivedate'].') ';
                     }
-                    if (isset($ref_data['journal'])) {
-                         $references .= "<i>{$ref_data['journal']}</i> ";
-                    }
-                    if (isset($ref_data['location'])) {
-                         $references .= $ref_data['location'].". ";
-                    }
-                    if (isset($ref_data['publisher'])) {
-                         $references .= $ref_data['publisher'].". ";
-                    }
-                    if (isset($ref_data['doi'])) {
-                         $references .= "doi:".$ref_data['doi'].". ";
-                    }
-                    if (isset($ref_data['isbn'])) {
-                         $references .= "ISBN:".$ref_data['isbn'].". ";
-                    }
-                    if (isset($ref_data['jstor'])) {
-                         $references .= "JSTOR:".$ref_data['jstor'].". ";
-                    }
-                    if (isset($ref_data['oclc'])) {
-                         $references .= "OCLC:".$ref_data['oclc'].". ";
-                    }
-                    if (isset($ref_data['volume'])) {
-                         $references .= "<b>".$ref_data['volume'].
-                            "</b> ";
-                    }
-                    if (isset($ref_data['issue'])) {
-                         $references .= "#".$ref_data['issue'].". ";
-                    }
-                    if (isset($ref_data['date'])) {
-                         $references .= $ref_data['date'].". ";
-                    }
-                    if (isset($ref_data['year'])) {
-                         $references .= $ref_data['year'].". ";
-                    }
-                    if (isset($ref_data['page'])) {
-                         $references .= "p.".$ref_data['page'].". ";
+                    if (isset($ref_data['accessdate']) &&
+                        !isset($ref_data['archivedate'])) {
+                        $references .= '('.$ref_data['accessdate'].') ';
                     }
-                    if (isset($ref_data['pages'])) {
-                         $references .= "pp.".$ref_data['pages'].". ";
+                    foreach ([
+                        "quote" => ['"', '". '], "author" => ['', '. '],
+                        "title" => ['"', '". '], "journal" => ['<i>', '</i> '],
+                        "location" => ['', '. '], "publisher" => ['', '. '],
+                        "doi" => ['doi:', '. '], "isbn" => ['ISBN:', '. '],
+                        "jstor" => ['JSTOR:', '. '], "oclc" => ['OCLC:', '. '],
+                        "volume" => ['<b>', '</b>'], "issue" => ['#', '. '],
+                        "date" => ['', '. '], "year" => ['', '. '],
+                        "page" => ['p.', '. '], "pages" => ['pp.', '. '],
+                        ] as $field => $tags) {
+                        if (isset($ref_data[$field])) {
+                            $references .= $tags[0] . $ref_data[$field] .
+                                $tags[1];
+                        }
                     }
                 }
                 $references .="</div>\n";
@@ -891,6 +884,64 @@ class WikiParser implements CrawlConstants
         $links = array_unique($links);
         return $links;
     }
+    /**
+     *
+     */
+    public static function makeWikiPageHead($head_vars = [])
+    {
+        $head_string = "";
+        foreach (self::PAGE_DEFAULTS as $key => $default) {
+            $head_string .= urlencode($key) . "=" .
+                urlencode($head_vars[$key] ?? $default) . "\n\n";
+        }
+        return $head_string;
+    }
+    /**
+     * Used to parse head meta variables out of a data string provided either
+     * from a wiki page or a static page. Meta data is stored in lines
+     * before the first occurrence of END_HEAD_VARS. Head variables
+     * are name=value pairs. An example of head
+     * variable might be:
+     * title = This web page's title
+     * Anything after a semi-colon on a line in the head section is treated as
+     * a comment
+     *
+     * @param string $page_data this is the actual content of a wiki or
+     *      static page
+     * @param bool whether to output just an array of head variables or
+     *      if output a pair [head vars, page body]
+     * @return array the associative array of head variables or pair
+     *      [head vars, page body]
+     */
+    public static function parsePageHeadVars($page_data, $with_body = false)
+    {
+        $page_parts = explode(self::END_HEAD_VARS, $page_data);
+        $head_object = [];
+        if (count($page_parts) > 1) {
+            $head_lines = preg_split("/\n\n/", array_shift($page_parts));
+            $page_data = implode(self::END_HEAD_VARS, $page_parts);
+            foreach ($head_lines as $line) {
+                $semi_pos =  (strpos($line, ";")) ? strpos($line, ";"):
+                    strlen($line);
+                $line = substr($line, 0, $semi_pos);
+                $line_parts = explode("=", $line);
+                if (count($line_parts) == 2) {
+                    $key = trim(urldecode($line_parts[0]));
+                    $value = urldecode(trim($line_parts[1]));
+                    if ($key == 'page_alias') {
+                        $value = str_replace(" ", "_", $value);
+                    }
+                    $head_object[$key] = $value;
+                }
+            }
+        } else {
+            $page_data = $page_parts[0];
+        }
+        if ($with_body) {
+            return [$head_object, $page_data];
+        }
+        return $head_object;
+    }
 }
 /**
  * Callback used by a preg_replace_callback in nextPage to make a table
diff --git a/src/models/GroupModel.php b/src/models/GroupModel.php
index 0422ec51a..1b0063a26 100644
--- a/src/models/GroupModel.php
+++ b/src/models/GroupModel.php
@@ -1831,6 +1831,7 @@ class GroupModel extends Model implements MediaConstants
         $db = $this->db;
         $pubdate = ($pubdate == -1) ? time() : $pubdate;
         $parser = new WikiParser($base_address, $additional_substitutions);
+        $end_head = WikiParser::END_HEAD_VARS;
         if ($add_relationship_data) {
             $links_relationships = $parser->fetchLinks($page);
         }
@@ -1841,6 +1842,9 @@ class GroupModel extends Model implements MediaConstants
             if (strstr($page, '{{submit|') !== false) {
                 $is_form = true;
             }
+            if (!str_contains($page, $end_head)) {
+                $page = WikiParser::makeWikiPageHead() . $end_head . $page;
+            }
             $parsed_page = $parser->parse($page);
             if ($is_form && strstr($page, '<form ') !== false) {
                 return null;
@@ -1851,14 +1855,13 @@ class GroupModel extends Model implements MediaConstants
             $parsed_page = $this->insertResourcesParsePage($group_id, $page_id,
                 $locale_tag, $parsed_page);
             if ($is_form) {
-                $end_head = "END_HEAD_VARS";
                 $parsed_page = str_replace($end_head, $end_head .
                     "\n<form method='post' >\n<input type='hidden' name='" .
                     C\CSRF_TOKEN . "' value='[{just-token}]' >" .
                     "<input type='hidden' name='CSV_FORM_HASH' ".
                     "value='[{form-hash}]' >", $parsed_page);
                 $parsed_page .= "\n</form>\n";
-                $page_body = explode("END_HEAD_VARS", $parsed_page, 2)[1];
+                $page_body = explode($end_head, $parsed_page, 2)[1];
                 $parsed_page = preg_replace("/\[{form\-hash}\]/",
                     "[{form-hash". L\crawlHash($page_body) . "}]",
                     $parsed_page);
@@ -3867,7 +3870,8 @@ EOD;
      *      (only used in case run non-empty)
      */
     public function copyFileToGroupPageResource($tmp_name, $file_name,
-        $mime_type, $group_id, $page_id, $sub_path = "", $data = "")
+        $mime_type, $group_id, $page_id, $sub_path = "", $data = "",
+        $timestamp = 0)
     {
         $folders = $this->getGroupPageResourcesFolders($group_id, $page_id,
             $sub_path, true);
@@ -3881,10 +3885,10 @@ EOD;
                 return false;
             }
             $file_size = filesize("$folder/$file_name");
-            $vcs->createVersion("$folder/$file_name");
+            $vcs->createVersion("$folder/$file_name", "", $timestamp);
         } else {
             $file_size = strlen($data);
-            $vcs->headPutContents("$folder/$file_name", $data);
+            $vcs->headPutContents("$folder/$file_name", $data, $timestamp);
         }
         $this->makeThumbStripExif($file_name, $folder, $thumb_folder,
             $mime_type);
@@ -4100,7 +4104,8 @@ EOD;
         if (!($clip_page_id = $this->getPageId($clip_group_id,
             C\CLIPBOARD_PAGE_NAME, C\DEFAULT_LOCALE) ) ) {
             $clip_page_id = $this->setPageName($user_id, $clip_group_id,
-                C\CLIPBOARD_PAGE_NAME, "page_type=media_list\n\nEND_HEAD_VARS" .
+                C\CLIPBOARD_PAGE_NAME, "page_type=media_list\n\n" .
+                WikiParser::END_HEAD_VARS .
                 C\CLIPBOARD_PAGE_NAME, C\DEFAULT_LOCALE, "create", "", "");
             if (!$clip_page_id) {
                 return false;
@@ -4714,12 +4719,12 @@ EOD;
             $result = $db->execute($sql, $params);
             $i = 0;
             if ($result) {
-                $separator_len = strlen("END_HEAD_VARS");
+                $separator_len = strlen(WikiParser::END_HEAD_VARS);
                 $stretch = ($_SERVER["MOBILE"]) ? 5 : 9;
                 $max_title_len = $stretch * C\NAME_TRUNCATE_LEN;
                 while ($pages[$i] = $db->fetchArray($result)) {
                     $head_pos = strpos($pages[$i]['DESCRIPTION'],
-                        "END_HEAD_VARS");
+                        WikiParser::END_HEAD_VARS);
                     if ($head_pos) {
                         $head = substr($pages[$i]['DESCRIPTION'], 0, $head_pos);
                         if (preg_match('/page_type\=(.*)/', $head, $matches)) {
ViewGit