Browse Source

Add basic metropolis code

nsITobin 4 months ago
parent
commit
e86e5cd661
100 changed files with 15922 additions and 330 deletions
  1. 1 0
      .gitattributes
  2. 25 0
      .gitignore
  3. 324 0
      base/skin/stylesheet.css
  4. 14 330
      base/skin/template.xhtml
  5. 0 0
      base/src/.exists
  6. 24 0
      base/src/main.php
  7. 2443 0
      base/src/utils.php
  8. 8 0
      components/site/content/index.mtz
  9. 1 0
      components/site/skin/template.html
  10. 18 0
      components/site/src/main.php
  11. 22 0
      index.php
  12. 203 0
      modules/mtzGenerateContent.php
  13. 36 0
      third_party/markdown/License.md
  14. 10 0
      third_party/markdown/Markdown.inc.php
  15. 1889 0
      third_party/markdown/Markdown.php
  16. 38 0
      third_party/markdown/MarkdownInterface.php
  17. 106 0
      third_party/smarty/Autoloader.php
  18. 179 0
      third_party/smarty/LICENSE
  19. 1397 0
      third_party/smarty/Smarty.class.php
  20. 16 0
      third_party/smarty/bootstrap.php
  21. 160 0
      third_party/smarty/debug.tpl
  22. 121 0
      third_party/smarty/plugins/block.textformat.php
  23. 62 0
      third_party/smarty/plugins/function.counter.php
  24. 92 0
      third_party/smarty/plugins/function.cycle.php
  25. 204 0
      third_party/smarty/plugins/function.fetch.php
  26. 286 0
      third_party/smarty/plugins/function.html_checkboxes.php
  27. 158 0
      third_party/smarty/plugins/function.html_image.php
  28. 230 0
      third_party/smarty/plugins/function.html_options.php
  29. 266 0
      third_party/smarty/plugins/function.html_radios.php
  30. 388 0
      third_party/smarty/plugins/function.html_select_date.php
  31. 354 0
      third_party/smarty/plugins/function.html_select_time.php
  32. 164 0
      third_party/smarty/plugins/function.html_table.php
  33. 134 0
      third_party/smarty/plugins/function.mailto.php
  34. 142 0
      third_party/smarty/plugins/function.math.php
  35. 145 0
      third_party/smarty/plugins/modifier.capitalize.php
  36. 85 0
      third_party/smarty/plugins/modifier.date_format.php
  37. 103 0
      third_party/smarty/plugins/modifier.debug_print_var.php
  38. 257 0
      third_party/smarty/plugins/modifier.escape.php
  39. 71 0
      third_party/smarty/plugins/modifier.mb_wordwrap.php
  40. 55 0
      third_party/smarty/plugins/modifier.regex_replace.php
  41. 37 0
      third_party/smarty/plugins/modifier.replace.php
  42. 26 0
      third_party/smarty/plugins/modifier.spacify.php
  43. 62 0
      third_party/smarty/plugins/modifier.truncate.php
  44. 28 0
      third_party/smarty/plugins/modifiercompiler.cat.php
  45. 32 0
      third_party/smarty/plugins/modifiercompiler.count_characters.php
  46. 26 0
      third_party/smarty/plugins/modifiercompiler.count_paragraphs.php
  47. 26 0
      third_party/smarty/plugins/modifiercompiler.count_sentences.php
  48. 31 0
      third_party/smarty/plugins/modifiercompiler.count_words.php
  49. 32 0
      third_party/smarty/plugins/modifiercompiler.default.php
  50. 110 0
      third_party/smarty/plugins/modifiercompiler.escape.php
  51. 30 0
      third_party/smarty/plugins/modifiercompiler.from_charset.php
  52. 30 0
      third_party/smarty/plugins/modifiercompiler.indent.php
  53. 29 0
      third_party/smarty/plugins/modifiercompiler.lower.php
  54. 20 0
      third_party/smarty/plugins/modifiercompiler.noprint.php
  55. 24 0
      third_party/smarty/plugins/modifiercompiler.string_format.php
  56. 30 0
      third_party/smarty/plugins/modifiercompiler.strip.php
  57. 28 0
      third_party/smarty/plugins/modifiercompiler.strip_tags.php
  58. 30 0
      third_party/smarty/plugins/modifiercompiler.to_charset.php
  59. 44 0
      third_party/smarty/plugins/modifiercompiler.unescape.php
  60. 28 0
      third_party/smarty/plugins/modifiercompiler.upper.php
  61. 39 0
      third_party/smarty/plugins/modifiercompiler.wordwrap.php
  62. 89 0
      third_party/smarty/plugins/outputfilter.trimwhitespace.php
  63. 26 0
      third_party/smarty/plugins/shared.escape_special_chars.php
  64. 35 0
      third_party/smarty/plugins/shared.literal_compiler_param.php
  65. 49 0
      third_party/smarty/plugins/shared.make_timestamp.php
  66. 53 0
      third_party/smarty/plugins/shared.mb_str_replace.php
  67. 51 0
      third_party/smarty/plugins/shared.mb_unicode.php
  68. 19 0
      third_party/smarty/plugins/variablefilter.htmlspecialchars.php
  69. 219 0
      third_party/smarty/sysplugins/smarty_cacheresource.php
  70. 297 0
      third_party/smarty/sysplugins/smarty_cacheresource_custom.php
  71. 538 0
      third_party/smarty/sysplugins/smarty_cacheresource_keyvaluestore.php
  72. 68 0
      third_party/smarty/sysplugins/smarty_data.php
  73. 90 0
      third_party/smarty/sysplugins/smarty_internal_block.php
  74. 235 0
      third_party/smarty/sysplugins/smarty_internal_cacheresource_file.php
  75. 52 0
      third_party/smarty/sysplugins/smarty_internal_compile_append.php
  76. 96 0
      third_party/smarty/sysplugins/smarty_internal_compile_assign.php
  77. 189 0
      third_party/smarty/sysplugins/smarty_internal_compile_block.php
  78. 24 0
      third_party/smarty/sysplugins/smarty_internal_compile_block_child.php
  79. 31 0
      third_party/smarty/sysplugins/smarty_internal_compile_block_parent.php
  80. 117 0
      third_party/smarty/sysplugins/smarty_internal_compile_break.php
  81. 89 0
      third_party/smarty/sysplugins/smarty_internal_compile_call.php
  82. 105 0
      third_party/smarty/sysplugins/smarty_internal_compile_capture.php
  83. 79 0
      third_party/smarty/sysplugins/smarty_internal_compile_child.php
  84. 96 0
      third_party/smarty/sysplugins/smarty_internal_compile_config_load.php
  85. 25 0
      third_party/smarty/sysplugins/smarty_internal_compile_continue.php
  86. 40 0
      third_party/smarty/sysplugins/smarty_internal_compile_debug.php
  87. 70 0
      third_party/smarty/sysplugins/smarty_internal_compile_eval.php
  88. 158 0
      third_party/smarty/sysplugins/smarty_internal_compile_extends.php
  89. 164 0
      third_party/smarty/sysplugins/smarty_internal_compile_for.php
  90. 343 0
      third_party/smarty/sysplugins/smarty_internal_compile_foreach.php
  91. 236 0
      third_party/smarty/sysplugins/smarty_internal_compile_function.php
  92. 207 0
      third_party/smarty/sysplugins/smarty_internal_compile_if.php
  93. 347 0
      third_party/smarty/sysplugins/smarty_internal_compile_include.php
  94. 157 0
      third_party/smarty/sysplugins/smarty_internal_compile_insert.php
  95. 37 0
      third_party/smarty/sysplugins/smarty_internal_compile_ldelim.php
  96. 62 0
      third_party/smarty/sysplugins/smarty_internal_compile_make_nocache.php
  97. 73 0
      third_party/smarty/sysplugins/smarty_internal_compile_nocache.php
  98. 31 0
      third_party/smarty/sysplugins/smarty_internal_compile_parent.php
  99. 124 0
      third_party/smarty/sysplugins/smarty_internal_compile_private_block_plugin.php
  100. 228 0
      third_party/smarty/sysplugins/smarty_internal_compile_private_foreachsection.php

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+* -text

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+# .gitignore - List of filenames git should ignore
+
+# Filenames that should be ignored wherever they appear
+*~
+*.rej
+*.pyc
+*.pyo
+TAGS
+tags
+ID
+.DS_Store*
+
+# Vim swap files.
+.*.sw[a-z]
+
+# Directories
+.obj/
+_tools/
+datastore/
+datastore
+base/tests/*
+/datastore/.config/*
+
+# Ignore misc files that need not be in the repo
+/robots.txt

+ 324 - 0
base/skin/stylesheet.css

@@ -0,0 +1,324 @@
+@charset "utf-8";
+      body {
+        margin: 0px;
+        background-color: #404040;
+        background-image: radial-gradient(circle at 50% 85%, #dee6e8 0%, #c6ced1 80%, #b8cacc 100%),
+                          radial-gradient(ellipse at 50% 100%, #ecf2f0 0%, #ecf2f080 50%, #00000000 100%);
+        background-repeat: no-repeat;
+        background-attachment: fixed;
+        background-position-x: center;
+      }
+
+      body, td, th {
+        font-family: "RedHatText";
+        font-size: 12pt;
+        color: #000000;
+      }
+
+      /* ========================================================================== */
+
+      a:link, a:active, a:visited {
+        color: #0066cc;
+        text-decoration: none;
+      }
+
+      a:hover {
+        text-decoration: underline;
+        color: #cc0000;
+      }
+
+      hr {
+        border-top: 1px dotted #000;
+        border-bottom: 0px #000;
+        border-left: 0px #000;
+        border-right: 0px #000;
+        border-width: 1px;
+      }
+
+      .alignleft {
+        float: left;
+        text-align: left;
+        margin-right: 10px;
+      }
+
+      .alignright {
+        float: right;
+        text-align: right;
+        margin-left: 10px;
+      }
+
+      .aligncenter {
+        display:block;
+        margin-left:auto;
+        margin-right:auto;
+      }
+
+      h1, h2, h3 {
+        font-family: "RedHatDisplay";
+        line-height: 100%;
+        padding-bottom: 2px;
+        margin: 0.5em auto;
+      }
+
+      h1, h2 {
+        border-bottom: 1px solid #d6e5f5;
+      }
+
+      h1:first-of-type {
+        margin-top: 10px;
+      }
+
+      /* ========================================================================== */
+
+      .binoc-dwm-surface {
+        background-color: #29456a !important;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-window {
+        width: 1200px;
+        min-width: 1200px;
+        max-width: 1200px;
+        margin: 0 auto;
+        margin-top: 32px;
+        border: 1px solid #5e81a6;
+        box-shadow: inset 0px 0px 1px #212424;
+        box-shadow: 0px 0px 20px rgba(0,0,0,0.40);
+        transition: all 0.2s linear;
+        padding: 0px;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-caption {
+        height: 28px;
+        margin-top: 0px;
+        margin-left: 8px;
+        margin-right: 8px;
+        line-height: inital;
+        text-align: left;
+        color: #fff;
+        font-size: 10pt;
+        text-shadow: 0px 1px 1px black, 0px 1px 1px #000;
+        overflow: hidden;
+      }
+
+      #binoc-caption p {
+        margin: 0px;
+        padding-top: 5px;
+        font-size: 9.5pt;
+        font-weight: bold;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-client {
+        background-color: #fcfcfc;
+        border-left: 1px solid #5e81a6;
+        border-right: 1px solid #5e81a6;
+        border-top: 1px solid #5e81a6;
+        margin-top: 0px;
+        margin-left: 2px;
+        margin-right: 2px;
+        margin-bottom: 0px;
+        padding: 0px;
+        overflow-x: hidden;
+        min-height: 634px;
+        height: 634px;
+      }
+
+      #binoc-client p {
+        line-height: 125%;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-commandbar {
+        height: 30px;
+        max-height: 30px;
+        background-color: #f5f6f7 !important;
+        border-bottom: 1px solid #d7d7d7;
+        margin: 0px;
+        padding-top: 0px;
+        font-size: 10pt;
+        color: #1e395b;
+        /* text-shadow: 0px 2px 2px black, 0px 1px 1px #000; */
+        font-family: "RedHatDisplay";
+      }
+
+      #binoc-commandbar > ul {
+        display: table;
+        margin: 0px;
+        padding: 0px;
+      }
+
+      #binoc-commandbar > ul > li {
+        float: left;
+        list-style: none;
+        margin-top: 6px;
+        margin-left: 12px;
+        margin-bottom: 0px;
+      }
+
+      #binoc-commandbar > ul > li > a,
+      #binoc-commandbar > ul > li > a:visited,
+      #binoc-commandbar > ul > li > a:hover,
+      #binoc-commandbar > ul > li > a:active {
+        text-decoration: none;
+      }
+
+      #binoc-commandbar > ul > li > a,
+      #binoc-commandbar > ul > li > a:visited {
+        color: #1e395b;
+        border: 1px solid transparent;
+        padding-top: 2px;
+        padding-left: 12px;
+        padding-right: 12px;
+        padding-bottom: 2px;
+      }
+
+      #binoc-commandbar > ul > li > a:hover {
+        border: 1px solid #6dbde4;
+        background-color: #dcebf4;
+      }
+
+      #binoc-commandbar > ul > li > a:active {
+        border: 1px solid #26a0da;
+        background-color: #c3e1f0;
+      }
+
+      #binoc-commandbar > ul > li > ul,
+      #binoc-commandbar > ul > li > p {
+        display: none;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-body {
+        margin: 0px;
+        display: table;
+        width: 100%;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-content {
+        display: table-cell;
+        vertical-align: top;
+        overflow-x: hidden;
+        padding-top: 0px;
+        padding-left: 12px;
+        padding-right: 12px;
+        padding-bottom: 0px;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-sidebar {
+        display: table-cell;
+        vertical-align: top;
+        overflow-x: hidden;
+        border-left: 1px solid #d6e5f5;
+        min-height: 400px;
+        min-width: 220px;
+        max-width: 220px;
+        width: 220px;
+
+        padding-top: 0px;
+        padding-left: 12px;
+        padding-right: 12px;
+        padding-bottom: 12px;
+      }
+
+      #binoc-sidebar > ul {
+        list-style: none;
+        padding-left: 0px;
+      }
+
+      #binoc-sidebar > ul > li {
+        margin-bottom: 2px;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-statusbar {
+        height: 20px;
+        min-height: 20px;
+        max-height: 20px;
+        background-color: #f0f0f0 !important;
+        border-top: 1px solid #d7d7d7;
+        margin: 0px;
+        font-size: 9pt;
+        color: #000;
+        font-family: "RedHatDisplay";
+        border-left: 1px solid #5e81a6;
+        border-right: 1px solid #5e81a6;
+        border-bottom: 1px solid #5e81a6;
+        margin-top: 0px;
+        margin-left: 2px;
+        margin-right: 2px;
+        margin-bottom: 2px;
+        padding-top: 2px;
+        padding-left: 4px;
+        padding-right: 4px;
+      }
+
+      /* ========================================================================== */
+
+      #binoc-copyright {
+        margin: 0 auto;
+        font-size: 10pt;
+        line-height: inital;
+        margin-top: 10px;
+        margin-bottom: 10px;
+        width: 1200px;
+        min-width: 1200px;
+        text-align: center;
+        font-weight: bold;
+        font-family: "RedHatDisplay";
+        color: #fff;
+        text-shadow: 0px 2px 2px black, 0px 1px 1px #000;
+      }
+
+      /* ========================================================================== */
+
+      .fadeIn {
+        animation: fade 0.4s;
+      }
+
+      @keyframes fade {
+        from { opacity: 0; }
+        to { opacity: 1; }
+      }
+
+      .pulseText { animation: pulse 2000ms infinite; }
+      @keyframes pulse { 50% { color: red; } }
+
+      /* ========================================================================== */
+
+      .special-textbox {
+        width: 1190px;
+        max-width: 1190px;
+        height: 597px;
+        max-height: 597px;
+        margin: 0px;
+        margin-left: -12px;
+        padding: 0px;
+        padding-top: 6px;
+        padding-left: 6px;
+        background-color: transparent;
+        border: none;
+        box-sizing: content-box !important;
+        resize: none;
+      }
+
+      /* ========================================================================== */
+
+      .clearfix:after {
+        content: ".";
+        display: block;
+        height: 0;
+        clear: both;
+        visibility: hidden;
+      }

+ 14 - 330
base/skin/template.xhtml

@@ -1,345 +1,29 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-    <title>{$SITE_NAME} - {$PAGE_TITLE}</title>
+    <title>{$PAGE_TITLE} - {$SITE_NAME}</title>
     <link rel="stylesheet" href="{$SKIN_PATH}/webfonts/red-hat-font.css" />
-    <style>
-      body {
-        margin: 0px;
-        /* background-color: #51667b; Mattcast Blue*/
-        background-color: #181f2e;
-        background-image: url('{$SKIN_PATH}/background.webp');
-        background-repeat: no-repeat;
-        background-attachment: fixed;
-        background-position-x: center;
-      }
-
-      body, td, th {
-        font-family: "RedHatText";
-        font-size: 12pt;
-        color: #000000;
-      }
-
-      /* ========================================================================== */
-
-      a:link, a:active, a:visited {
-        color: #0066cc;
-        text-decoration: none;
-      }
-
-      a:hover {
-        text-decoration: underline;
-        color: #cc0000;
-      }
-
-      hr {
-        border-top: 1px dotted #000;
-        border-bottom: 0px #000;
-        border-left: 0px #000;
-        border-right: 0px #000;
-        border-width: 1px;
-      }
-
-      .alignleft {
-        float: left;
-        text-align: left;
-        margin-right: 10px;
-      }
-
-      .alignright {
-        float: right;
-        text-align: right;
-        margin-left: 10px;
-      }
-
-      .aligncenter {
-        display:block;
-        margin-left:auto;
-        margin-right:auto;
-      }
-
-      h1, h2, h3 {
-        font-family: "RedHatDisplay";
-        line-height: 100%;
-        padding-bottom: 2px;
-        margin: 0.5em auto;
-      }
-
-      h1:first-of-type {
-        margin-top: 10px;
-      }
-
-      h1, h2 {
-        border-bottom: 1px solid #d6e5f5;
-      }
-
-      /* ========================================================================== */
-
-      .binoc-background-blur {
-        background-color: #020814;
-        background-image: url('{$SKIN_PATH}/background-blur.webp');
-        background-repeat: no-repeat;
-        background-attachment: fixed;
-        background-position-x: center;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-window {
-        width: 1200px;
-        min-width: 1200px;
-        max-width: 1200px;
-        margin: 0 auto;
-        margin-top: 32px;
-        border-radius: 5px;
-        border: 1px solid #f9f9f9;
-        box-shadow: 0px 2px 10px #000000;
-        transition: all 0.2s linear;
-        padding: 0px;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-caption {
-        height: 40px;
-        margin-top: 0px;
-        margin-left: 7px;
-        margin-right: 6px;
-        line-height: inital;
-        text-align: left;
-        color: #fff;
-        font-size: 10pt;
-        text-shadow: 0px 2px 2px black, 0px 1px 1px #000;
-        overflow: hidden;
-      }
-
-      #binoc-caption p {
-        margin: 0px;
-        padding-top: 8px;
-        font-size: 12pt;
-        font-weight: normal;
-      }
-
-      #binoc-caption-back,
-      #binoc-caption-back:visited {
-        background-image: url('{$SKIN_PATH}/back.png');
-        width: 31px;
-        height: 31px;
-        margin-top: 4px;
-      }
-
-      #binoc-caption-back:hover {
-        background-position-x: -31px;
-      }
-
-      #binoc-caption-back:active {
-        background-position-x: -62px;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-client {
-        background-color: #fcfcfc;
-        border: 1px solid #1f1f1f;
-        outline: 1px solid #9f9f9f;
-        margin-top: 0px;
-        margin-left: 6px;
-        margin-right: 6px;
-        margin-bottom: 6px;
-        padding: 0px;
-        overflow-x: hidden;
-        min-height: 634px;
-      }
-
-      #binoc-client p {
-        line-height: 125%;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-commandbar {
-        height: 30px;
-        max-height: 30px;
-        background: rgb(0,0,128);
-        background: linear-gradient(0deg, rgba(255,255,255,0) 50%, rgba(255,255,255,0.20) 50%, rgba(255,255,255,0.50) 100%),
-                    linear-gradient(90deg, #4e5882 0%, #5eb0d0 50%, #4e5882 100%);
-        background-blend-mode: hard-light;
-        border-bottom: 1px solid #1f1f1f;
-        margin: 0px;
-        padding-top: 0px;
-        font-size: 10pt;
-        color: #fff;
-        text-shadow: 0px 2px 2px black, 0px 1px 1px #000;
-        font-family: "RedHatDisplay";
-      }
-
-      #binoc-commandbar > ul {
-        display: table;
-        margin: 0px;
-        padding: 0px;
-      }
-
-      #binoc-commandbar > ul > li {
-        float: left;
-        list-style: none;
-        margin-top: 5px;
-        margin-left: 12px;
-        margin-bottom: 0px;
-      }
-
-      #binoc-commandbar > ul > li > a,
-      #binoc-commandbar > ul > li > a:active,
-      #binoc-commandbar > ul > li > a:visited{
-        text-decoration: none;
-        color: #fff;
-      }
-
-      #binoc-commandbar > ul > li > a {
-        border: 1px solid transparent;
-        border-radius: 2px;
-        padding-top: 2px;
-        padding-left: 12px;
-        padding-right: 12px;
-        padding-bottom: 2px;
-      }
-
-      #binoc-commandbar > ul > li > a:hover {
-        border: 1px solid #a1b6cb;
-        box-shadow: 0 0 1px #1f1f1f;
-        background: linear-gradient(0deg,
-                                    rgba(255,255,255,0) 50%,
-                                    rgba(255,255,255,0.10) 50%,
-                                    rgba(255,255,255,0.20) 85%,
-                                    rgba(255,255,255,0.50) 100%);
-      }
-
-      #binoc-commandbar > ul > li > a:active {
-        border: 1px solid #000;
-        box-shadow: none;
-        background: linear-gradient(0deg,
-                                    rgba(31,31,31,0.05) 50%,
-                                    rgba(31,31,31,0.05) 50%,
-                                    rgba(31,31,31,0.20) 85%,
-                                    rgba(31,31,31,0.50) 100%);
-      }
-
-      #binoc-commandbar > ul > li > ul,
-      #binoc-commandbar > ul > li > p {
-        display: none;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-body {
-        margin: 0px;
-        display: table;
-        width: 100%;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-content {
-        display: table-cell;
-        vertical-align: top;
-        overflow-x: hidden;
-        padding-top: 0px;
-        padding-left: 12px;
-        padding-right: 12px;
-        padding-bottom: 12px;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-sidebar {
-        display: table-cell;
-        vertical-align: top;
-        overflow-x: hidden;
-        border-left: 1px solid #d6e5f5;
-        min-height: 400px;
-        min-width: 220px;
-        max-width: 220px;
-        width: 220px;
-
-        padding-top: 0px;
-        padding-left: 12px;
-        padding-right: 12px;
-        padding-bottom: 12px;
-      }
-
-      #binoc-sidebar > ul {
-        list-style: none;
-        padding-left: 0px;
-      }
-
-      #binoc-sidebar > ul > li {
-        margin-bottom: 2px;
-      }
-
-      /* ========================================================================== */
-
-      #binoc-copyright {
-        margin: 0 auto;
-        font-size: 10pt;
-        line-height: inital;
-        margin-top: 10px;
-        margin-bottom: 10px;
-        width: 1200px;
-        min-width: 1200px;
-        text-align: center;
-        font-weight: bold;
-        font-family: "RedHatDisplay";
-        color: #fff;
-        text-shadow: 0px 2px 2px black, 0px 1px 1px #000;
-      }
-
-      /* ========================================================================== */
-
-      .special-textbox {
-        width: 1148px;
-        margin-top: 12px;
-        margin-bottom: -12px;
-        background-color: #fff;
-        border: 1px solid #e3e9ef;
-        border-top-color: #abadb3;
-        border-radius: 2px;
-        padding: 6px;
-        resize: none;
-      }
-
-      /* ========================================================================== */
-
-      .clearfix:after {
-        content: ".";
-        display: block;
-        height: 0;
-        clear: both;
-        visibility: hidden;
-      }
+    <style id="cssMain" type="text/css">
+      {$SITE_STYLESHEET}
     </style>
   </head>
   <body>
-    <div id="binoc-window" class="binoc-background-blur">
+    <div id="binoc-window" class="binoc-dwm-surface">
       <div id="binoc-caption">
-        <div id="binoc-caption-back" class="alignleft" onclick="window.history.back();"></div>
         <p>{$PAGE_TITLE}</p>
       </div>
       <div id="binoc-client">
-        <div id="binoc-commandbar">
-          <ul>
-            {$SITE_MENU}
-          </ul>
-        </div>
-        <div id="binoc-body">
-          <div id="binoc-content">
-            {$PAGE_CONTENT}
-          </div>
-        </div>
+        <div id="binoc-commandbar"><ul>{$SITE_MENU}</ul></div>
+        <div id="binoc-body"><div id="binoc-content" class="fadeIn">{$PAGE_CONTENT}</div></div>
+      </div>
+      <div id="binoc-statusbar">
+        <span id="binoc-statustext" class="alignleft" style="min-width: 220px;">{$PAGE_STATUS}</span>
+        <span class="alignright" style="border-left: 1px solid #d7d7d7; padding-left: 8px; color: #6d6d6d"><em>{$SOFTWARE_VENDOR} {$SOFTWARE_NAME} {$SOFTWARE_VERSION}</em></span>
+        <span class="alignright" style="border-left: 1px solid #d7d7d7; padding-left: 8px;">{$SITE_DOMAIN}</span>
       </div>
     </div>
-    <p id="binoc-copyright">
-      Powered by {$SOFTWARE_NAME} {$SOFTWARE_VERSION}
-      <br />
-    </p>
   </body>
 </html>

+ 0 - 0
base/src/.exists


+ 24 - 0
base/src/main.php

@@ -0,0 +1,24 @@
+<?php /****************************************************************************************************************
+* PHP Code is filmed before a live studio audience.
+**********************************************************************************************************************/
+
+// == | Main | ========================================================================================================
+
+// Site Offline
+if (gGetConfig('app.offline', 'true') || file_exists(gBuildPath(kRootPath, '.offline'))) {
+  gError('This site is currently unavailable. Please try again later.');
+}
+
+// --------------------------------------------------------------------------------------------------------------------
+
+// Register modules
+gRegisterIncludes('COMPONENTS', ['site']);
+gRegisterIncludes('MODULES', ['generateContent']);
+
+// --------------------------------------------------------------------------------------------------------------------
+
+// Fall through to utils loading the component which will be the Site component by default.
+
+// ====================================================================================================================
+
+?>

+ 2443 - 0
base/src/utils.php

@@ -0,0 +1,2443 @@
+<?php /****************************************************************************************************************
+* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/.
+*
+* Portions of this file are under other licenses separated by namespace
+* boundaries. This is noted were applicable.
+**********************************************************************************************************************/
+
+namespace { // == | Setup and Global Constants | ======================================================================
+
+// Check for kRootPath
+if (!defined('kRootPath')) {
+  die('BinOC Metropolis Utilities: You MUST define kRootPath.');
+}
+
+// Do not allow this to be included more than once...
+if (defined('kUtilities')) {
+  die('BinOC Metropolis Utilities: You may not include this file more than once.');
+}
+
+// Define that this is a thing which can double as a version check.
+define('kUtilities', '2.0.0b1');
+
+// --------------------------------------------------------------------------------------------------------------------
+
+// These constants must be defined because they might be used before gMtzRuntime is init'd
+if (!defined('kAppVendor'))             { define('kAppVendor', 'BinOC'); }
+if (!defined('kAppName'))               { define('kAppName', 'Metropolis-based Software'); }
+if (!defined('kAppVersion'))            { define('kAppVersion', kUtilities); }
+if (!defined('kAppRepository'))         { define('kAppRepository', '#'); }
+if (!defined('kUtilsGlobalWrappers'))   { define('kUtilsGlobalWrappers', true); }
+if (!defined('kBootstrap'))             { define('kBootstrap', true); }
+if (!defined('kDefaultSkinPath'))       { define('kDefaultSkinPath', './base/skin'); }
+
+// --------------------------------------------------------------------------------------------------------------------
+
+// We like CLI
+define('SAPI_IS_CLI', php_sapi_name() == "cli");
+define('CLI_NO_LOGO', in_array('--nologo', $GLOBALS['argv'] ?? []));
+
+// Enable PHP Error Reporting
+error_reporting(E_ALL);
+ini_set('display_errors', true);
+ini_set('display_startup_errors', true);
+const E_EXCEPTION = 65536;
+
+// Debug flag (CLI always triggers debug mode)
+define('kDebugMode', $_GET['debug'] ?? SAPI_IS_CLI);
+
+// --------------------------------------------------------------------------------------------------------------------
+
+// Define basic symbol constants
+const kNewLine              = "\n";
+const kCR                   = "\r";
+const kLF                   = "\n";
+const kTab                  = "\t";
+const kSpace                = " ";
+const kQuote                = '"';
+const kApostrophe           = "'";
+const kLessThan             = "<";
+const kGreaterThan          = ">";
+const kAmpersand            = "&";
+const kSlash                = "/";
+const kBackSlash            = "\\";
+const kEqual                = "=";
+const kDash                 = "-";
+const kPlus                 = "+";
+const kExclamation          = "!";
+const kSemicolon            = ";";
+const kHashsign             = "#";
+const kAsterisk             = "*";
+const kUnderbar             = "_";
+const kComma                = ",";
+const kLeftParen            = "(";
+const kRightParen           = ")";
+const kLeftBrace            = "{";
+const kRightBrace           = "}";
+const kQuestionMark         = "?";
+const kLeftSquareBracket    = "[";
+const kRightSquareBracket   = "]";
+const kNullCh               = "\0";
+
+const kForwardSlash         = kSlash;
+const kMinus                = kDash;
+
+const kDot                  = ".";
+const kPipe                 = "|";
+const kDollar               = "\$";
+const kColon                = ":";
+
+const kScopeOperator        = kColon . kColon;
+const kResolutionOperator   = kDash . kGreaterThan;
+const kDotDot               = kDot . kDot;
+const kSpaceDashSpace       = kSpace . kDash . kSpace;
+const kColonSlashSlash      = kColon . kSlash . kSlash;
+
+const kEmptyString          = "";
+const kEmptyArray           = [];
+
+const kNegOne               = -1;
+const kZero                 = 0;
+const kPosOne               = 1;
+
+// --------------------------------------------------------------------------------------------------------------------
+
+const kJsonPrettyEncode     = gMtzUtils::JSON_ENCODE_FLAGS['display'];
+
+// --------------------------------------------------------------------------------------------------------------------
+
+const kDefaultSkinName      = 'default';
+const kDefaultMenu          = ['/' => 'Front Page (Home)'];
+const kSpecialComponent     = 'special';
+const kSpecialComponentName = 'Special Component';
+
+const PALEMOON_GUID         = '{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}';
+
+// ====================================================================================================================
+
+
+
+// ====================================================================================================================
+
+// == | Static Application Class | ====================================================================================
+
+class gMtzRuntime {
+  private static $sInitialized = false;
+  private static $sRuntimeConfiguration = kEmptyArray;
+  public static $sModule = kEmptyArray;
+  const VIRTUAL_NODES         = ['constant', 'superglobal'];
+  
+  /********************************************************************************************************************
+  * Application Init
+  *********************************************************************************************************************/
+  public static function init() {
+    if (gMtzRuntime::$sInitialized) {
+      return gMtzRuntime::$sInitialized;
+    }
+
+    // ------------------------------------------------------------------------------------------------------------------
+
+    $path = gMtzUtils::ExplodePath(gMtzRuntime::SuperGlobal('get', 'path', kSlash));
+
+    if (defined('kLocalDomain') && SAPI_IS_CLI) {
+      $localDomain = kLocalDomain;
+    }
+
+    $config = array(
+      'app' => array(
+        'component'   => gMtzRuntime::SuperGlobal('get', 'component', 'site'),
+        'path'        => $path,
+        'depth'       => count($path ?? kEmptyArray),
+        'debug'       => kDebugMode,
+        'offline'     => file_exists(gMtzUtils::BuildPath(kRootPath, '.offline')),
+      ),
+      'network' => array(
+        'scheme'      => gMtzRuntime::SuperGlobal('server', 'SCHEME') ?? (gMtzRuntime::SuperGlobal('server', 'HTTPS') ? 'https' : 'http'),
+        'serverName'  => gMtzRuntime::SuperGlobal('server', 'SEVER_NAME', ($localDomain ?? 'localhost')),
+        'requestUri'  => gMtzRuntime::SuperGlobal('server', 'REQUEST_URI', kSlash),
+        'requestPath' => gMtzRuntime::SuperGlobal('get', 'path', kSlash),
+        'remoteAddr'  => gMtzRuntime::SuperGlobal('server', 'HTTP_X_FORWARDED_FOR', gMtzRuntime::SuperGlobal('server', 'REMOTE_ADDR', '127.0.0.1')),
+        'userAgent'   => gMtzRuntime::SuperGlobal('server', 'HTTP_USER_AGENT', 'php' . kDash . PHP_SAPI . kSlash . PHP_VERSION),
+        'baseDomain'  => gMtzConsole::GetDomain(gMtzRuntime::SuperGlobal('server', 'SERVER_NAME', ($localDomain ?? 'localhost'))),
+        'subDomain'   => gMtzConsole::GetDomain(gMtzRuntime::SuperGlobal('server', 'SERVER_NAME', ($localDomain ?? 'localhost')), true),
+      ),
+      'console' => array(
+        'output' => array(
+          'contentType' => 'text/plain',
+          'responseCode'  => 200,
+          'responseBody'  => kEmptyString,
+          'httpHeaders' => kEmptyArray,
+        ),
+        'ui' => array(
+          'mainmenu'      => kDefaultMenu,
+          'commandbar'    => kDefaultMenu,
+          'statustext'    => 'Done',
+        ),
+      ),
+    );
+
+    if (defined('kDebugDomain') && !SAPI_IS_CLI) {
+      $config['app']['debug'] = (gMtzRuntime::SuperGlobal('server', 'SERVER_NAME', 'localhost') != constant('kDebugDomain') ?? kEmptyString) ?
+                                file_exists(gMtzUtils::BuildPath(kRootPath, '.debugMode')) :
+                                !kDebugMode;
+    }
+
+    // ------------------------------------------------------------------------------------------------------------------
+
+    gMtzRuntime::$sRuntimeConfiguration = new \Adbar\Dot($config);
+
+    // ------------------------------------------------------------------------------------------------------------------
+    
+    // Set error handlers
+    gMtzConsole::EnableErrorHandler();
+
+    // ------------------------------------------------------------------------------------------------------------------
+
+    // These are global wrapping functions.
+    if (kUtilsGlobalWrappers) {
+      // Application
+      function gError                       (...$args) { return gMtzRuntime::Error(...$args); }
+      function gNotFound                    (...$args) { return gMtzRuntime::NotFound(...$args); }
+      function gReadFile                    (...$args) { return gMtzRuntime::ReadFile(...$args); }
+      function gGetConfig                   (...$args) { return gMtzRuntime::GetConfig(...$args); }
+      function gSetConfig                   (...$args) { return gMtzRuntime::SetConfig(...$args); }
+      function gEnsureValue                 (...$args) { return gMtzRuntime::EnsureValue(...$args); }
+      function gRegisterIncludes            (...$args) { return gMtzRuntime::RegisterIncludes(...$args); }
+      function gImportModules               (...$args) { return gMtzRuntime::ImportModules(...$args); }
+      function gLoadComponent               (...$args) { return gMtzRuntime::LoadComponent(...$args); }
+      function gRegistry                    (...$args) { return gMtzRuntime::GetConfig(...$args); }
+      function gRegSet                      (...$args) { return gMtzRuntime::SetConfig(...$args); }
+
+      // Application Utilities
+      function gSubst                       (...$args) { return gMtzUtils::Subst(...$args); }
+      function gSubstEx                     (...$args) { return gMtzUtils::SubstEx(...$args); }
+      function gContains                    (...$args) { return gMtzUtils::Contains(...$args); }
+      function gExplodeStr                  (...$args) { return gMtzUtils::ExplodeStr(...$args); }
+      function gBuildPath                   (...$args) { return gMtzUtils::BuildPath(...$args); }
+      function gStripStr                    (...$args) { return gMtzUtils::StripStr(...$args); }
+      function gHexString                   (...$args) { return gMtzUtils::HexString(...$args); }
+      function gPasswordHash                (...$args) { return gMtzUtils::PasswordHash(...$args); }
+      function gPasswordVerify              (...$args) { return gMtzUtils::PasswordVerify(...$args); }
+      function gGlobalIdentifer             (...$args) { return gMtzUtils::GlobalIdentifer(...$args); }
+
+      // Console
+      function gHeader                      (...$args) { return gMtzConsole::Header(...$args); }
+      function gContentType                 (...$args) { return gMtzConsole::ContentType(...$args); }
+      function gSendHeaders                 (...$args) { return gMtzConsole::SendHeaders(...$args); }
+      function gRedirect                    (...$args) { return gMtzConsole::Redirect(...$args); }
+      function gOutput                      (...$args) { return gMtzConsole::Output(...$args); }
+      function gContent                     (...$args) { return gMtzConsole::Content(...$args); }
+    }
+
+    // ------------------------------------------------------------------------------------------------------------------
+
+    gMtzRuntime::$sInitialized = true;
+
+    // This will bootstrap the Metropolis-based application loading. You may set kBoostrap to false in the entry point to
+    // completely bypass this execution and return to the entry point.
+    //
+    // Bootstrap has THREE modes of operation:
+    // App is Special "Component"     - Simply forces the application to always load the Special "Component".
+    // App is ./base/src/main.php     - In this mode we will load the application from this file and if execution isn't
+    //                                  otherwise terminated it will terminate here with a 404/PC LOAD LETTER.
+    // App is Entry point             - Return to the entry point but allow for the possibility of switching to one of
+    //                                  the other modes without adding or removing kBootstrap
+    //
+    // NOTE: Future behavior will have kBootstrap set to false by default.. like in a few weeks if not sooner.
+    if (kBootstrap) {
+      // The application effectively IS the special component
+      if (gMtzRuntime::GetConfig('constant.appIsSpecialComponent')) {
+        gMtzRuntime::LoadComponent(kSpecialComponent);
+      }
+
+      // App is ./base/src/app.php
+      if (file_exists(gMtzUtils::BuildPath(kRootPath, 'base', 'src', 'main.php'))) {
+        require_once(gMtzUtils::BuildPath(kRootPath, 'base', 'src', 'main.php'));
+
+        if (gMtzRuntime::GetConfig('app.path.0') == kSpecialComponent) {
+          gMtzRuntime::SetConfig('app.component', kSpecialComponent);
+        }
+
+        gMtzRuntime::LoadComponent(gMtzRuntime::GetConfig('app.component'));
+        gMtzRuntime::NotFound('PC LOAD LETTER');
+      }
+    }
+  }
+
+  /********************************************************************************************************************
+  * Set app config key
+  *********************************************************************************************************************/
+  public static function GetConfig(string $aDottedKey, mixed $aFallback = null) {
+    $rv = null;
+
+    if (gMtzUtils::Contains($aDottedKey, gMtzRuntime::VIRTUAL_NODES, gMtzUtils::STARTS_WITH)) {
+      $keyNodes = gMtzUtils::ExplodeStr(kDot, $aDottedKey) ?? kEmptyArray;
+      $firstNode = $keyNodes[array_key_first($keyNodes)] ?? kEmptyString;
+      $dot = new \Adbar\Dot();
+      switch($firstNode) {
+        case 'constant':
+          if (count($keyNodes) < 2) { return $aFallback; }
+
+          $ucConst = strtoupper($keyNodes[1]);
+          $prefixConst = 'k' . ucfirst($keyNodes[1]);
+
+          switch (true) {
+            case defined($prefixConst):
+              $rv = constant($prefixConst);
+              break;
+            case defined($ucConst):
+              $rv = constant($ucConst);
+              break;
+            case defined($keyNodes[1]):
+              $rv = constant($keyNodes[1]);
+              break;
+            default:
+              return null;
+          }
+
+          if (!(is_array($rv) || $rv instanceof ArrayAccess)) { return $rv ?? $aFallback; }
+          unset($keyNodes[0], $keyNodes[1]);
+          $rv = $dot->setArray($rv)->get(implode(kDot, $keyNodes), $aFallback);
+          break;
+        case 'superglobal':
+          if (count($keyNodes) < 3) { return $aFallback; }
+          $rv = gMtzRuntime::SuperGlobal($keyNodes[1], $keyNodes[2]);
+          if (!(is_array($rv) || $rv instanceof ArrayAccess)) { return $rv ?? $aFallback; }
+          unset($keyNodes[0], $keyNodes[1]);
+          $rv = $dot->setArray($rv)->get(implode(kDot, $keyNodes), $aFallback);
+          break;
+        default:
+          return null;
+      }
+    }
+    else { $rv = gMtzRuntime::$sRuntimeConfiguration->get($aDottedKey, $aFallback); }
+
+    return $rv; 
+  }
+
+  /********************************************************************************************************************
+  * Set app config key
+  *********************************************************************************************************************/
+  public static function SetConfig(string $aDottedKey, mixed $aNewValue) {
+    $keyNodes = gMtzUtils::ExplodeStr(kDot, $aDottedKey) ?? kEmptyArray;
+    $firstNode = $keyNodes[array_key_first($keyNodes)] ?? kEmptyString;
+    $lastNode = $keyNodes[array_key_last($keyNodes)] ?? kEmptyString;
+
+    if (gMtzUtils::Contains(gMtzRuntime::VIRTUAL_NODES, $firstNode, gMtzUtils::IN_ARRAY)) {
+      gMtzRuntime::Error('Setting values on virtual nodes is not supported.');
+    }
+
+    if (gMtzUtils::Contains($lastNode, '[]', gMtzUtils::ENDS_WITH)) {
+      $aDottedKey = substr($aDottedKey, 0, -2);
+      
+      $oldValue = gMtzRuntime::$sRuntimeConfiguration->get($aDottedKey, kEmptyArray);
+
+      if ($oldValue && !is_array($oldValue)) {
+        $oldValue = [$oldValue];
+      }
+
+      $oldValue[] = $aNewValue;
+      $aNewValue = $oldValue;
+    }
+    else if (is_numeric($lastNode)) {
+      array_pop($keyNodes);
+      $truncatedDottedKey = implode(kDot, $keyNodes);
+      $oldValue = gMtzRuntime::$sRuntimeConfiguration->get(implode(kDot, $keyNodes));
+
+      if (!$oldValue || !array_is_list($oldValue)) {
+        gMtzRuntime::Error('Using indexed keys can only be done if the value is already an indexed list.');
+      }
+
+      $oldValue[(int)$lastNode] = $aNewValue;
+      $aNewValue = $oldValue;
+      $aDottedKey = $truncatedDottedKey;
+    }
+
+    return gMtzRuntime::$sRuntimeConfiguration->set($aDottedKey, $aNewValue);
+  }
+
+  /********************************************************************************************************************
+  * Loads a component.
+  *********************************************************************************************************************/
+  public static function LoadComponent(string $aComponent) {
+    if ($aComponent == kSpecialComponent) {
+      gMtzRuntime::SpecialComponent();
+    }
+
+    $componentPath = gMtzRuntime::GetConfig('constant.components' . kDot . $aComponent);
+
+    if (!$componentPath) {
+      gMtzRuntime::NotFound('Unknown component.');
+    }
+
+    if (!file_exists($componentPath)) {
+      gMtzRuntime::NotFound('Failed to load the' . kSpace . $aComponent . kSpace .'component.');
+    }
+
+    gMtzRuntime::SetConfig('app.componentPath', gMtzUtils::BuildPath(kRootPath, 'components', $aComponent));
+    require_once($componentPath);
+  }
+
+  /********************************************************************************************************************
+  * Read a file
+  *********************************************************************************************************************/
+  public static function ReadFile(string $aFile) {
+    $rv = @file_get_contents($aFile);
+    return gMtzRuntime::EnsureValue($rv);
+  }
+
+  /********************************************************************************************************************
+  * General Error Function
+  *
+  * @param $aMessage   Error message
+  ********************************************************************************************************************/
+  public static function Error(mixed $aMessage = null) {
+    if (!$aMessage) {
+      $aMessage = 'No further details were provided.';
+    }
+
+    if (!is_string($aMessage)) {
+      gMtzConsole::Output($aMessage);
+    }
+
+    gMtzConsole::Report(['code' => E_ALL, 'message' => $aMessage,
+                         'file' => null, 'line' => null,
+                         'trace' => debug_backtrace(2)]);
+  }
+
+  /********************************************************************************************************************
+  * Sends 404 or prints error message if debug mode
+  ********************************************************************************************************************/
+  public static function NotFound(?string $aMessage = null) {
+    if (!$aMessage) {
+      $aMessage = 'HTTP/1.1 404 Not Found';
+    }
+
+    if (gMtzRuntime::Debug()) {
+      gMtzConsole::Report(['code' => E_ALL, 'message' => $aMessage,
+                           'file' => null, 'line' => null,
+                           'trace' => debug_backtrace(2)]);
+    }
+    gMtzConsole::Header(404);
+  }
+
+  /********************************************************************************************************************
+  * Check if a value should be null
+  ********************************************************************************************************************/
+  public static function EnsureValue($aValue, $aFallback = null) {
+    return (empty($aValue) || $aValue === 'none') ? $aFallback : $aValue;
+  }
+
+  /**********************************************************************************************************************
+  * Special Component!
+  ***********************************************************************************************************************/
+  public static function SpecialComponent() {
+    $spCurrentPath = gMtzRuntime::GetConfig('app.path');
+    $spPathCount = gMtzRuntime::GetConfig('app.depth');
+
+    if ($spCurrentPath[0] != kSpecialComponent) {
+      gMtzConsole::Redirect(kSlash . kSpecialComponent . kSlash);
+    }
+
+    gMtzRuntime::SetConfig('app.component', kSpecialComponent);
+
+    if (gMtzRuntime::GetConfig('constant.disableSpecialComponent')) {
+      gMtzRuntime::NotFound('The special component has been disabled.');
+    }
+
+    gMtzRuntime::SetConfig('console.ui.sectionName', kSpecialComponentName);
+
+    // The Special Component never has more than one level below it
+    // We still have to determine the root of the component though...
+    if ($spPathCount == 1) {
+      // URL /special/
+      $spSpecialFunction = 'root';
+    }
+    else {
+      // URL /special/xxx/
+      if ($spPathCount > 2) {
+        gMtzRuntime::NotFound('The special component only has one path level.');
+      }
+      $spSpecialFunction = $spCurrentPath[1];
+    }
+
+    $spCommandBar = array(
+      '/special/'                 => kSpecialComponentName,
+      '/special/test/'            => 'Test Cases',
+      '/special/guid/'            => 'GUID',
+      '/special/hex/'             => 'Hex String',
+    );
+
+    gMtzRuntime::SetConfig('console.ui.commandbar', gMtzRuntime::GetConfig('constant.components.site') ?
+                                               array_merge(kDefaultMenu, $spCommandBar) :
+                                               $spCommandBar);
+
+    unset($spCurrentPath, $spPathCount, $spCommandBar);
+
+    switch ($spSpecialFunction) {
+      case 'root':
+        $spContent = '<h2>Welcome</h2>' .
+                     '<p>Please select a special function from the command bar above.';
+        gMtzConsole::Content($spContent, ['title' => 'Overview']);
+        break;
+      case 'test':
+        if (!gMtzRuntime::Debug()) {
+          gMtzRuntime::NotFound('This special function is not available when not in debug mode.');
+        }
+        $spCase = gMtzRuntime::GetConfig('superglobal.get.case');
+        $spTestsPath = gMtzUtils::BuildPath(kRootPath, 'base', 'tests');
+        $spGlobTests = glob(gMtzUtils::BuildPath($spTestsPath, kAsterisk . gMtzUtils::FILE_EXT['php']));
+        $spTests = kEmptyArray;
+
+        foreach ($spGlobTests as $_value) {
+          $spTests[] = gMtzUtils::Subst($_value, [gMtzUtils::FILE_EXT['php'] => kEmptyString, $spTestsPath . kSlash => kEmptyString]);
+        }
+
+        if ($spCase) {
+          if (!gMtzUtils::Contains($spCase, $spTests)) {
+            gMtzRuntime::Error('Unknown test case.');
+          }
+
+          gMtzRuntime::SetConfig('special.testCase', $spCase);
+          require_once(gMtzUtils::BuildPath($spTestsPath, $spCase . gMtzUtils::FILE_EXT['php']));
+          headers_sent() ? exit() : gMtzRuntime::Error('The operation completed successfully.');
+        }
+
+        $spContent = kEmptyString;
+
+        foreach ($spTests as $_value) {
+          $spContent .= '<li><a href="/special/test/?case=' . $_value . '">' . $_value . '</a></li>';
+        }
+
+        $spContent = ($spContent == kEmptyString) ?
+                     '<p>There are no test cases.</p>' :
+                     '<h2>Please select a test case&hellip;</h2><ul>' . $spContent . '</ul>' . str_repeat('<br />', 3);
+
+        gMtzConsole::Content($spContent, ['title' => 'Test Cases']);
+        break;
+      case 'guid':
+        gMtzConsole::Content(gMtzUtils::GlobalIdentifer(gMtzRuntime::GetConfig('superglobal.get.vendor'), true),
+                 ['title' => 'Globally Unique Identifier (In XPIDL Notation)', 'textbox' => true]);
+        break;
+      case 'hex':
+        gMtzConsole::Content(gMtzUtils::HexString(gMtzRuntime::GetConfig('superglobal.get.length', 40)),
+                 ['title' => 'Pseudo-Random Hex String', 'textbox' => true]);
+        break;
+      case 'system':
+        if (!gMtzRuntime::Debug()) {
+          gMtzRuntime::NotFound('This special function is not available when not in debug mode.');
+        }
+        gMtzRuntime::Header('html', true);
+        phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_ENVIRONMENT | INFO_VARIABLES);
+        break;
+      default:
+        gMtzRuntime::NotFound('There is no matching function in the special component.');
+    }
+
+    // We're done here
+    exit();
+  }
+  
+  /********************************************************************************************************************
+  * Access Super Globals
+  ********************************************************************************************************************/
+  public static function SuperGlobal($aNode, $aKey, $aDefault = null) {
+    $rv = null;
+
+    // Turn the variable type into all caps prefixed with an underscore
+    $aNode = kUnderbar . strtoupper($aNode);
+
+    // This handles the superglobals
+    switch($aNode) {
+      case '_CHECK':
+        $rv = gMtzRuntime::EnsureValue($aKey);
+        break;
+      case '_GET':
+        if (SAPI_IS_CLI && $GLOBALS['argc'] > 1) {
+          $args = kEmptyArray;
+
+          foreach (array_slice($GLOBALS['argv'], 1) as $_value) {
+            $arg = @explode('=', $_value);
+
+            if (count($arg) < 2) {
+              continue;
+            }
+
+            $attr = str_replace('--', kEmptyString, $arg[0]);
+            $val = gMtzRuntime::EnsureValue(str_replace('"', kEmptyString, $arg[1]));
+
+            if (!$attr && !$val) {
+              continue;
+            }
+
+            $args[$attr] = $val;
+          }
+
+          $rv = $args[$aKey] ?? $aDefault;
+          break;
+        }
+      case '_SERVER':
+      case '_ENV':
+      case '_FILES':
+      case '_POST':
+      case '_COOKIE':
+      case '_SESSION':
+        $rv = $GLOBALS[$aNode][$aKey] ?? $aDefault;
+        break;
+      default:
+        // We don't know WHAT was requested but it is obviously wrong...
+        gMtzRuntime::Error('Unknown system node.');
+    }
+    
+    // We always pass $_GET values through a general regular expression
+    // This allows only a-z A-Z 0-9 - / { } @ % whitespace and ,
+    if ($rv && $aNode == "_GET") {
+      $rv = preg_replace(gMtzUtils::REGEX_PATTERNS['query'], kEmptyString, $rv);
+    }
+
+    // Files need special handling.. In principle we hard fail if it is anything other than
+    // OK or NO FILE
+    if ($rv && $aNode == "_FILES") {
+      if (!in_array($rv['error'], [UPLOAD_ERR_OK, UPLOAD_ERR_NO_FILE])) {
+        gMtzRuntime::Error('Upload of ' . $aKey . ' failed with error code: ' . $rv['error']);
+      }
+
+      // No file is handled as merely being null
+      if ($rv['error'] == UPLOAD_ERR_NO_FILE) {
+        return null;
+      }
+
+      // Cursory check the actual mime-type and replace whatever the web client sent
+      $rv['type'] = mime_content_type($rv['tmp_name']);
+    }
+
+    return $rv;
+  }
+
+  /********************************************************************************************************************
+  * Registers Files to be included such as components and modules
+  *********************************************************************************************************************/
+  public static function RegisterIncludes($aConst, $aIncludes) {
+    $aConst = strtoupper($aConst);
+
+    if (defined($aConst)) {
+      gMtzRuntime::Error($aConst . kSpace . 'files are already registered and may not be updated.');
+    }
+
+    $includes = kEmptyArray;
+
+    foreach($aIncludes as $_key => $_value) { 
+      switch ($aConst) {
+        case 'COMPONENTS':
+          $includes[$_value] = gMtzUtils::BuildPath(kRootPath, 'components', $_value, 'src', 'main' . gMtzUtils::FILE_EXT['php']);
+          break;
+        case 'MODULES':
+          $includes[$_value] = gMtzUtils::BuildPath(kRootPath, 'modules', 'mtz' . ucfirst($_value) . gMtzUtils::FILE_EXT['php']);
+          break;
+        case 'LIBRARIES':
+          if (str_contains($_value, kDot . kDot)) {
+            return;
+          }
+
+          $includes[$_key] = gMtzUtils::BuildPath(kRootPath, 'third_party', $_value);
+          break;
+        default:
+          gError('Unknown include type');
+      }
+    }
+
+    define($aConst, $includes);
+  }
+
+  /**********************************************************************************************************************
+  * Includes a module
+  **********************************************************************************************************************/
+  public static function ImportModules(...$aModules) {
+    if (!defined('MODULES')) {
+      funcError('MODULES is not defined');
+    }
+
+    foreach ($aModules as $_value) {
+      if (!array_key_exists($_value, MODULES)) {
+        gError('Unable to import unknown module ' . $_value);
+      }
+
+      $className = 'mtz' . ucfirst($_value);
+      $moduleName = $_value;
+   
+      if (array_key_exists($moduleName, gMtzRuntime::$sModule)) {
+        gError('Module ' . $_value . ' has already been imported');
+      }
+
+      require(MODULES[$_value]);
+      gMtzRuntime::$sModule[$moduleName] = new $className();
+    }
+  }
+
+  /********************************************************************************************************************
+  * Get the registry property and return it
+  ********************************************************************************************************************/
+  public static function Component(?string $aCompareComponent = null) {
+    $rv = (gMtzRuntime::$sRuntimeConfiguration) ? gMtzRuntime::GetConfig('app.component') : gMtzRuntime::SuperGlobal('get', 'component', 'site');
+
+    if ($aCompareComponent) {
+      $rv = ($rv === $aCompareComponent);
+    }
+
+    return $rv;
+  }
+
+  /********************************************************************************************************************
+  * Get the registry property and return it
+  ********************************************************************************************************************/
+  public static function Debug() {
+    return (gMtzRuntime::$sRuntimeConfiguration) ? gMtzRuntime::GetConfig('app.debug') : kDebugMode;
+  }
+
+  /********************************************************************************************************************
+  * Get the registry property and return it
+  ********************************************************************************************************************/
+  public static function GetStore() {
+    return gMtzRuntime::$sRuntimeConfiguration->get();
+  }
+}
+
+// ====================================================================================================================
+
+// == | Application Utilities | =======================================================================================
+
+class gMtzUtils {
+  const CONTAINS = kZero;
+  const STARTS_WITH = kPosOne;
+  const ENDS_WITH = kNegOne;
+  const IN_ARRAY = kZero;
+  const KEY_EXISTS = kPosOne;
+
+  // --------------------------------------------------------------------------------------------------------------------
+
+  const PASSWORD_CLEARTEXT    = "clrtxt";
+  const PASSWORD_HTACCESS     = "apr1";
+  const BASE64_ALPHABET       = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+  const APRMD5_ALPHABET       = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+  // --------------------------------------------------------------------------------------------------------------------
+
+  const FILE_WRITE_FLAGS      = "w+";
+  const FILE_EXT             = array(
+    'php'                     => kDot . 'php',
+    'ini'                     => kDot . 'ini',
+    'html'                    => kDot . 'html',
+    'xhtml'                   => kDot . 'xhtml',
+    'xml'                     => kDot . 'xml',
+    'rdf'                     => kDot . 'rdf',
+    'json'                    => kDot . 'json',
+    'content'                 => kDot . 'content',
+    'tpl'                     => kDot . 'tpl',
+    'xpinstall'               => kDot . 'xpi',
+    'jar'                     => kDot . 'jar',
+    'winstaller'              => kDot . 'installer' . kDot . 'exe',
+    'winportable'             => kDot . 'portable' . kDot . 'exe',
+    'mar'                     => kDot . 'mar',
+    'mar-bz2'                 => kDot . 'complete' . kDot . 'mar',
+    '7z'                      => kDot . '7z',
+    'zip'                     => kDot . 'zip',
+    'tz'                      => kDot . 'tz',
+    't7z'                     => kDot . 't7z',
+    'tgz'                     => kDot . 'tgz',
+    'tbz'                     => kDot . 'tbz',
+    'txz'                     => kDot . 'txz',
+    'tar-zip'                 => kDot . 'tar' . kDot . 'zip',
+    'tar-7z'                  => kDot . 'tar' . kDot . '7z',
+    'tar-gz'                  => kDot . 'tar' . kDot . 'gz',
+    'tar-bz2'                 => kDot . 'tar' . kDot . 'bz2',
+    'tar-xz'                  => kDot . 'tar' . kDot . 'xz', 
+  );
+
+  // ----------------------------------------------------------------------------------------------------------------
+
+  const TAG_XML               = '<?xml version="1.0" encoding="utf-8" ?>';
+
+  // ----------------------------------------------------------------------------------------------------------------
+
+  const JSON_ENCODE_FLAGS            = array(
+    'display'                 => JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
+    'storage'                 => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
+  );
+
+  // ----------------------------------------------------------------------------------------------------------------
+
+  const REGEX_PATTERNS        = array(
+    'query'                   => "/[^-a-zA-Z0-9_\-\/\{\}\@\.\%\s\,]/",
+    'yaml'                    => "/\A---(.|\n)*?---/",
+    'guid'                    => "/^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/i",
+    'host'                    => "/[a-z0-9-\._]+\@[a-z0-9-\._]+/i",
+  );
+
+  // ------------------------------------------------------------------------------------------------------------------
+
+  /**********************************************************************************************************************
+  * gMtzUtils::Subst
+  ***********************************************************************************************************************/
+  public static function Subst(string $aString, array $aSubsts, bool $aRegEx = false) {
+    $rv = $aString;
+    $replaceFunction = $aRegEx ? 'preg_replace' : 'str_replace';
+
+    foreach ($aSubsts as $_key => $_value) {
+      $rv = call_user_func($replaceFunction, ($aRegEx ? kSlash . $_key . kSlash . 'iU' : $_key), $_value, $rv);
+    }
+
+    return !$rv ? gMtzRuntime::Error('Something has gone wrong...') : $rv;
+  } 
+    
+  /**********************************************************************************************************************
+  * gMtzUtils::SubstEx
+  ***********************************************************************************************************************/
+  public static function SubstEx(string $aString, ...$aSubsts) {
+    $keyPrefix = kLeftBrace;
+    $keySuffix = kRightBrace;
+    $substs = $aSubsts;
+
+    // Accept an array as the second argument for named placeholder substitution further arguments or
+    // changing the pre/post prefix.
+    if (is_array($aSubsts[0])) {
+      $substs = $aSubsts[0];
+      $keyPrefix = $aSubsts[1] ?? kLeftBrace;
+      $keySuffix = $aSubsts[2] ?? kRightBrace;
+    }
+
+    $keyCallback = function ($aKey) use ($keyPrefix, $keySuffix) { return $keyPrefix . $aKey . $keySuffix; };
+    $substs = array_combine(array_map($keyCallback, array_keys($substs)), $substs);
+
+    return gMtzUtils::Subst($aString, $substs);
+  }
+
+  /**********************************************************************************************************************
+  * gMtzUtils::Contains
+  ***********************************************************************************************************************/
+  public static function Contains(string|array $aHaystack, string|array $aNeedle, int $aPos = kZero) {
+    $rv = false;
+    if (is_string($aNeedle)) {
+      $aNeedle = [$aNeedle];
+    }
+
+    foreach ($aNeedle as $_value) {
+      if (is_array($aHaystack)) {
+        $rv = ($aPos === 1) ? array_key_exists($_value, $aHaystack) : in_array($_value, $aHaystack);
+      }
+      else {
+        switch ($aPos) {
+          case gMtzUtils::STARTS_WITH:
+            $rv = str_starts_with($aHaystack, $_value);
+            break;
+          case gMtzUtils::ENDS_WITH:
+            $rv = str_ends_with($aHaystack, $_value);
+            break;
+          case gMtzUtils::CONTAINS:
+          default:
+            $rv = str_contains($aHaystack, $_value);
+        }
+      }
+
+      if ($rv) {
+        break;
+      }
+    }
+
+    return $rv;
+  }
+
+  /**********************************************************************************************************************
+  * gMtzUtils::ExplodeStr
+  ***********************************************************************************************************************/
+  public static function ExplodeStr(string $aSeparator, string $aString) {
+    return (!str_contains($aString, $aSeparator)) ? [$aString] :
+            array_values(array_filter(explode($aSeparator, $aString), 'strlen'));
+  }
+
+  /**********************************************************************************************************************
+  * gExplodePath
+  ***********************************************************************************************************************/
+  public static function ExplodePath(string $aPath) {
+    return ($aPath == kSlash) ? ['root'] : gMtzUtils::ExplodeStr(kSlash, $aPath);
+  }
+
+  /**********************************************************************************************************************
+  * gMtzUtils::BuildPath
+  ***********************************************************************************************************************/
+  public static function BuildPath(...$aParts) {
+    $parts = kEmptyArray;
+    $path = strtr(implode(kSlash, $aParts), '\\', kSlash);
+    $prefix = kEmptyString;
+    $absolute = false;
+
+    // extract a prefix being a protocol://, protocol:, protocol://drive: or simply drive:
+    if (preg_match('{^( [0-9a-z]{2,}+: (?: // (?: [a-z]: )? )? | [a-z]: )}ix', $path, $match)) {
+      $prefix = $match[1];
+      $path = substr($path, strlen($prefix));
+    }
+
+    if (substr($path, 0, 1) === kSlash) {
+      $absolute = true;
+      $path = substr($path, 1);
+    }
+
+    $up = false;
+
+    foreach (explode(kSlash, $path) as $chunk) {
+      if (kDotDot === $chunk && ($absolute || $up)) {
+        array_pop($parts);
+        $up = !(empty($parts) || kDotDot === end($parts));
+      }
+      elseif (kDot !== $chunk && kEmptyString !== $chunk) {
+        $parts[] = $chunk;
+        $up = kDotDot !== $chunk;
+      }
+    }
+
+    return $prefix . ($absolute ? kSlash : kEmptyString) . implode(kSlash, $parts);
+  }
+
+  /**********************************************************************************************************************
+  * gMtzUtils::StripStr
+  ***********************************************************************************************************************/
+  public static function StripStr (string $aStr, string $aStrip = kEmptyString) {
+    return str_replace($aStrip, kEmptyString, $aStr);
+  }
+  /**********************************************************************************************************************
+  * Generate a random hexadecimal string
+  *
+  * @param $aLength   Desired number of final chars
+  * @returns          Random hexadecimal string of desired length
+  **********************************************************************************************************************/
+  public static function HexString(int $aLength = 40) {
+    return bin2hex(random_bytes(($aLength <= 1) ? 1 : (int)($aLength / 2)));
+  }
+
+  /********************************************************************************************************************
+  * Hash a password
+  *********************************************************************************************************************/
+  public static function PasswordHash(string $aPassword, mixed $aCrypt = PASSWORD_BCRYPT, ?string $aSalt = null) {
+    switch ($aCrypt) {
+      case gMtzUtils::PASSWORD_CLEARTEXT:
+        // We can "hash" a cleartext password by prefixing it with the fake algo prefix $clear$
+        if (str_contains($aPassword, kDollar)) {
+          // Since the dollar sign is used as an identifier and/or separator for hashes we can't use passwords
+          // that contain said dollar sign.
+          gMtzRuntime::Error('Cannot "hash" this Clear Text password because it contains a dollar sign.');
+        }
+
+        return kDollar . gMtzUtils::PASSWORD_CLEARTEXT . kDollar . time() . kDollar . $aPassword;
+      case gMtzUtils::PASSWORD_HTACCESS:
+        // We want to be able to generate Apache APR1-MD5 hashes for use in .htpasswd situations.
+        $salt = $aSalt;
+
+        if (!$salt) {
+          $salt = kEmptyString;
+
+          for ($i = 0; $i < 8; $i++) {
+            $offset = hexdec(bin2hex(openssl_random_pseudo_bytes(1))) % 64;
+            $salt .= gMtzUtils::APRMD5_ALPHABET[$offset];
+          }
+        }
+
+        $salt = substr($salt, 0, 8);
+        $max = strlen($aPassword);
+        $context = $aPassword . kDollar . gMtzUtils::PASSWORD_HTACCESS . kDollar . $salt;
+        $binary = pack('H32', md5($aPassword . $salt . $aPassword));
+
+        for ($i = $max; $i > 0; $i -= 16) {
+          $context .= substr($binary, 0, min(16, $i));
+        }
+
+        for ($i = $max; $i > 0; $i >>= 1) {
+          $context .= ($i & 1) ? chr(0) : $aPassword[0];
+        }
+
+        $binary = pack('H32', md5($context));
+
+        for ($i = 0; $i < 1000; $i++) {
+          $new = ($i & 1) ? $aPassword : $binary;
+
+          if ($i % 3) {
+            $new .= $salt;
+          }
+          if ($i % 7) {
+            $new .= $aPassword;
+          }
+
+          $new .= ($i & 1) ? $binary : $aPassword;
+          $binary = pack('H32', md5($new));
+        }
+
+        $hash = kEmptyString;
+
+        for ($i = 0; $i < 5; $i++) {
+          $k = $i + 6;
+          $j = $i + 12;
+          if($j == 16) $j = 5;
+          $hash = $binary[$i] . $binary[$k] . $binary[$j] . $hash;
+        }
+
+        $hash = chr(0) . chr(0) . $binary[11] . $hash;
+        $hash = strtr(strrev(substr(base64_encode($hash), 2)), gMtzUtils::BASE64_ALPHABET, gMtzUtils::APRMD5_ALPHABET);
+
+        return kDollar . gMtzUtils::PASSWORD_HTACCESS . kDollar . $salt . kDollar . $hash;
+      default:
+        // Else, our standard (and secure) default is PASSWORD_BCRYPT hashing.
+        // We do not allow custom salts for anything using password_hash as PHP generates secure salts.
+        // PHP Generated passwords are also self-verifiable via password_verify.
+        return password_hash($aPassword, $aCrypt);
+    }
+  }
+
+  /********************************************************************************************************************
+  * Check a password
+  *********************************************************************************************************************/
+  public static function PasswordVerify(string $aPassword, string $aHash) {
+    // We can accept a pseudo-hash for clear text passwords in the format of $clrtxt$unix-epoch$clear-text-password
+    if (str_starts_with($aHash, kDollar . gMtzUtils::PASSWORD_CLEARTEXT)) {
+      $password = gMtzUtils::ExplodeStr(kDollar, $aHash) ?? null;
+
+      if ($password == null || count($password) > 3) {
+        gMtzRuntime::Error('Unable to "verify" this Clear Text "hashed" password.');
+      }
+
+      return $aPassword === $password[2];
+    }
+
+    // We can also accept an Apache APR1-MD5 password that is commonly used in .htpasswd
+    if (str_starts_with($aHash, kDollar . gMtzUtils::PASSWORD_HTACCESS)) {
+      $salt = gMtzUtils::ExplodeStr(kDollar, $aHash)[1] ?? null;
+
+      if(!$salt) {
+        gMtzRuntime::Error('Unable to verify this Apache APR1-MD5 hashed password.');
+      }
+
+      return gMtzUtils::PasswordHash($aPassword, gMtzUtils::PASSWORD_HTACCESS, $salt) === $aHash;
+    }
+
+    // For everything else send to the native password_verify function.
+    // It is almost certain to be a BCRYPT2 hash but hashed passwords generated BY PHP are self-verifiable.
+    return password_verify($aPassword, $aHash);
+  }
+
+  /********************************************************************************************************************
+  * Generates a v4 random guid or a "v4bis" guid with static vendor node
+  *********************************************************************************************************************/
+  public static function GlobalIdentifer(?string $aVendor = null, ?bool $aXPCOM = null) {
+    if ($aVendor) {
+      if (strlen($aVendor) < 3) {
+        gMtzRuntime::Error('v4bis GUIDs require a defined vendor of more than three characters long.');
+      }
+
+      // Generate 8 pseudo-random bytes
+      $bytes = random_bytes(8);
+
+      // Knock the vendor down to lowercase so we can simply use a switch case
+      $vendor = strtolower($aVendor);
+
+      // We want "v4bis" GUIDs with the static vendor part to match the broken version of GUIDGenX for known nodes
+      // as Moonchild half-assed his tool that he wrote for this and by the time it was discovered several were already
+      // using the previous incorrect GUIDs.
+      $knownVendorNodes = array(
+        'binoc'           => hex2bin('8b97957ad5f8ea47'),
+        'binoc-legacy'    => hex2bin('9aa0aa0e607640b9'),
+        'mcp'             => hex2bin('bfc5fc555c87dbc4'),
+        'lootyhoof'       => hex2bin('b98e98e62085837f'),
+        'pseudo-static'   => hex2bin('93763763d1ad1978')
+      );
+
+      switch ($vendor) {
+        case 'binoc':
+        case 'binaryoutcast':
+        case 'binary outcast':
+          $bytes .= $knownVendorNodes['binoc'];
+          break;
+        case 'pseudo-static':
+        case 'pseudostatic':
+        case 'addons':
+        case 'add-ons':
+        case 'apmo':
+          $bytes .= $knownVendorNodes['pseudo-static'];
+          break;
+        case 'mcp':
+        case 'moonchildproductions':
+        case 'moonchild productions':
+          $bytes .= $knownVendorNodes['mcp'];
+          break;
+        case 'binoc-legacy':
+        case 'lootyhoof':
+          $bytes .= $knownVendorNodes[$vendor];
+          break;
+        default:
+          // Since this isn't a known busted vendor node then we should generate it ourselves.
+          // This matches the fixed version of GUIDGenX 1.1 which is to md5 hash the vendor string then
+          // split it in half and XOR the two parts for the final value
+
+          // XXXTobin: A future update could do a sha256 by halving it and again then XORing the two sets
+          // and XORing the final two results.
+          $vendor = hash('md5', $aVendor);
+          $bytes .= hex2bin(substr($vendor, 0, 16)) ^ hex2bin(substr($vendor, 16, 32));
+      }
+    }
+    else {
+      // This is a pure v4 UUID spec which is 16 pseudo-random bytes.
+      $bytes = random_bytes(16);
+    }
+
+    // Set the version and variant
+    // NOTE: Like everything Moonzilla does, he did not set the variant value when he initially came up with "v4bis"
+    // putting a busted generator into production use for the whole team. Sad!
+    $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
+    $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
+
+    $hex = bin2hex($bytes);
+    $guid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($hex, 4));
+
+    // We want the GUID in XPIDL/C++ Header notation
+    if ($aXPCOM) {
+      $explode = gMtzUtils::ExplodeStr(kDash, $guid);
+      $rv = "%{C++" . kNewLine . "//" . kSpace . kLeftBrace . $guid . kRightBrace . kNewLine .
+            "#define NS_CHANGE_ME_IID" . kSpace . 
+            vsprintf("{ 0x%s, 0x%s, 0x%s, { 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s } }",
+                     [$explode[0], $explode[1], $explode[2],
+                      substr($explode[3], 0, 2), substr($explode[3], 2, 2),
+                      substr($explode[4], 0, 2), substr($explode[4], 2, 2), substr($explode[4], 4, 2),
+                      substr($explode[4], 6, 2), substr($explode[4], 8, 2), substr($explode[4], 10, 2)]) . kNewLine .
+            "%}";
+    }
+    else {
+      // We like Microsoft GUID notation not UUID which means Lisa needs braces.. I mean the GUID.
+      $rv = '{' . $guid . '}';
+    }
+
+    return $rv;
+  }
+}
+
+// == | Static Output Class | =========================================================================================
+
+class gMtzConsole {
+  const PHP_ERROR_CODES = array(
+    E_ERROR                   => 'System Error',
+    E_WARNING                 => 'System Warning',
+    E_PARSE                   => 'System Error (Parser)',
+    E_NOTICE                  => 'System Notice',
+    E_CORE_ERROR              => 'System Error (Core)',
+    E_CORE_WARNING            => 'System Warning (Core)',
+    E_COMPILE_ERROR           => 'System Error (Compiler)',
+    E_COMPILE_WARNING         => 'System Warning (Compiler)',
+    E_USER_ERROR              => 'System Error (Application)',
+    E_USER_WARNING            => 'System Warning (Application)',
+    E_USER_NOTICE             => 'System Notice (Application)',
+    E_STRICT                  => 'System Error (Strict)',
+    E_RECOVERABLE_ERROR       => 'System Error (Recoverable)',
+    E_DEPRECATED              => 'System Deprecation',
+    E_USER_DEPRECATED         => 'System Deprecation (Application)',
+    E_ALL                     => 'Unable to Comply',
+    E_EXCEPTION               => 'System Exception',
+  );
+
+  const HTTP_HEADERS = array(
+    404                       => 'HTTP/1.1 404 Not Found',
+    501                       => 'HTTP/1.1 501 Not Implemented',
+  );
+
+  const HTTP_STATUS_CODE = array(
+    200       => 'OK',
+    204       => 'No Content',
+    400       => 'Bad Request',
+    401       => 'Unauthorized',
+    402       => 'Payment Required',
+    403       => 'Forbidden',
+    404       => 'Not Found',
+    405       => 'Method Not Allowed',
+    406       => 'Not Acceptable',
+    415       => 'Unsupported Media Type',
+    418       => 'I\'m a teapot',
+    500       => 'Internal Server Error',
+    501       => 'Not Implemented',
+    502       => 'Bad Gateway',
+    503       => 'Service Unavailable',
+  );
+
+  const MIME_TYPES = array(
+    'text'    => 'text/plain',
+    'html'    => 'text/html',
+    'rdf'     => 'text/rdf',
+    'xul'     => 'application/vnd.mozilla.xul+xml',
+    'xhtml'   => 'application/xhtml+xml',
+    'xml'     => 'text/xml',
+    'css'     => 'text/css',
+    'js'      => 'application/js',
+    'json'    => 'application/json',
+    'bin'     => 'application/octet-stream',
+    'xpi'     => 'application/x-xpinstall',
+    'jar'     => 'application/java-archive',
+    '7z'      => 'application/x-7z-compressed',
+    'xz'      => 'application/x-xz',
+    'zip'     => 'application/zip',
+  );
+
+  /**********************************************************************************************************************
+  * Error handling
+  **********************************************************************************************************************/
+  public static function EnableErrorHandler() {
+    set_error_handler(__CLASS__ . kScopeOperator . "phpErrorHandler");
+    set_exception_handler(__CLASS__ . kScopeOperator . "phpExceptionHandler");
+  }
+
+  public static function DisableErrorHandler() { restore_error_handler(); restore_exception_handler(); }
+
+  public static function phpErrorHandler($eCode, $eMessage, $eFile, $eLine) {
+    if (!(error_reporting() & $eCode)) {
+      // Don't do jack shit because the developers of PHP think users shouldn't be trusted.
+      return;
+    }
+
+    self::Report(['code' => $eCode, 'message' => $eMessage,
+                  'file' => $eFile, 'line' => $eLine,
+                  'trace' => debug_backtrace(2)]);
+  }
+
+  public static function phpExceptionHandler($ex) {
+    self::Report(['code' => E_EXCEPTION, 'message' => $ex->getMessage(),
+                  'file' => $ex->getFile(), 'line' => $ex->getLine(),
+                  'trace' => $ex->getTrace()]);
+  }
+
+  /**********************************************************************************************************************
+  * Sends HTTP Headers to client using a short name
+  *
+  * @dep HTTP_HEADERS
+  * @dep kDebugMode
+  * @dep gMtzRuntime::Error()
+  * @param $aHeader    Short name of header
+  **********************************************************************************************************************/
+  public static function Header(string|int $aHeader, bool $aSendAllOnContentType = false) { 
+    if (is_int($aHeader) && gMtzUtils::Contains(self::HTTP_STATUS_CODE, $aHeader, 1)) {
+      self::HttpStatusCode($aHeader);
+      
+      if ($aHeader < 400) {
+        return true;
+      }
+      else {
+        self::SendHeaders();
+        exit();
+      }
+    }
+
+    if (gMtzUtils::Contains(self::MIME_TYPES, $aHeader, 1)) {
+      self::ContentType($aHeader);
+
+      if ($aSendAllOnContentType) {
+        return self::SendHeaders();
+      }
+      else {
+        return true;
+      }
+    }
+
+    return gMtzRuntime::SetConfig('console.output.headers[]', $aHeader);
+  }
+
+  /********************************************************************************************************************
+  * Gets or sets the "default" content type so we don't have to output the header ourselves in most cases.
+  ********************************************************************************************************************/
+  public static function SendHeaders() {
+    $responseCode = gMtzRuntime::GetConfig('console.output.responseCode', 200);
+    gMtzRuntime::SetConfig('console.output.headers[]', 'Content-type'. kColon . kSpace . gMtzRuntime::GetConfig('console.output.contentType'));
+    gMtzRuntime::SetConfig('console.output.headers[]', 'HTTP/1.1' . kSpace . $responseCode . kSpace . self::HTTP_STATUS_CODE[$responseCode]);
+
+    $headers = gMtzRuntime::GetConfig('console.output.headers', kEmptyArray);
+
+    foreach ($headers as $_value) {
+      header(trim($_value), true);
+    }
+  }
+
+  /********************************************************************************************************************
+  * Gets or sets the "default" content type so we don't have to output the header ourselves in most cases.
+  ********************************************************************************************************************/
+  public static function HttpStatusCode(?string $aStatusCode = null) {
+    if (!$aStatusCode) {
+      gMtzRuntime::GetConfig('console.output.responseCode', 200);
+    }
+
+    if (gMtzUtils::Contains(self::HTTP_STATUS_CODE, $aStatusCode, 1)) {
+      gMtzRuntime::SetConfig('console.output.responseCode', $aStatusCode);
+    }
+  }
+
+  /********************************************************************************************************************
+  * Gets or sets the "default" content type so we don't have to output the header ourselves in most cases.
+  ********************************************************************************************************************/
+  public static function ContentType(?string $aContentType = null) {
+     if ($aContentType === null) {
+      return gMtzRuntime::GetConfig('console.output.contentType');
+    }
+
+    if (gMtzUtils::Contains(self::MIME_TYPES, $aContentType, 1)) {
+      return gMtzRuntime::SetConfig('console.output.contentType', self::MIME_TYPES[$aContentType]);
+    }
+  }
+
+  /**********************************************************************************************************************
+  * Sends HTTP Header to redirect the client to another URL
+  *
+  * @param $aURL   URL to redirect to
+  **********************************************************************************************************************/
+  public static function Redirect($aURL) { header('Location: ' . $aURL, true, 302); exit(); }
+
+  /**********************************************************************************************************************
+  * Get a subdomain or base domain from a host
+  *
+  * @dep kDot
+  * @dep gMtzUtils::ExplodeStr()
+  * @param $aHost       Hostname
+  * @param $aReturnSub  Should return subdmain
+  * @returns            domain or subdomain
+  ***********************************************************************************************************************/
+  public static function GetDomain(string $aHost, ?bool $aReturnSub = null) {
+    $host = gMtzUtils::ExplodeStr(kDot, $aHost);
+    return implode(kDot, $aReturnSub ? array_slice($host, 0, -2) : array_slice($host, -2, 2));
+  }
+
+  /**********************************************************************************************************************
+  * var_export() + exit()
+  **********************************************************************************************************************/
+  public static function Export($aVariable) { print(var_export($aVariable, true)); exit(); }
+
+  /******************************************************************************************************************
+  * Output details about a failure condition
+  ******************************************************************************************************************/
+  public static function Report(array $aMetadata) {
+    if (gMtzRuntime::Debug() && gMtzRuntime::SuperGlobal('get', 'runtime')) {
+      gMtzConsole::Output((gMtzConsole::PHP_ERROR_CODES[$aMetadata['code']] ?? gMtzConsole::PHP_ERROR_CODES[E_ALL]) . kSpaceDashSpace . $aMetadata['message']);
+    }
+
+    $traceline = fn($eFile, $eLine) => str_replace(kRootPath, kEmptyString, $eFile) . kColon . $eLine;
+    $functions = ['phpErrorHandler', 'phpExceptionHandler', 'trigger_error'];
+    $trace = ($aMetadata['file'] && $aMetadata['line']) ? [$traceline($aMetadata['file'], $aMetadata['line'])] : kEmptyArray;
+
+    foreach ($aMetadata['trace'] as $_key => $_value) {
+      if (in_array($_value['function'], $functions)) {
+        continue;
+      }
+
+      $trace[] = $traceline($_value['file'], $_value['line']);
+    }
+
+    $title = gMtzConsole::PHP_ERROR_CODES[$aMetadata['code']] ?? gMtzConsole::PHP_ERROR_CODES[E_ALL];
+    $content = $aMetadata['message'];
+
+    if (!SAPI_IS_CLI) {
+      $content = '<h2>Traceback</h2><ul>';
+
+      foreach ($trace as $_value) {
+        $content .= '<li>' .  str_replace(kSlash, kDot, str_replace('.php', kEmptyString, ltrim($_value, kSlash))) . '</li>';
+      }
+
+      $content .= '</ul>';
+      $content .= is_string($aMetadata['message']) ?
+                 '<hr /><p><strong>' . $aMetadata['message'] . '</strong></p>':
+                 kEmptyString;
+
+      $commandBar = ['onclick="history.back()"' => 'Go Back'];
+
+      if (gMtzRuntime::Component(kSpecialComponent) || !gMtzRuntime::GetConfig('constant.components.site')) {
+        gMtzRuntime::SetConfig('console.ui.commandbar', array_merge($commandBar, ['/special/' => kSpecialComponentName]));
+      }
+      else {
+        gMtzRuntime::SetConfig('console.ui.commandbar', array_merge($commandBar, kDefaultMenu));
+      }
+
+      gMtzRuntime::SetConfig('console.ui.sectionName', kEmptyString);
+      gMtzConsole::Content($content, ['title' => $title, 'statustext' => '<span class="pulseText">Please contact a system administrator.</span>']);
+    }
+
+    gMtzConsole::Output(['title'=> $title, 'content' => ['errorMessage' => $content, 'traceback' => $trace]]);
+  }
+
+  /******************************************************************************************************************
+  * Simply prints output and sends header if not cli and exits
+  ******************************************************************************************************************/
+  public static function Output($aContent, ?string $aHeader = 'text') {
+    $store = gMtzRuntime::GetStore();
+    $content = null;
+
+    if ($aContent === true) {
+      $content = gMtzRuntime::EnsureValue($store['console']['output']['responseBody']);
+    }
+
+    if (gMtzRuntime::Debug() && gMtzRuntime::SuperGlobal('get', 'runtime')) {
+      $finalContent = $store;
+      $finalContent['console']['output']['responseBody'] = $content ?? $aContent;
+      $finalContent['console']['output']['responseTime'] = microtime(true) - gMtzRuntime::SuperGlobal('server', 'REQUEST_TIME_FLOAT', 0);
+      $finalContent = json_encode($finalContent, gMtzUtils::JSON_ENCODE_FLAGS['display']);
+      self::Header('text', true);
+      print($finalContent);
+      exit();
+    }
+
+    if (is_array($aContent)) {
+      $title = $aContent['title'] ?? 'Output';
+      $content = $aContent['content'] ?? kEmptyString;
+
+      if ($title == 'Output' && $content == kEmptyString) {
+        $content = $aContent ?? $content;
+      }
+    }
+    else {
+      $title = 'Output';
+      $content = $content ?? $aContent ?? kEmptyString;
+    }
+
+    $content = (is_string($content) || is_int($content)) ? $content : json_encode($content, gMtzUtils::JSON_ENCODE_FLAGS['display']);
+
+    // Send the header if not cli
+    if (SAPI_IS_CLI) {
+      if (!CLI_NO_LOGO) {
+        $software = $title . kSpaceDashSpace . kAppVendor . kSpace . kAppName . kSpace . kAppVersion;
+        $titleLength = 120 - 8 - strlen($software);
+        $titleLength = ($titleLength > 0) ? $titleLength : 2;
+        $title = kNewLine . '==' . kSpace . kPipe . kSpace . $software . kSpace . kPipe . kSpace . str_repeat('=', $titleLength);
+        $content = $title . kNewLine . kNewLine . $content . kNewLine . kNewLine . str_repeat('=', 120) . kNewLine;
+      }
+    }
+    else {
+      self::ContentType($aHeader);
+      self::SendHeaders();
+    }
+
+    // Write out the content
+    print($content);
+
+    // We're done here...
+    exit();
+  }
+
+  /******************************************************************************************************************
+  * Basic Site Content Generation using a Special Template
+  ******************************************************************************************************************/
+  public static function Content(mixed $aContent, array $aMetadata = kEmptyArray) {
+    $template = gMtzRuntime::ReadFile(kDefaultSkinPath . kSlash . 'template.xhtml');
+    $stylesheet = gMtzRuntime::ReadFile(kDefaultSkinPath . kSlash . 'stylesheet.css');
+
+    if (!$template) {
+      gMtzConsole::Output(['content' => $aContent, 'title' => $aMetadata['title'] ?? 'Output']);
+    }
+
+    $content = $aContent;
+
+    $metadata = function($val) use(&$aMetadata) {
+      return $aMetadata[$val] ?? null;
+    };
+
+    $menuize = function($aMenu) {
+      $rv = kEmptyString;
+
+      foreach ($aMenu as $_key => $_value) {
+        if (gMtzUtils::Contains($_key, 'onclick=', 1)) {
+          $rv .= '<li><a href="#"' . kSpace . $_key . '>' . $_value . '</a></li>';
+        }
+        else {
+          $rv .= '<li><a href="' . $_key . '">' . $_value . '</a></li>';
+        }
+      }
+
+      return $rv;
+    };
+
+    if ((is_string($content) || is_int($content)) && !$metadata('textbox') && !$metadata('iframe')) {
+      if (!gMtzUtils::Contains($content, ['<p', '<ul', '<ol', '<h1', '<h2', '<h3', '<table'])) {
+        $content = '<p>' . $content . '</p>';
+      }
+    }
+    else {
+      $aMetadata['textbox'] = true;
+    }
+
+    if ($metadata('textbox')) {
+      $content = (is_string($content) || is_int($content)) ? $content : json_encode($content, gMtzUtils::JSON_ENCODE_FLAGS['display']);
+      $content = '<form><textarea class="special-textbox" name="content" rows="30" readonly>' . $content . '</textarea></form>';
+    }
+
+    $siteName = gMtzRuntime::GetConfig('console.ui.siteName', kAppName);
+    $sectionName = gMtzRuntime::GetConfig('console.ui.sectionName', kEmptyString);
+
+    if ($sectionName) {
+      $siteName = $sectionName . kSpaceDashSpace . $siteName;
+    }
+
+    $isTestCase = (!$metadata('title') && gMtzRuntime::GetConfig('special.testCase') && gMtzRuntime::Component(kSpecialComponent));
+
+    $substs = array(
+      '{$SITE_STYLESHEET}'  => $stylesheet ?? kEmptyString,
+      '{$PAGE_CONTENT}'     => $content,
+      '{$SITE_DOMAIN}'      => gMtzRuntime::SuperGlobal('server', 'SERVER_NAME') ?? 'localhost',
+      '{$SITE_NAME}'        => $siteName,
+      '{$SITE_MENU}'        => $menuize(gMtzRuntime::GetConfig('console.ui.commandbar')),
+      '{$SITE_SECTION}'     => $sectionName ?? kEmptyString,
+      '{$PAGE_TITLE}'       => $isTestCase ? '[Test]' . kSpace . gMtzRuntime::GetConfig('special.testCase') : ($metadata('title') ?? 'Output'),
+      '{$PAGE_STATUS}'      => $metadata('statustext') ?? gMtzRuntime::GetConfig('console.ui.statustext'),
+      '{$SKIN_PATH}'        => substr(kDefaultSkinPath, 1),
+      '{$SOFTWARE_VENDOR}'  => kAppVendor,
+      '{$SOFTWARE_NAME}'    => kAppName,
+      '{$SOFTWARE_VERSION}' => kAppVersion,
+    );
+
+    $content = gMtzUtils::Subst($template, $substs);
+
+    @ob_end_clean();
+    gMtzConsole::Output($content, 'html');
+  }
+
+  /**********************************************************************************************************************
+  * Create an XML Document 
+  ***********************************************************************************************************************/
+  public static function extensibleMarkup(string|array $aData, ?bool $aDirectOutput = null) {
+    if (is_string($aData)) {
+      $xml = $aData;
+    }
+    else {
+      $dom = new DOMDocument('1.0');
+      $dom->encoding = "UTF-8";
+      $dom->formatOutput = true;
+
+      $processChildren = function($aData) use (&$dom, &$processChildren) {
+        if (!($aData['@elem'] ?? null)) {
+          return false;
+        }
+
+        // Create the element
+        $element = $dom->createElement($aData['@elem']);
+
+        // Properly handle content using XML and not try and be lazy.. It almost never works!
+        if (array_key_exists('@content', $aData) && is_string($aData['@content'])) {
+          if (gMtzUtils::Contains($aData['@content'], ["<", ">", "?", "&", "'", "\""])) {
+            $content = $dom->createCDATASection($aData['@content'] ?? kEmptyString);
+          }
+          else {
+            $content = $dom->createTextNode($aData['@content'] ?? kEmptyString);
+          }
+
+          $element->appendChild($content);
+        }
+       
+        // Add any attributes
+        if (!empty($aData['@attr']) && is_array($aData['@attr'])) {
+          foreach ($aData['@attr'] as $_key => $_value) {
+            $element->setAttribute($_key, $_value);
+          }
+        }
+       
+        // Any other items in the data array should be child elements
+        foreach ($aData as $_key => $_value) {
+          if (!is_numeric($_key)) {
+            continue;
+          }
+       
+          $child = $processChildren($_value);
+          if ($child) {
+            $element->appendChild($child);
+          }
+        }
+       
+        return $element;
+      };
+
+      $child = $processChildren($aData);
+      $xml = null;
+
+      if ($child) {
+        $dom->appendChild($child);
+      }
+
+      $xml = $dom->saveXML();
+
+      if (!$xml) {
+        gMtzRuntime::Error('Could not generate extensible markup.');
+      }
+    }
+
+    if ($aDirectOutput) {
+      gMtzConsole::Output($xml, 'xml');
+    }
+
+    return $xml;
+  }
+}
+
+} // ==================================================================================================================
+// ====================================================================================================================
+
+namespace mozilla\vc { // == | nsIVersionComparator | =================================================================
+
+/**
+* Implements Mozilla Toolkit's nsIVersionComparator
+*
+* Version strings are dot-separated sequences of version-parts.
+*
+* A version-part consists of up to four parts, all of which are optional:
+* <number-a><string-b><number-c><string-d (everything else)>
+* A version-part may also consist of a single asterisk "*" which indicates
+* "infinity".
+*
+* Numbers are base-10, and are zero if left out.
+* Strings are compared bytewise.
+*
+* For additional backwards compatibility, if "string-b" is "+" then
+* "number-a" is incremented by 1 and "string-b" becomes "pre".
+*
+* 1.0pre1
+* < 1.0pre2  
+*   < 1.0 == 1.0.0 == 1.0.0.0
+*     < 1.1pre == 1.1pre0 == 1.0+
+*       < 1.1pre1a
+*         < 1.1pre1
+*           < 1.1pre10a
+*             < 1.1pre10
+*
+* Although not required by this interface, it is recommended that
+* numbers remain within the limits of a signed char, i.e. -127 to 128.
+*/
+class ToolkitVersionPart {
+  public $numA = 0;
+  public $strB = null;
+  public $numC = 0;
+  public $extraD = null;
+}
+
+class ToolkitVersionComparator {
+  public static function compare($a, $b) {
+    do {
+      $va = new ToolkitVersionPart();
+      $vb = new ToolkitVersionPart();
+      $a = self::parseVersionPart($a, $va);
+      $b = self::parseVersionPart($b, $vb);
+      
+      $result = self::compareVersionPart($va, $vb);
+      
+      if ($result != 0){
+        break;
+      }
+    }
+
+    while ($a != null || $b != null);
+
+    if ($result >= 1) { $result = 1; }
+    if ($result <= -1) { $result = -1; }
+
+    return $result;
+  }
+  
+  
+  private static function parseVersionPart($aVersion, ToolkitVersionPart $result) {
+    if ($aVersion === null || strlen($aVersion) == 0) {
+      return $aVersion;
+    }
+    
+    $tok = explode(".", trim($aVersion));
+    $part = $tok[0];
+    
+    if ($part == "*") {
+      $result->numA = PHP_INT_MAX;
+      $result->strB = "";
+    }
+    else {
+      $vertok = new ToolkitVersionPartTokenizer($part);
+      $next = $vertok->nextToken();
+      if (is_numeric($next)){
+        $result->numA = $next;
+      }
+      else {
+        $result->numA = 0;
+      }
+      
+      if ($vertok->hasMoreElements()) {
+        $str = $vertok->nextToken();
+        // if part is of type "<num>+"
+        if ($str[0] == '+') {
+          $result->numA++;
+          $result->strB = "pre";
+        }
+        else {
+          // else if part is of type "<num><alpha>..."
+          $result->strB = $str;
+          
+          if ($vertok->hasMoreTokens()) {
+            $next = $vertok->nextToken();
+            if (is_numeric($next)){
+              $result->numC = $next;
+            }
+            else {
+              $result->numC = 0;
+            }
+            if ($vertok->hasMoreTokens()) {
+              $result->extraD = $vertok->getRemainder();
+            }
+          }
+        }
+      }
+    }
+
+    if (sizeOf($tok)>1) {
+      // return everything after "."
+      return substr($aVersion, strlen($part) + 1);
+    }
+
+    return null;
+  }
+  
+  
+  private static function compareVersionPart(ToolkitVersionPart $va, ToolkitVersionPart $vb) {
+    $res = self::compareInt($va->numA, $vb->numA);
+    if ($res != 0) { return $res; }
+    
+    $res = self::compareString($va->strB, $vb->strB);
+    if ($res != 0) { return $res; }
+    
+    $res = self::compareInt($va->numC, $vb->numC);
+    if ($res != 0) { return $res; }
+    
+    return self::compareString($va->extraD, $vb->extraD);
+  }
+  
+  
+  private static function compareInt($n1, $n2) { return $n1 - $n2; }
+  
+  
+  private static function compareString($str1, $str2) {
+    // any string is *before* no string
+    if ($str1 === null) { return ($str2 !== null) ? 1 : 0; }
+    if ($str2 === null) { return -1; }
+    return strcmp($str1, $str2);
+  } 
+}
+
+/**
+ * Specialized tokenizer for Mozilla version strings.  A token can
+ * consist of one of the four sections of a version string:
+ * <number-a><string-b><number-c><string-d (everything else)>
+ */
+class ToolkitVersionPartTokenizer {
+  private $part = '';
+  
+  public function __construct($aPart) { $this->part = $aPart; }
+  
+  public function hasMoreElements() { return strlen($this->part) != 0; }
+  public function hasMoreTokens() { return strlen($this->part) != 0; }
+  public function nextToken() { return $this->nextElement(); }
+  
+  public function nextElement() {
+    if (preg_match('/^[\+\-]?[0-9].*/', $this->part)) {
+      // if string starts with a number...
+      $index = 0;
+      if ($this->part[0] == '+' || $this->part[0] == '-') {
+        $index = 1;
+      }
+      while (($index < strlen($this->part)) && is_numeric($this->part[$index])) {
+        $index++;
+      }
+      $numPart = substr($this->part, 0, $index);
+      $this->part = substr($this->part, $index);
+      return $numPart;
+    }
+    else {
+      // ... or if this is the non-numeric part of version string
+      $index = 0;
+      while (($index < strlen($this->part)) && !is_numeric($this->part[$index])) {
+        $index++;
+      }
+      $alphaPart = substr($this->part, 0, $index);
+      $this->part = substr($this->part, $index);
+      return $alphaPart;
+    }
+  }
+
+  /**
+   * Returns what remains of the original string, without tokenization.  This
+   * method is useful for getting the <string-d (everything else)>;
+   * section of a version string.
+   * 
+   * @return remaining version string
+   */
+  public function getRemainder() { return $this->part; }
+}
+
+} // ==================================================================================================================
+// ====================================================================================================================
+
+namespace Adbar { // == | Adbar | =====================================================================================
+
+use Countable;
+use ArrayAccess;
+use ArrayIterator;
+use JsonSerializable;
+use IteratorAggregate;
+use Traversable;
+
+/**
+ * Dot
+ *
+ * This class provides a dot notation access and helper functions for
+ * working with arrays of data. Inspired by Laravel Collection.
+ *
+ * @template TKey of array-key
+ * @template TValue mixed
+ *
+ * @implements \ArrayAccess<TKey, TValue>
+ * @implements \IteratorAggregate<TKey, TValue>
+ */
+class Dot implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
+{
+  /**
+   * The stored items
+   *
+   * @var array<TKey, TValue>
+   */
+  protected $items = [];
+
+  /**
+   * The character to use as a delimiter, defaults to dot (.)
+   *
+   * @var non-empty-string
+   */
+  protected $delimiter = ".";
+
+  // ====
+
+  /**
+   * Create a new Dot instance
+   *
+   * @param  mixed  $items
+   * @param  bool  $parse
+   * @param  non-empty-string  $delimiter
+   * @return void
+   */
+  public function __construct($items = [], $parse = false, $delimiter = ".") {
+    $items = $this->getArrayItems($items);
+    $this->delimiter = $delimiter ?: ".";
+    
+    if ($parse) {
+      $this->set($items);
+    }
+    else {
+      $this->items = $items;
+    }
+  }
+
+  /**
+   * Set a given key / value pair or pairs
+   * if the key doesn't exist already
+   *
+   * @param  array<TKey, TValue>|int|string  $keys
+   * @param  mixed  $value
+   * @return $this
+   */
+  public function add($keys, $value = null) {
+    if (is_array($keys)) {
+      foreach ($keys as $key => $value) {
+        $this->add($key, $value);
+      }
+    }
+    elseif ($this->get($keys) === null) {
+      $this->set($keys, $value);
+    }
+
+    return $this;
+  }
+
+  /**
+   * Return all the stored items
+   *
+   * @return array<TKey, TValue>
+   */
+  public function all() { return $this->items; }
+
+  /**
+   * Delete the contents of a given key or keys
+   *
+   * @param  array<TKey>|int|string|null  $keys
+   * @return $this
+   */
+  public function clear($keys = null)
+  {
+    if ($keys === null) {
+      $this->items = [];
+      return $this;
+    }
+
+    $keys = (array) $keys;
+    foreach ($keys as $key) { $this->set($key, []); }
+    return $this;
+  }
+
+  /**
+   * Delete the given key or keys
+   *
+   * @param  array<TKey>|array<TKey, TValue>|int|string  $keys
+   * @return $this
+   */
+  public function delete($keys) {
+    $keys = (array) $keys;
+
+    foreach ($keys as $key) {
+      if ($this->exists($this->items, $key)) {
+        unset($this->items[$key]);
+        continue;
+      }
+
+      $items = &$this->items;
+      $segments = explode($this->delimiter, $key);
+      $lastSegment = array_pop($segments);
+
+      foreach ($segments as $segment) {
+        if (!isset($items[$segment]) || !is_array($items[$segment])) {
+          continue 2;
+        }
+
+        $items = &$items[$segment];
+      }
+
+      unset($items[$lastSegment]);
+    }
+
+    return $this;
+  }
+
+  /**
+   * Checks if the given key exists in the provided array.
+   *
+   * @param  array<TKey, TValue>  $array Array to validate
+   * @param  int|string  $key  The key to look for
+   * @return bool
+   */
+  protected function exists($array, $key) { return array_key_exists($key, $array); }
+
+  /**
+   * Flatten an array with the given character as a key delimiter
+   *
+   * @param  string  $delimiter
+   * @param  mixed  $items
+   * @param  string  $prepend
+   * @return array<TKey, TValue>
+   */
+  public function flatten($delimiter = '.', $items = null, $prepend = '') {
+    $flatten = [];
+
+    if ($items === null) {
+      $items = $this->items;
+    }
+
+    foreach ($items as $key => $value) {
+      if (is_array($value) && !empty($value)) {
+        $flatten[] = $this->flatten($delimiter, $value, $prepend . $key . $delimiter);
+      }
+      else {
+        $flatten[] = [$prepend . $key => $value];
+      }
+    }
+
+    return array_merge(...$flatten);
+  }
+
+  /**
+   * Return the value of a given key
+   *
+   * @param  int|string|null  $key
+   * @param  mixed  $default
+   * @return mixed
+   */
+  public function get($key = null, $default = null) {
+    if ($key === null) { return $this->items; }
+
+    if ($this->exists($this->items, $key)) {
+      return $this->items[$key];
+    }
+
+    if (!is_string($key) || strpos($key, $this->delimiter) === false) {
+      return $default;
+    }
+
+    $items = $this->items;
+
+    foreach (explode($this->delimiter, $key) as $segment) {
+      if (!is_array($items) || !$this->exists($items, $segment)) {
+        return $default;
+      }
+
+      $items = &$items[$segment];
+    }
+
+    return $items;
+  }
+
+  /**
+   * Return the given items as an array
+   *
+   * @param  array<TKey, TValue>|self<TKey, TValue>|object|string  $items
+   * @return array<TKey, TValue>
+   */
+  protected function getArrayItems($items) {
+    if (is_array($items)) { return $items; }
+
+    if ($items instanceof self) {
+      return $items->all();
+    }
+
+    return (array) $items;
+  }
+
+  /**
+   * Check if a given key or keys exists
+   *
+   * @param  array<TKey>|int|string  $keys
+   * @return bool
+   */
+  public function has($keys) {
+    $keys = (array) $keys;
+
+    if (!$this->items || $keys === []) {
+      return false;
+    }
+
+    foreach ($keys as $key) {
+      $items = $this->items;
+
+      if ($this->exists($items, $key)) {
+        continue;
+      }
+
+      foreach (explode($this->delimiter, $key) as $segment) {
+        if (!is_array($items) || !$this->exists($items, $segment)) {
+          return false;
+        }
+
+        $items = $items[$segment];
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Check if a given key or keys are empty
+   *
+   * @param  array<TKey>|int|string|null  $keys
+   * @return bool
+   */
+  public function isEmpty($keys = null) {
+    if ($keys === null) {
+      return empty($this->items);
+    }
+
+    $keys = (array) $keys;
+
+    foreach ($keys as $key) {
+      if (!empty($this->get($key))) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  // ====
+
+  /**
+   * Merge a given array or a Dot object with the given key
+   * or with the whole Dot object
+   *
+   * @param  array<TKey, TValue>|self<TKey, TValue>|string  $key
+   * @param  array<TKey, TValue>|self<TKey, TValue>  $value
+   * @return $this
+   */
+  public function merge($key, $value = []) {
+    if (is_array($key)) {
+      $this->items = array_merge($this->items, $key);
+    }
+    elseif (is_string($key)) {
+      $items = (array) $this->get($key);
+      $value = array_merge($items, $this->getArrayItems($value));
+
+      $this->set($key, $value);
+    }
+    elseif ($key instanceof self) {
+      $this->items = array_merge($this->items, $key->all());
+    }
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Recursively merge a given array or a Dot object with the given key
+   * or with the whole Dot object.
+   *
+   * Duplicate keys are converted to arrays.
+   *
+   * @param  array<TKey, TValue>|self<TKey, TValue>|string  $key
+   * @param  array<TKey, TValue>|self<TKey, TValue>  $value
+   * @return $this
+   */
+  public function mergeRecursive($key, $value = []) {
+    if (is_array($key)) {
+      $this->items = array_merge_recursive($this->items, $key);
+    }
+    elseif (is_string($key)) {
+      $items = (array) $this->get($key);
+      $value = array_merge_recursive($items, $this->getArrayItems($value));
+
+      $this->set($key, $value);
+    }
+    elseif ($key instanceof self) {
+      $this->items = array_merge_recursive($this->items, $key->all());
+    }
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Recursively merge a given array or a Dot object with the given key
+   * or with the whole Dot object.
+   *
+   * Instead of converting duplicate keys to arrays, the value from
+   * given array will replace the value in Dot object.
+   *
+   * @param  array<TKey, TValue>|self<TKey, TValue>|string  $key
+   * @param  array<TKey, TValue>|self<TKey, TValue>  $value
+   * @return $this
+   */
+  public function mergeRecursiveDistinct($key, $value = []) {
+    if (is_array($key)) {
+      $this->items = $this->arrayMergeRecursiveDistinct($this->items, $key);
+    }
+    elseif (is_string($key)) {
+      $items = (array) $this->get($key);
+      $value = $this->arrayMergeRecursiveDistinct($items, $this->getArrayItems($value));
+
+      $this->set($key, $value);
+    }
+    elseif ($key instanceof self) {
+      $this->items = $this->arrayMergeRecursiveDistinct($this->items, $key->all());
+    }
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Merges two arrays recursively. In contrast to array_merge_recursive,
+   * duplicate keys are not converted to arrays but rather overwrite the
+   * value in the first array with the duplicate value in the second array.
+   *
+   * @param  array<TKey, TValue>|array<TKey, array<TKey, TValue>>  $array1 Initial array to merge
+   * @param  array<TKey, TValue>|array<TKey, array<TKey, TValue>>  $array2 Array to recursively merge
+   * @return array<TKey, TValue>|array<TKey, array<TKey, TValue>>
+   */
+  protected function arrayMergeRecursiveDistinct(array $array1, array $array2) {
+    $merged = &$array1;
+
+    foreach ($array2 as $key => $value) {
+      if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
+        $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
+      }
+      else {
+        $merged[$key] = $value;
+      }
+    }
+
+    return $merged;
+  }
+
+  // ====
+
+  /**
+   * Return the value of a given key and
+   * delete the key
+   *
+   * @param  int|string|null  $key
+   * @param  mixed  $default
+   * @return mixed
+   */
+  public function pull($key = null, $default = null) {
+    if ($key === null) {
+      $value = $this->all();
+      $this->clear();
+
+      return $value;
+    }
+
+    $value = $this->get($key, $default);
+    $this->delete($key);
+
+    return $value;
+  }
+
+  // ====
+
+  /**
+   * Push a given value to the end of the array
+   * in a given key
+   *
+   * @param  mixed  $key
+   * @param  mixed  $value
+   * @return $this
+   */
+  public function push($key, $value = null) {
+    if ($value === null) {
+      $this->items[] = $key;
+
+      return $this;
+    }
+
+    $items = $this->get($key);
+
+    if (is_array($items) || $items === null) {
+      $items[] = $value;
+      $this->set($key, $items);
+    }
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Replace all values or values within the given key
+   * with an array or Dot object
+   *
+   * @param  array<TKey, TValue>|self<TKey, TValue>|string  $key
+   * @param  array<TKey, TValue>|self<TKey, TValue>  $value
+   * @return $this
+   */
+  public function replace($key, $value = []) {
+    if (is_array($key)) {
+      $this->items = array_replace($this->items, $key);
+    }
+    elseif (is_string($key)) {
+      $items = (array) $this->get($key);
+      $value = array_replace($items, $this->getArrayItems($value));
+
+      $this->set($key, $value);
+    }
+    elseif ($key instanceof self) {
+      $this->items = array_replace($this->items, $key->all());
+    }
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Set a given key / value pair or pairs
+   *
+   * @param  array<TKey, TValue>|int|string  $keys
+   * @param  mixed  $value
+   * @return $this
+   */
+  public function set($keys, $value = null) {
+    if (is_array($keys)) {
+      foreach ($keys as $key => $value) {
+        $this->set($key, $value);
+      }
+
+      return $this;
+    }
+
+    $items = &$this->items;
+
+    if (is_string($keys)) {
+      foreach (explode($this->delimiter, $keys) as $key) {
+        if (!isset($items[$key]) || !is_array($items[$key])) {
+          $items[$key] = [];
+        }
+
+        $items = &$items[$key];
+      }
+    }
+
+    $items = $value;
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Replace all items with a given array
+   *
+   * @param  mixed  $items
+   * @return $this
+   */
+  public function setArray($items) {
+    $this->items = $this->getArrayItems($items);
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Replace all items with a given array as a reference
+   *
+   * @param  array<TKey, TValue>  $items
+   * @return $this
+   */
+  public function setReference(array &$items) {
+    $this->items = &$items;
+
+    return $this;
+  }
+
+  // ====
+
+  /**
+   * Return the value of a given key or all the values as JSON
+   *
+   * @param  mixed  $key
+   * @param  int  $options
+   * @return string|false
+   */
+  public function toJson($key = null, $options = 0) {
+    if (is_string($key)) {
+      return json_encode($this->get($key), $options);
+    }
+
+    $options = $key === null ? 0 : $key;
+
+    return json_encode($this->items, $options);
+  }
+
+  // ====
+
+  /**
+   * Output or return a parsable string representation of the
+   * given array when exported by var_export()
+   *
+   * @param  array<TKey, TValue>  $items
+   * @return object
+   */
+  public static function __set_state(array $items): object { return (object) $items; }
+
+  // ====
+
+  /*
+   * --------------------------------------------------------------
+   * ArrayAccess interface
+   * --------------------------------------------------------------
+   */
+
+  /**
+   * Check if a given key exists
+   *
+   * @param  int|string  $key
+   * @return bool
+   */
+  public function offsetExists($key): bool { return $this->has($key); }
+
+  // ====
+
+  /**
+   * Return the value of a given key
+   *
+   * @param  int|string  $key
+   * @return mixed
+   */
+  #[\ReturnTypeWillChange]
+  public function offsetGet($key) { return $this->get($key); }
+
+  // ====
+
+  /**
+   * Set a given value to the given key
+   *
+   * @param int|string|null  $key
+   * @param mixed  $value
+   */
+  public function offsetSet($key, $value): void {
+    if ($key === null) {
+      $this->items[] = $value;
+
+      return;
+    }
+
+    $this->set($key, $value);
+  }
+
+  // ====
+
+  /**
+   * Delete the given key
+   *
+   * @param  int|string  $key
+   * @return void
+   */
+  public function offsetUnset($key): void { $this->delete($key); }
+
+  // ====
+
+  /*
+   * --------------------------------------------------------------
+   * Countable interface
+   * --------------------------------------------------------------
+   */
+
+  /**
+   * Return the number of items in a given key
+   *
+   * @param  int|string|null  $key
+   * @return int
+   */
+  public function count($key = null): int { return count($this->get($key)); }
+
+  // ====
+
+  /*
+   * --------------------------------------------------------------
+   * IteratorAggregate interface
+   * --------------------------------------------------------------
+   */
+
+  /**
+   * Get an iterator for the stored items
+   *
+   * @return \ArrayIterator<TKey, TValue>
+   */
+  public function getIterator(): Traversable { return new ArrayIterator($this->items); }
+
+  // ====
+
+  /*
+   * --------------------------------------------------------------
+   * JsonSerializable interface
+   * --------------------------------------------------------------
+   */
+
+  /**
+   * Return items for JSON serialization
+   *
+   * @return array<TKey, TValue>
+   */
+  public function jsonSerialize(): array { return $this->items; }
+}
+
+} // ==================================================================================================================
+// ====================================================================================================================
+
+namespace { // ========================================================================================================
+
+// An application has been detected - Unimatrix 424, Grid 116 - activate.
+gMtzRuntime::init();
+
+} // ==================================================================================================================
+// ====================================================================================================================

+ 8 - 0
components/site/content/index.mtz

@@ -0,0 +1,8 @@
+---
+title: Index page
+heading: Heading Text
+tagline: banana
+type: html
+---
+<h2>Test</h2>
+<p>content</p>

+ 1 - 0
components/site/skin/template.html

@@ -0,0 +1 @@
+{$PAGE_CONTENT}

+ 18 - 0
components/site/src/main.php

@@ -0,0 +1,18 @@
+<?php /****************************************************************************************************************
+* PHP Code is filmed before a live studio audience.
+**********************************************************************************************************************/
+
+// == | Main | ========================================================================================================
+
+// Set the site name
+gSetConfig('console.ui.siteName', 'SeaMonkey Add-ons');
+
+// Import generateContent
+gImportModules('generateContent');
+
+// Try and load static content based on the url
+gMtzRuntime::$sModule['generateContent']->LoadStaticContent();
+
+// ====================================================================================================================
+
+?>

+ 22 - 0
index.php

@@ -0,0 +1,22 @@
+<?php /****************************************************************************************************************
+* PHP Code is filmed before a live studio audience.
+**********************************************************************************************************************/
+
+// == | Entry Point | =================================================================================================
+
+// kRootPath is defined as the absolute path (without a trailing slash) of the document root or the scriptdir if cli.
+// NOTE: We use this as the current working directory may change for the lifetime of the script in cli.
+// With the addition of gPath() from the utils will normalize all paths making cross-platform possible without much
+// trouble.
+define('kRootPath', empty($_SERVER['DOCUMENT_ROOT']) ? __DIR__ : $_SERVER['DOCUMENT_ROOT']);
+const kDebugDomain = 'addons.thereisonlyxul.org';
+const kAppVendor = 'Binary Outcast';
+const kAppName = 'Neptune Add-ons and Package Infrastructure';
+const kAppVersion = '1.0.0a1';
+
+// Load fundamental utils
+require_once(kRootPath . '/base/src/utils.php');
+print('hi');
+// ====================================================================================================================
+
+?>

+ 203 - 0
modules/mtzGenerateContent.php

@@ -0,0 +1,203 @@
+<?php
+class mtzGenerateContent {
+  
+  // Resource path normally same as component
+  private $resPath;
+
+  // Content path is resPath + /content
+  private $contentPath;
+
+  // Content path is resPath + /posts
+  private $postsPath;
+
+  // Skin path is resPath + /skin
+  private $skinPath;
+
+  // Holds the component template
+  private $template;
+
+  // Holds the page content
+  private $content;
+
+  // Holds the page metadata
+  private $metadata;
+
+  // Temporarily stores the final output which is template + content + processing
+  // This is then sent to the utils to be output'd
+  private $finalOutput;
+
+  /********************************************************************************************************************
+  * Class constructor that sets initial state of things
+  ********************************************************************************************************************/
+  function __construct() {
+    $this->resPath = gGetConfig('constant.components.' . gGetConfig('app.component'));
+    $this->resPath = dirname(str_replace('/src', kEmptyString, $this->resPath));
+
+    $this->skinPath = gBuildPath($this->resPath, 'skin');
+    $this->contentPath = gBuildPath($this->resPath, 'content');
+    $this->postsPath = gBuildPath($this->resPath, 'posts');
+
+    $this->template = gReadFile(gBuildPath($this->skinPath, 'template.html'));
+
+    if (!$this->template) {
+      gError('Unable to load skin resources.');
+    }
+  }
+
+  /********************************************************************************************************************
+  * Load static content from component resources
+  ********************************************************************************************************************/
+  public function LoadStaticContent() {
+    $requestURI = gGetConfig('network.requestPath');
+
+    if (str_contains($requestURI, kDotDot)) {
+      gError('Reference Code: ID-10-T');
+    }
+
+    $contentFilename = 'index.mtz';
+    $contentFilepath = $this->contentPath . $requestURI . $contentFilename;
+
+    if (!file_exists($contentFilepath)) {
+      gNotFound('Content could not be loaded.');
+    }
+
+    $metacontent = $this->_ParseYaml(gReadFile($contentFilepath));
+
+    $this->metadata = $metacontent['data'];
+    $this->content = $metacontent['content'];
+
+    $this->Display();
+  }
+
+  /********************************************************************************************************************
+  * Display (Output) the final content
+  ********************************************************************************************************************/
+  public function Display() {
+    $this->content = $this->_ParseCodeTags($this->metadata['type'] ?? 'html', $this->content);
+
+    $substs = array(
+      'PAGE_CONTENT' => $this->content,
+      'SOFTWARE_NAME' => kEmptyString,
+      'SOFTWARE_VERSION' => kEmptyString,
+      'SOFTWARE_DEBUG' => kEmptyString,
+      'CONTENT_PATH' => gMtzUtils::StripStr($this->contentPath, kRootPath) . kSlash . gGetConfig('network.requestPath'),
+      'BASE_SKIN_PATH' => kDefaultSkinPath,
+      'COMPONENT_SKIN_PATH' => gMtzUtils::StripStr($this->skinPath, kRootPath),
+      'SITE_NAME' => gGetConfig('console.ui.siteName', kAppName),
+      'SITE_MENU' => kEmptyString,
+      'SITE_SECTION' => gGetConfig('app.path.0', kEmptyString),
+      'PAGE_TITLE' => $this->metadata['title'],
+      'PAGE_HEADING' => $this->metadata['heading'],
+      'PAGE_TAGLINE' => $this->metadata['tagline'],
+      'PAGE_MENU' => kEmptyString,
+      'PAGE_SIDEBAR' => kEmptyString,
+      'PAGE_PATH' => gGetConfig('network.requestPath'),
+      'PAGE_PATH_0' => gGetConfig('app.path.0', kEmptyString),
+      'PAGE_PATH_1' => gGetConfig('app.path.1', kEmptyString),
+      'PAGE_PATH_2' => gGetConfig('app.path.2', kEmptyString),
+      'PAGE_PATH_3' => gGetConfig('app.path.3', kEmptyString),
+      'CURRENT_YEAR' => date("Y"),
+      'CACHE_EPOCH' => time(),
+    );
+
+    $this->finalOutput = gMtzUtils::SubstEx($this->template, $substs, kLeftBrace . kDollar);
+
+    gSetConfig('console.output.responseBody', $this->finalOutput);
+    gOutput(true, 'html');
+  }
+
+  /********************************************************************************************************************
+  * Get yaml header as an array and strip the content of the yaml header
+  ********************************************************************************************************************/
+  private function _ParseYaml($aContent) {
+    return ['data'    => gEnsureValue(@yaml_parse($aContent)),
+            'content' => preg_replace(gMtzUtils::REGEX_PATTERNS['yaml'], kEmptyString, $aContent)];
+  }
+
+  /********************************************************************************************************************
+  * Translates content with bbCode-like tags to HTML or returns already used HTML depending on type
+  ********************************************************************************************************************/
+  private function _ParseCodeTags($aType, $aContent) {
+    if (!gEnsureValue($aContent)) {
+      return null;
+    }
+
+    switch ($aType) {
+      case 'phoebus':
+        // This is the phoebusCode mangling that needs to die in a fire
+        $aContent = htmlentities($aContent, ENT_XHTML);
+
+        // Replace new lines with <br />
+        $aContent = nl2br($aContent, true);
+
+        $simpleTags = array(
+          '[b]'         => '<strong>',
+          '[/b]'        => '</strong>',
+          '[i]'         => '<em>',
+          '[/i]'        => '</em>',
+          '[u]'         => '<u>',
+          '[/u]'        => '</u>',
+          '[ul]'        => '</p><ul><fixme />',
+          '[/ul]'       => '</ul><p><fixme />',
+          '[ol]'        => '</p><ol><fixme />',
+          '[/ol]'       => '</ol><p><fixme />',
+          '[li]'        => '<li>',
+          '[/li]'       => '</li>',
+          '[section]'   => '</p><h3>',
+          '[/section]'  => '</h3><p><fixme />'
+        );
+
+        $regexTags = array(
+          '\<(ul|\/ul|li|\/li|p|\/p)\><br \/>'  => '<$1>',
+          '\[url=(.*)\](.*)\[\/url\]'           => '<a href="$1" target="_blank">$2</a>',
+          '\[url\](.*)\[\/url\]'                => '<a href="$1" target="_blank">$1</a>',
+          '\[img(.*)\](.*)\[\/img\]'            => kEmptyString,
+        );
+
+        // Process the substs
+        $aContent = gSubst(gSubst($aContent, $simpleTags), $regexTags, true);
+
+        // Less hacky than what is in funcReadManifest
+        // Remove linebreak special cases
+        $aContent = str_replace('<fixme /><br />', kEmptyString, $aContent);
+        break;
+      case 'selene':
+      case 'phobos':
+      case 'mtz':
+        $aContent = htmlentities($aContent, ENT_XHTML);
+
+        $htmlTags       = implode(kPipe, ['p', 'span', 'small', 'br', 'hr', 'ul', 'ol', 'li', 'table', 'th', 'tr', 'td',
+                                         'caption', 'col', 'colgroup', 'thead', 'tbody', 'tfoot']);
+
+        $regexTags      = array(
+          "\[\/(" . $htmlTags . ")\]"         => '</$1>',
+          "\[(" . $htmlTags . ")\]"           => '<$1>',
+          "\[break\]"                         => '<br />',
+          "\[dblbreak\]"                      => '<br /><br/>',
+          "\[separator\]"                     => '<hr />',
+          "\[header=\"(.*)\"\]"               => '<h2>$1</h2>',
+          "\[header](.*)\[\/header\]"         => '<h2>$1</h2>',
+          "\[section=\"(.*)\"\]"              => '<h3>$1</h3>',
+          "\[section](.*)\[\/section\]"       => '<h3>$1</h3>',
+          "\[b](.*)\[\/b\]"                   => '<strong>$1</strong>',
+          "\[i](.*)\[\/i\]"                   => '<em>$1</em>',
+          "\[u](.*)\[\/u\]"                   => '<u>$1</u>',
+          "\[anchor=(.*)\]"                   => '<a name="$1"></a>',
+          "\[link=(.*)\](.*)\[\/link\]"       => '<a href="$1">$2</a>',
+          "\[url=(.*)\](.*)\[\/url\]"         => '<a href="$1" target="_blank">$2</a>',
+          "\[url\](.*)\[\/url\]"              => '<a href="$1" target="_blank">$1</a>',
+          '\[img(.*)\](.*)\[\/img\]'          => '<img src="$2"$1 />',
+        );
+
+        // Finally process the regex substs
+        $aContent = gSubst($aContent, $regexTags, true);
+        
+        break;
+    }
+
+    // And return
+    return $aContent;
+  }
+}
+
+?>

+ 36 - 0
third_party/markdown/License.md

@@ -0,0 +1,36 @@
+PHP Markdown Lib  
+Copyright (c) 2004-2022 Michel Fortin
+<https://michelf.ca/>  
+All rights reserved.
+
+Based on Markdown  
+Copyright (c) 2003-2006 John Gruber  
+<https://daringfireball.net/>  
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+  be used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.

+ 10 - 0
third_party/markdown/Markdown.inc.php

@@ -0,0 +1,10 @@
+<?php
+
+// Use this file if you cannot use class autoloading. It will include all the
+// files needed for the Markdown parser.
+//
+// Take a look at the PSR-0-compatible class autoloading implementation
+// in the Readme.php file if you want a simple autoloader setup.
+
+require_once dirname(__FILE__) . '/MarkdownInterface.php';
+require_once dirname(__FILE__) . '/Markdown.php';

+ 1889 - 0
third_party/markdown/Markdown.php

@@ -0,0 +1,1889 @@
+<?php
+/**
+ * Markdown  -  A text-to-HTML conversion tool for web writers
+ *
+ * @package   php-markdown
+ * @author    Michel Fortin <michel.fortin@michelf.com>
+ * @copyright 2004-2022 Michel Fortin <https://michelf.com/projects/php-markdown/>
+ * @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/>
+ */
+
+namespace Michelf;
+
+/**
+ * Markdown Parser Class
+ */
+class Markdown implements MarkdownInterface {
+	/**
+	 * Define the package version
+	 * @var string
+	 */
+	const MARKDOWNLIB_VERSION = "2.0.0";
+
+	/**
+	 * Simple function interface - Initialize the parser and return the result
+	 * of its transform method. This will work fine for derived classes too.
+	 *
+	 * @api
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	public static function defaultTransform(string $text): string {
+		// Take parser class on which this function was called.
+		$parser_class = static::class;
+
+		// Try to take parser from the static parser list
+		static $parser_list;
+		$parser =& $parser_list[$parser_class];
+
+		// Create the parser it not already set
+		if (!$parser) {
+			$parser = new $parser_class;
+		}
+
+		// Transform text using parser.
+		return $parser->transform($text);
+	}
+
+	/**
+	 * Configuration variables
+	 */
+	/**
+	 * Change to ">" for HTML output.
+	 */
+	public string $empty_element_suffix = " />";
+
+	/**
+	 * The width of indentation of the output markup
+	 */
+	public int $tab_width = 4;
+
+	/**
+	 * Change to `true` to disallow markup or entities.
+	 */
+	public bool $no_markup   = false;
+	public bool $no_entities = false;
+
+
+	/**
+	 * Change to `true` to enable line breaks on \n without two trailling spaces
+	 * @var boolean
+	 */
+	public bool $hard_wrap = false;
+
+	/**
+	 * Predefined URLs and titles for reference links and images.
+	 */
+	public array $predef_urls   = array();
+	public array $predef_titles = array();
+
+	/**
+	 * Optional filter function for URLs
+	 * @var callable|null
+	 */
+	public $url_filter_func = null;
+
+	/**
+	 * Optional header id="" generation callback function.
+	 * @var callable|null
+	 */
+	public $header_id_func = null;
+
+	/**
+	 * Optional function for converting code block content to HTML
+	 * @var callable|null
+	 */
+	public $code_block_content_func = null;
+
+	/**
+	 * Optional function for converting code span content to HTML.
+	 * @var callable|null
+	 */
+	public $code_span_content_func = null;
+
+	/**
+	 * Class attribute to toggle "enhanced ordered list" behaviour
+	 * setting this to true will allow ordered lists to start from the index
+	 * number that is defined first.
+	 *
+	 * For example:
+	 * 2. List item two
+	 * 3. List item three
+	 *
+	 * Becomes:
+	 * <ol start="2">
+	 * <li>List item two</li>
+	 * <li>List item three</li>
+	 * </ol>
+	 */
+	public bool $enhanced_ordered_list = false;
+
+	/**
+	 * Parser implementation
+	 */
+	/**
+	 * Regex to match balanced [brackets].
+	 * Needed to insert a maximum bracked depth while converting to PHP.
+	 */
+	protected int $nested_brackets_depth = 6;
+	protected string $nested_brackets_re;
+
+	protected int $nested_url_parenthesis_depth = 4;
+	protected string $nested_url_parenthesis_re;
+
+	/**
+	 * Table of hash values for escaped characters:
+	 */
+	protected string $escape_chars = '\`*_{}[]()>#+-.!';
+	protected string $escape_chars_re;
+
+	/**
+	 * Constructor function. Initialize appropriate member variables.
+	 * @return void
+	 */
+	public function __construct() {
+		$this->_initDetab();
+		$this->prepareItalicsAndBold();
+
+		$this->nested_brackets_re =
+			str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+			str_repeat('\])*', $this->nested_brackets_depth);
+
+		$this->nested_url_parenthesis_re =
+			str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+			str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+
+		$this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+
+		// Sort document, block, and span gamut in ascendent priority order.
+		asort($this->document_gamut);
+		asort($this->block_gamut);
+		asort($this->span_gamut);
+	}
+
+
+	/**
+	 * Internal hashes used during transformation.
+	 */
+	protected array $urls        = array();
+	protected array $titles      = array();
+	protected array $html_hashes = array();
+
+	/**
+	 * Status flag to avoid invalid nesting.
+	 */
+	protected bool $in_anchor = false;
+
+	/**
+	 * Status flag to avoid invalid nesting.
+	 */
+	protected bool $in_emphasis_processing = false;
+
+	/**
+	 * Called before the transformation process starts to setup parser states.
+	 * @return void
+	 */
+	protected function setup() {
+		// Clear global hashes.
+		$this->urls        = $this->predef_urls;
+		$this->titles      = $this->predef_titles;
+		$this->html_hashes = array();
+		$this->in_anchor   = false;
+		$this->in_emphasis_processing = false;
+	}
+
+	/**
+	 * Called after the transformation process to clear any variable which may
+	 * be taking up memory unnecessarly.
+	 * @return void
+	 */
+	protected function teardown() {
+		$this->urls        = array();
+		$this->titles      = array();
+		$this->html_hashes = array();
+	}
+
+	/**
+	 * Main function. Performs some preprocessing on the input text and pass
+	 * it through the document gamut.
+	 *
+	 * @api
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	public function transform(string $text): string {
+		$this->setup();
+
+		# Remove UTF-8 BOM and marker character in input, if present.
+		$text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+		# Standardize line endings:
+		#   DOS to Unix and Mac to Unix
+		$text = preg_replace('{\r\n?}', "\n", $text);
+
+		# Make sure $text ends with a couple of newlines:
+		$text .= "\n\n";
+
+		# Convert all tabs to spaces.
+		$text = $this->detab($text);
+
+		# Turn block-level HTML blocks into hash entries
+		$text = $this->hashHTMLBlocks($text);
+
+		# Strip any lines consisting only of spaces and tabs.
+		# This makes subsequent regexen easier to write, because we can
+		# match consecutive blank lines with /\n+/ instead of something
+		# contorted like /[ ]*\n+/ .
+		$text = preg_replace('/^[ ]+$/m', '', $text);
+
+		# Run document gamut methods.
+		foreach ($this->document_gamut as $method => $priority) {
+			$text = $this->$method($text);
+		}
+
+		$this->teardown();
+
+		return $text . "\n";
+	}
+
+	/**
+	 * Define the document gamut
+	 */
+	protected array $document_gamut = array(
+		// Strip link definitions, store in hashes.
+		"stripLinkDefinitions" => 20,
+		"runBasicBlockGamut"   => 30,
+	);
+
+	/**
+	 * Strips link definitions from text, stores the URLs and titles in
+	 * hash references
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function stripLinkDefinitions($text) {
+
+		$less_than_tab = $this->tab_width - 1;
+
+		// Link defs are in the form: ^[id]: url "optional title"
+		$text = preg_replace_callback('{
+							^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?:	# id = $1
+							  [ ]*
+							  \n?				# maybe *one* newline
+							  [ ]*
+							(?:
+							  <(.+?)>			# url = $2
+							|
+							  (\S+?)			# url = $3
+							)
+							  [ ]*
+							  \n?				# maybe one newline
+							  [ ]*
+							(?:
+								(?<=\s)			# lookbehind for whitespace
+								["(]
+								(.*?)			# title = $4
+								[")]
+								[ ]*
+							)?	# title is optional
+							(?:\n+|\Z)
+			}xm',
+			array($this, '_stripLinkDefinitions_callback'),
+			$text
+		);
+		return $text;
+	}
+
+	/**
+	 * The callback to strip link definitions
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _stripLinkDefinitions_callback($matches) {
+		$link_id = strtolower($matches[1]);
+		$url = $matches[2] == '' ? $matches[3] : $matches[2];
+		$this->urls[$link_id] = $url;
+		$this->titles[$link_id] =& $matches[4];
+		return ''; // String that will replace the block
+	}
+
+	/**
+	 * Hashify HTML blocks
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function hashHTMLBlocks($text) {
+		if ($this->no_markup) {
+			return $text;
+		}
+
+		$less_than_tab = $this->tab_width - 1;
+
+		/**
+		 * Hashify HTML blocks:
+		 *
+		 * We only want to do this for block-level HTML tags, such as headers,
+		 * lists, and tables. That's because we still want to wrap <p>s around
+		 * "paragraphs" that are wrapped in non-block-level tags, such as
+		 * anchors, phrase emphasis, and spans. The list of tags we're looking
+		 * for is hard-coded:
+		 *
+		 * *  List "a" is made of tags which can be both inline or block-level.
+		 *    These will be treated block-level when the start tag is alone on
+		 *    its line, otherwise they're not matched here and will be taken as
+		 *    inline later.
+		 * *  List "b" is made of tags which are always block-level;
+		 */
+		$block_tags_a_re = 'ins|del';
+		$block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+						   'script|noscript|style|form|fieldset|iframe|math|svg|'.
+						   'article|section|nav|aside|hgroup|header|footer|'.
+						   'figure|details|summary';
+
+		// Regular expression for the content of a block tag.
+		$nested_tags_level = 4;
+		$attr = '
+			(?>				# optional tag attributes
+			  \s			# starts with whitespace
+			  (?>
+				[^>"/]+		# text outside quotes
+			  |
+				/+(?!>)		# slash not followed by ">"
+			  |
+				"[^"]*"		# text inside double quotes (tolerate ">")
+			  |
+				\'[^\']*\'	# text inside single quotes (tolerate ">")
+			  )*
+			)?
+			';
+		$content =
+			str_repeat('
+				(?>
+				  [^<]+			# content without tag
+				|
+				  <\2			# nested opening tag
+					'.$attr.'	# attributes
+					(?>
+					  />
+					|
+					  >', $nested_tags_level).	// end of opening tag
+					  '.*?'.					// last level nested tag content
+			str_repeat('
+					  </\2\s*>	# closing nested tag
+					)
+				  |
+					<(?!/\2\s*>	# other tags with a different name
+				  )
+				)*',
+				$nested_tags_level);
+		$content2 = str_replace('\2', '\3', $content);
+
+		/**
+		 * First, look for nested blocks, e.g.:
+		 * 	<div>
+		 * 		<div>
+		 * 		tags for inner block must be indented.
+		 * 		</div>
+		 * 	</div>
+		 *
+		 * The outermost tags must start at the left margin for this to match,
+		 * and the inner nested divs must be indented.
+		 * We need to do this before the next, more liberal match, because the
+		 * next match will start at the first `<div>` and stop at the
+		 * first `</div>`.
+		 */
+		$text = preg_replace_callback('{(?>
+			(?>
+				(?<=\n)			# Starting on its own line
+				|				# or
+				\A\n?			# the at beginning of the doc
+			)
+			(						# save in $1
+
+			  # Match from `\n<tag>` to `</tag>\n`, handling nested tags
+			  # in between.
+
+						[ ]{0,'.$less_than_tab.'}
+						<('.$block_tags_b_re.')# start tag = $2
+						'.$attr.'>			# attributes followed by > and \n
+						'.$content.'		# content, support nesting
+						</\2>				# the matching end tag
+						[ ]*				# trailing spaces/tabs
+						(?=\n+|\Z)	# followed by a newline or end of document
+
+			| # Special version for tags of group a.
+
+						[ ]{0,'.$less_than_tab.'}
+						<('.$block_tags_a_re.')# start tag = $3
+						'.$attr.'>[ ]*\n	# attributes followed by >
+						'.$content2.'		# content, support nesting
+						</\3>				# the matching end tag
+						[ ]*				# trailing spaces/tabs
+						(?=\n+|\Z)	# followed by a newline or end of document
+
+			| # Special case just for <hr />. It was easier to make a special
+			  # case than to make the other regex more complicated.
+
+						[ ]{0,'.$less_than_tab.'}
+						<(hr)				# start tag = $2
+						'.$attr.'			# attributes
+						/?>					# the matching end tag
+						[ ]*
+						(?=\n{2,}|\Z)		# followed by a blank line or end of document
+
+			| # Special case for standalone HTML comments:
+
+					[ ]{0,'.$less_than_tab.'}
+					(?s:
+						<!-- .*? -->
+					)
+					[ ]*
+					(?=\n{2,}|\Z)		# followed by a blank line or end of document
+
+			| # PHP and ASP-style processor instructions (<? and <%)
+
+					[ ]{0,'.$less_than_tab.'}
+					(?s:
+						<([?%])			# $2
+						.*?
+						\2>
+					)
+					[ ]*
+					(?=\n{2,}|\Z)		# followed by a blank line or end of document
+
+			)
+			)}Sxmi',
+			array($this, '_hashHTMLBlocks_callback'),
+			$text
+		);
+
+		return $text;
+	}
+
+	/**
+	 * The callback for hashing HTML blocks
+	 * @param  string $matches
+	 * @return string
+	 */
+	protected function _hashHTMLBlocks_callback($matches) {
+		$text = $matches[1];
+		$key  = $this->hashBlock($text);
+		return "\n\n$key\n\n";
+	}
+
+	/**
+	 * Called whenever a tag must be hashed when a function insert an atomic
+	 * element in the text stream. Passing $text to through this function gives
+	 * a unique text-token which will be reverted back when calling unhash.
+	 *
+	 * The $boundary argument specify what character should be used to surround
+	 * the token. By convension, "B" is used for block elements that needs not
+	 * to be wrapped into paragraph tags at the end, ":" is used for elements
+	 * that are word separators and "X" is used in the general case.
+	 *
+	 * @param  string $text
+	 * @param  string $boundary
+	 * @return string
+	 */
+	protected function hashPart($text, $boundary = 'X') {
+		// Swap back any tag hash found in $text so we do not have to `unhash`
+		// multiple times at the end.
+		$text = $this->unhash($text);
+
+		// Then hash the block.
+		static $i = 0;
+		$key = "$boundary\x1A" . ++$i . $boundary;
+		$this->html_hashes[$key] = $text;
+		return $key; // String that will replace the tag.
+	}
+
+	/**
+	 * Shortcut function for hashPart with block-level boundaries.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function hashBlock($text) {
+		return $this->hashPart($text, 'B');
+	}
+
+	/**
+	 * Define the block gamut - these are all the transformations that form
+	 * block-level tags like paragraphs, headers, and list items.
+	 */
+	protected array $block_gamut = array(
+		"doHeaders"         => 10,
+		"doHorizontalRules" => 20,
+		"doLists"           => 40,
+		"doCodeBlocks"      => 50,
+		"doBlockQuotes"     => 60,
+	);
+
+	/**
+	 * Run block gamut tranformations.
+	 *
+	 * We need to escape raw HTML in Markdown source before doing anything
+	 * else. This need to be done for each block, and not only at the
+	 * begining in the Markdown function since hashed blocks can be part of
+	 * list items and could have been indented. Indented blocks would have
+	 * been seen as a code block in a previous pass of hashHTMLBlocks.
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function runBlockGamut($text) {
+		$text = $this->hashHTMLBlocks($text);
+		return $this->runBasicBlockGamut($text);
+	}
+
+	/**
+	 * Run block gamut tranformations, without hashing HTML blocks. This is
+	 * useful when HTML blocks are known to be already hashed, like in the first
+	 * whole-document pass.
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function runBasicBlockGamut($text) {
+
+		foreach ($this->block_gamut as $method => $priority) {
+			$text = $this->$method($text);
+		}
+
+		// Finally form paragraph and restore hashed blocks.
+		$text = $this->formParagraphs($text);
+
+		return $text;
+	}
+
+	/**
+	 * Convert horizontal rules
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doHorizontalRules($text) {
+		return preg_replace(
+			'{
+				^[ ]{0,3}	# Leading space
+				([-*_])		# $1: First marker
+				(?>			# Repeated marker group
+					[ ]{0,2}	# Zero, one, or two spaces.
+					\1			# Marker character
+				){2,}		# Group repeated at least twice
+				[ ]*		# Tailing spaces
+				$			# End of line.
+			}mx',
+			"\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
+			$text
+		);
+	}
+
+	/**
+	 * These are all the transformations that occur *within* block-level
+	 * tags like paragraphs, headers, and list items.
+	 */
+	protected array $span_gamut = array(
+		// Process character escapes, code spans, and inline HTML
+		// in one shot.
+		"parseSpan"           => -30,
+		// Process anchor and image tags. Images must come first,
+		// because ![foo][f] looks like an anchor.
+		"doImages"            =>  10,
+		"doAnchors"           =>  20,
+		// Make links out of things like `<https://example.com/>`
+		// Must come after doAnchors, because you can use < and >
+		// delimiters in inline links like [this](<url>).
+		"doAutoLinks"         =>  30,
+		"encodeAmpsAndAngles" =>  40,
+		"doItalicsAndBold"    =>  50,
+		"doHardBreaks"        =>  60,
+	);
+
+	/**
+	 * Run span gamut transformations
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function runSpanGamut($text) {
+		foreach ($this->span_gamut as $method => $priority) {
+			$text = $this->$method($text);
+		}
+
+		return $text;
+	}
+
+	/**
+	 * Do hard breaks
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doHardBreaks($text) {
+		if ($this->hard_wrap) {
+			return preg_replace_callback('/ *\n/',
+				array($this, '_doHardBreaks_callback'), $text);
+		} else {
+			return preg_replace_callback('/ {2,}\n/',
+				array($this, '_doHardBreaks_callback'), $text);
+		}
+	}
+
+	/**
+	 * Trigger part hashing for the hard break (callback method)
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doHardBreaks_callback($matches) {
+		return $this->hashPart("<br$this->empty_element_suffix\n");
+	}
+
+	/**
+	 * Turn Markdown link shortcuts into XHTML <a> tags.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doAnchors($text) {
+		if ($this->in_anchor) {
+			return $text;
+		}
+		$this->in_anchor = true;
+
+		// First, handle reference-style links: [link text] [id]
+		$text = preg_replace_callback('{
+			(					# wrap whole match in $1
+			  \[
+				('.$this->nested_brackets_re.')	# link text = $2
+			  \]
+
+			  [ ]?				# one optional space
+			  (?:\n[ ]*)?		# one optional newline followed by spaces
+
+			  \[
+				(.*?)		# id = $3
+			  \]
+			)
+			}xs',
+			array($this, '_doAnchors_reference_callback'), $text);
+
+		// Next, inline-style links: [link text](url "optional title")
+		$text = preg_replace_callback('{
+			(				# wrap whole match in $1
+			  \[
+				('.$this->nested_brackets_re.')	# link text = $2
+			  \]
+			  \(			# literal paren
+				[ \n]*
+				(?:
+					<(.+?)>	# href = $3
+				|
+					('.$this->nested_url_parenthesis_re.')	# href = $4
+				)
+				[ \n]*
+				(			# $5
+				  ([\'"])	# quote char = $6
+				  (.*?)		# Title = $7
+				  \6		# matching quote
+				  [ \n]*	# ignore any spaces/tabs between closing quote and )
+				)?			# title is optional
+			  \)
+			)
+			}xs',
+			array($this, '_doAnchors_inline_callback'), $text);
+
+		// Last, handle reference-style shortcuts: [link text]
+		// These must come last in case you've also got [link text][1]
+		// or [link text](/foo)
+		$text = preg_replace_callback('{
+			(					# wrap whole match in $1
+			  \[
+				([^\[\]]+)		# link text = $2; can\'t contain [ or ]
+			  \]
+			)
+			}xs',
+			array($this, '_doAnchors_reference_callback'), $text);
+
+		$this->in_anchor = false;
+		return $text;
+	}
+
+	/**
+	 * Callback method to parse referenced anchors
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doAnchors_reference_callback($matches) {
+		$whole_match =  $matches[1];
+		$link_text   =  $matches[2];
+		$link_id     =& $matches[3];
+
+		if ($link_id == "") {
+			// for shortcut links like [this][] or [this].
+			$link_id = $link_text;
+		}
+
+		// lower-case and turn embedded newlines into spaces
+		$link_id = strtolower($link_id);
+		$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+		if (isset($this->urls[$link_id])) {
+			$url = $this->urls[$link_id];
+			$url = $this->encodeURLAttribute($url);
+
+			$result = "<a href=\"$url\"";
+			if ( isset( $this->titles[$link_id] ) ) {
+				$title = $this->titles[$link_id];
+				$title = $this->encodeAttribute($title);
+				$result .=  " title=\"$title\"";
+			}
+
+			$link_text = $this->runSpanGamut($link_text);
+			$result .= ">$link_text</a>";
+			$result = $this->hashPart($result);
+		} else {
+			$result = $whole_match;
+		}
+		return $result;
+	}
+
+	/**
+	 * Callback method to parse inline anchors
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doAnchors_inline_callback($matches) {
+		$link_text		=  $this->runSpanGamut($matches[2]);
+		$url			=  $matches[3] === '' ? $matches[4] : $matches[3];
+		$title			=& $matches[7];
+
+		// If the URL was of the form <s p a c e s> it got caught by the HTML
+		// tag parser and hashed. Need to reverse the process before using
+		// the URL.
+		$unhashed = $this->unhash($url);
+		if ($unhashed !== $url)
+			$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
+
+		$url = $this->encodeURLAttribute($url);
+
+		$result = "<a href=\"$url\"";
+		if ($title) {
+			$title = $this->encodeAttribute($title);
+			$result .=  " title=\"$title\"";
+		}
+
+		$link_text = $this->runSpanGamut($link_text);
+		$result .= ">$link_text</a>";
+
+		return $this->hashPart($result);
+	}
+
+	/**
+	 * Turn Markdown image shortcuts into <img> tags.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doImages($text) {
+		// First, handle reference-style labeled images: ![alt text][id]
+		$text = preg_replace_callback('{
+			(				# wrap whole match in $1
+			  !\[
+				('.$this->nested_brackets_re.')		# alt text = $2
+			  \]
+
+			  [ ]?				# one optional space
+			  (?:\n[ ]*)?		# one optional newline followed by spaces
+
+			  \[
+				(.*?)		# id = $3
+			  \]
+
+			)
+			}xs',
+			array($this, '_doImages_reference_callback'), $text);
+
+		// Next, handle inline images:  ![alt text](url "optional title")
+		// Don't forget: encode * and _
+		$text = preg_replace_callback('{
+			(				# wrap whole match in $1
+			  !\[
+				('.$this->nested_brackets_re.')		# alt text = $2
+			  \]
+			  \s?			# One optional whitespace character
+			  \(			# literal paren
+				[ \n]*
+				(?:
+					<(\S*)>	# src url = $3
+				|
+					('.$this->nested_url_parenthesis_re.')	# src url = $4
+				)
+				[ \n]*
+				(			# $5
+				  ([\'"])	# quote char = $6
+				  (.*?)		# title = $7
+				  \6		# matching quote
+				  [ \n]*
+				)?			# title is optional
+			  \)
+			)
+			}xs',
+			array($this, '_doImages_inline_callback'), $text);
+
+		return $text;
+	}
+
+	/**
+	 * Callback to parse references image tags
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doImages_reference_callback($matches) {
+		$whole_match = $matches[1];
+		$alt_text    = $matches[2];
+		$link_id     = strtolower($matches[3]);
+
+		if ($link_id == "") {
+			$link_id = strtolower($alt_text); // for shortcut links like ![this][].
+		}
+
+		$alt_text = $this->encodeAttribute($alt_text);
+		if (isset($this->urls[$link_id])) {
+			$url = $this->encodeURLAttribute($this->urls[$link_id]);
+			$result = "<img src=\"$url\" alt=\"$alt_text\"";
+			if (isset($this->titles[$link_id])) {
+				$title = $this->titles[$link_id];
+				$title = $this->encodeAttribute($title);
+				$result .=  " title=\"$title\"";
+			}
+			$result .= $this->empty_element_suffix;
+			$result = $this->hashPart($result);
+		} else {
+			// If there's no such link ID, leave intact:
+			$result = $whole_match;
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Callback to parse inline image tags
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doImages_inline_callback($matches) {
+		$whole_match	= $matches[1];
+		$alt_text		= $matches[2];
+		$url			= $matches[3] == '' ? $matches[4] : $matches[3];
+		$title			=& $matches[7];
+
+		$alt_text = $this->encodeAttribute($alt_text);
+		$url = $this->encodeURLAttribute($url);
+		$result = "<img src=\"$url\" alt=\"$alt_text\"";
+		if (isset($title)) {
+			$title = $this->encodeAttribute($title);
+			$result .=  " title=\"$title\""; // $title already quoted
+		}
+		$result .= $this->empty_element_suffix;
+
+		return $this->hashPart($result);
+	}
+
+	/**
+	 * Parse Markdown heading elements to HTML
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doHeaders($text) {
+		/**
+		 * Setext-style headers:
+		 *	  Header 1
+		 *	  ========
+		 *
+		 *	  Header 2
+		 *	  --------
+		 */
+		$text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
+			array($this, '_doHeaders_callback_setext'), $text);
+
+		/**
+		 * atx-style headers:
+		 *   # Header 1
+		 *   ## Header 2
+		 *   ## Header 2 with closing hashes ##
+		 *   ...
+		 *   ###### Header 6
+		 */
+		$text = preg_replace_callback('{
+				^(\#{1,6})	# $1 = string of #\'s
+				[ ]*
+				(.+?)		# $2 = Header text
+				[ ]*
+				\#*			# optional closing #\'s (not counted)
+				\n+
+			}xm',
+			array($this, '_doHeaders_callback_atx'), $text);
+
+		return $text;
+	}
+
+	/**
+	 * Setext header parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doHeaders_callback_setext($matches) {
+		// Terrible hack to check we haven't found an empty list item.
+		if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) {
+			return $matches[0];
+		}
+
+		$level = $matches[2][0] == '=' ? 1 : 2;
+
+		// ID attribute generation
+		$idAtt = $this->_generateIdFromHeaderValue($matches[1]);
+
+		$block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>";
+		return "\n" . $this->hashBlock($block) . "\n\n";
+	}
+
+	/**
+	 * ATX header parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doHeaders_callback_atx($matches) {
+		// ID attribute generation
+		$idAtt = $this->_generateIdFromHeaderValue($matches[2]);
+
+		$level = strlen($matches[1]);
+		$block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>";
+		return "\n" . $this->hashBlock($block) . "\n\n";
+	}
+
+	/**
+	 * If a header_id_func property is set, we can use it to automatically
+	 * generate an id attribute.
+	 *
+	 * This method returns a string in the form id="foo", or an empty string
+	 * otherwise.
+	 * @param  string $headerValue
+	 * @return string
+	 */
+	protected function _generateIdFromHeaderValue($headerValue) {
+		if (!is_callable($this->header_id_func)) {
+			return "";
+		}
+
+		$idValue = call_user_func($this->header_id_func, $headerValue);
+		if (!$idValue) {
+			return "";
+		}
+
+		return ' id="' . $this->encodeAttribute($idValue) . '"';
+	}
+
+	/**
+	 * Form HTML ordered (numbered) and unordered (bulleted) lists.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doLists($text) {
+		$less_than_tab = $this->tab_width - 1;
+
+		// Re-usable patterns to match list item bullets and number markers:
+		$marker_ul_re  = '[*+-]';
+		$marker_ol_re  = '\d+[\.]';
+
+		$markers_relist = array(
+			$marker_ul_re => $marker_ol_re,
+			$marker_ol_re => $marker_ul_re,
+			);
+
+		foreach ($markers_relist as $marker_re => $other_marker_re) {
+			// Re-usable pattern to match any entirel ul or ol list:
+			$whole_list_re = '
+				(								# $1 = whole list
+				  (								# $2
+					([ ]{0,'.$less_than_tab.'})	# $3 = number of spaces
+					('.$marker_re.')			# $4 = first list item marker
+					[ ]+
+				  )
+				  (?s:.+?)
+				  (								# $5
+					  \z
+					|
+					  \n{2,}
+					  (?=\S)
+					  (?!						# Negative lookahead for another list item marker
+						[ ]*
+						'.$marker_re.'[ ]+
+					  )
+					|
+					  (?=						# Lookahead for another kind of list
+					    \n
+						\3						# Must have the same indentation
+						'.$other_marker_re.'[ ]+
+					  )
+				  )
+				)
+			'; // mx
+
+			// We use a different prefix before nested lists than top-level lists.
+			//See extended comment in _ProcessListItems().
+
+			if ($this->list_level) {
+				$text = preg_replace_callback('{
+						^
+						'.$whole_list_re.'
+					}mx',
+					array($this, '_doLists_callback'), $text);
+			} else {
+				$text = preg_replace_callback('{
+						(?:(?<=\n)\n|\A\n?) # Must eat the newline
+						'.$whole_list_re.'
+					}mx',
+					array($this, '_doLists_callback'), $text);
+			}
+		}
+
+		return $text;
+	}
+
+	/**
+	 * List parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doLists_callback($matches) {
+		// Re-usable patterns to match list item bullets and number markers:
+		$marker_ul_re  = '[*+-]';
+		$marker_ol_re  = '\d+[\.]';
+		$marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+		$marker_ol_start_re = '[0-9]+';
+
+		$list = $matches[1];
+		$list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
+
+		$marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+
+		$list .= "\n";
+		$result = $this->processListItems($list, $marker_any_re);
+
+		$ol_start = 1;
+		if ($this->enhanced_ordered_list) {
+			// Get the start number for ordered list.
+			if ($list_type == 'ol') {
+				$ol_start_array = array();
+				$ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array);
+				if ($ol_start_check){
+					$ol_start = $ol_start_array[0];
+				}
+			}
+		}
+
+		if ($ol_start > 1 && $list_type == 'ol'){
+			$result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>");
+		} else {
+			$result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
+		}
+		return "\n". $result ."\n\n";
+	}
+
+	/**
+	 * Nesting tracker for list levels
+	 */
+	protected int $list_level = 0;
+
+	/**
+	 * Process the contents of a single ordered or unordered list, splitting it
+	 * into individual list items.
+	 * @param  string $list_str
+	 * @param  string $marker_any_re
+	 * @return string
+	 */
+	protected function processListItems($list_str, $marker_any_re) {
+		/**
+		 * The $this->list_level global keeps track of when we're inside a list.
+		 * Each time we enter a list, we increment it; when we leave a list,
+		 * we decrement. If it's zero, we're not in a list anymore.
+		 *
+		 * We do this because when we're not inside a list, we want to treat
+		 * something like this:
+		 *
+		 *		I recommend upgrading to version
+		 *		8. Oops, now this line is treated
+		 *		as a sub-list.
+		 *
+		 * As a single paragraph, despite the fact that the second line starts
+		 * with a digit-period-space sequence.
+		 *
+		 * Whereas when we're inside a list (or sub-list), that line will be
+		 * treated as the start of a sub-list. What a kludge, huh? This is
+		 * an aspect of Markdown's syntax that's hard to parse perfectly
+		 * without resorting to mind-reading. Perhaps the solution is to
+		 * change the syntax rules such that sub-lists must start with a
+		 * starting cardinal number; e.g. "1." or "a.".
+		 */
+		$this->list_level++;
+
+		// Trim trailing blank lines:
+		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+		$list_str = preg_replace_callback('{
+			(\n)?							# leading line = $1
+			(^[ ]*)							# leading whitespace = $2
+			('.$marker_any_re.'				# list marker and space = $3
+				(?:[ ]+|(?=\n))	# space only required if item is not empty
+			)
+			((?s:.*?))						# list item text   = $4
+			(?:(\n+(?=\n))|\n)				# tailing blank line = $5
+			(?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+			}xm',
+			array($this, '_processListItems_callback'), $list_str);
+
+		$this->list_level--;
+		return $list_str;
+	}
+
+	/**
+	 * List item parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _processListItems_callback($matches) {
+		$item = $matches[4];
+		$leading_line =& $matches[1];
+		$leading_space =& $matches[2];
+		$marker_space = $matches[3];
+		$tailing_blank_line =& $matches[5];
+
+		if ($leading_line || $tailing_blank_line ||
+			preg_match('/\n{2,}/', $item))
+		{
+			// Replace marker with the appropriate whitespace indentation
+			$item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+			$item = $this->runBlockGamut($this->outdent($item)."\n");
+		} else {
+			// Recursion for sub-lists:
+			$item = $this->doLists($this->outdent($item));
+			$item = $this->formParagraphs($item, false);
+		}
+
+		return "<li>" . $item . "</li>\n";
+	}
+
+	/**
+	 * Process Markdown `<pre><code>` blocks.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doCodeBlocks($text) {
+		$text = preg_replace_callback('{
+				(?:\n\n|\A\n?)
+				(	            # $1 = the code block -- one or more lines, starting with a space/tab
+				  (?>
+					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
+					.*\n+
+				  )+
+				)
+				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
+			}xm',
+			array($this, '_doCodeBlocks_callback'), $text);
+
+		return $text;
+	}
+
+	/**
+	 * Code block parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doCodeBlocks_callback($matches) {
+		$codeblock = $matches[1];
+
+		$codeblock = $this->outdent($codeblock);
+		if (is_callable($this->code_block_content_func)) {
+			$codeblock = call_user_func($this->code_block_content_func, $codeblock, "");
+		} else {
+			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+		}
+
+		# trim leading newlines and trailing newlines
+		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
+
+		$codeblock = "<pre><code>$codeblock\n</code></pre>";
+		return "\n\n" . $this->hashBlock($codeblock) . "\n\n";
+	}
+
+	/**
+	 * Create a code span markup for $code. Called from handleSpanToken.
+	 * @param  string $code
+	 * @return string
+	 */
+	protected function makeCodeSpan($code) {
+		if (is_callable($this->code_span_content_func)) {
+			$code = call_user_func($this->code_span_content_func, $code);
+		} else {
+			$code = htmlspecialchars(trim($code), ENT_NOQUOTES);
+		}
+		return $this->hashPart("<code>$code</code>");
+	}
+
+	/**
+	 * Define the emphasis operators with their regex matches
+	 * @var array
+	 */
+	protected array $em_relist = array(
+		''  => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)',
+		'*' => '(?<![\s*])\*(?!\*)',
+		'_' => '(?<![\s_])_(?!_)',
+	);
+
+	/**
+	 * Define the strong operators with their regex matches
+	 * @var array
+	 */
+	protected array $strong_relist = array(
+		''   => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)',
+		'**' => '(?<![\s*])\*\*(?!\*)',
+		'__' => '(?<![\s_])__(?!_)',
+	);
+
+	/**
+	 * Define the emphasis + strong operators with their regex matches
+	 * @var array
+	 */
+	protected array $em_strong_relist = array(
+		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)',
+		'***' => '(?<![\s*])\*\*\*(?!\*)',
+		'___' => '(?<![\s_])___(?!_)',
+	);
+
+	/**
+	 * Container for prepared regular expressions
+	 */
+	protected ?array $em_strong_prepared_relist = null;
+
+	/**
+	 * Prepare regular expressions for searching emphasis tokens in any
+	 * context.
+	 * @return void
+	 */
+	protected function prepareItalicsAndBold() {
+		foreach ($this->em_relist as $em => $em_re) {
+			foreach ($this->strong_relist as $strong => $strong_re) {
+				// Construct list of allowed token expressions.
+				$token_relist = array();
+				if (isset($this->em_strong_relist["$em$strong"])) {
+					$token_relist[] = $this->em_strong_relist["$em$strong"];
+				}
+				$token_relist[] = $em_re;
+				$token_relist[] = $strong_re;
+
+				// Construct master expression from list.
+				$token_re = '{(' . implode('|', $token_relist) . ')}';
+				$this->em_strong_prepared_relist["$em$strong"] = $token_re;
+			}
+		}
+	}
+
+	/**
+	 * Convert Markdown italics (emphasis) and bold (strong) to HTML
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doItalicsAndBold($text) {
+		if ($this->in_emphasis_processing) {
+			return $text; // avoid reentrency
+		}
+		$this->in_emphasis_processing = true;
+
+		$token_stack = array('');
+		$text_stack = array('');
+		$em = '';
+		$strong = '';
+		$tree_char_em = false;
+
+		while (1) {
+			// Get prepared regular expression for seraching emphasis tokens
+			// in current context.
+			$token_re = $this->em_strong_prepared_relist["$em$strong"];
+
+			// Each loop iteration search for the next emphasis token.
+			// Each token is then passed to handleSpanToken.
+			$parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+			$text_stack[0] .= $parts[0];
+			$token =& $parts[1];
+			$text =& $parts[2];
+
+			if (empty($token)) {
+				// Reached end of text span: empty stack without emitting.
+				// any more emphasis.
+				while ($token_stack[0]) {
+					$text_stack[1] .= array_shift($token_stack);
+					$text_stack[0] .= array_shift($text_stack);
+				}
+				break;
+			}
+
+			$token_len = strlen($token);
+			if ($tree_char_em) {
+				// Reached closing marker while inside a three-char emphasis.
+				if ($token_len == 3) {
+					// Three-char closing marker, close em and strong.
+					array_shift($token_stack);
+					$span = array_shift($text_stack);
+					$span = $this->runSpanGamut($span);
+					$span = "<strong><em>$span</em></strong>";
+					$text_stack[0] .= $this->hashPart($span);
+					$em = '';
+					$strong = '';
+				} else {
+					// Other closing marker: close one em or strong and
+					// change current token state to match the other
+					$token_stack[0] = str_repeat($token[0], 3-$token_len);
+					$tag = $token_len == 2 ? "strong" : "em";
+					$span = $text_stack[0];
+					$span = $this->runSpanGamut($span);
+					$span = "<$tag>$span</$tag>";
+					$text_stack[0] = $this->hashPart($span);
+					$$tag = ''; // $$tag stands for $em or $strong
+				}
+				$tree_char_em = false;
+			} else if ($token_len == 3) {
+				if ($em) {
+					// Reached closing marker for both em and strong.
+					// Closing strong marker:
+					for ($i = 0; $i < 2; ++$i) {
+						$shifted_token = array_shift($token_stack);
+						$tag = strlen($shifted_token) == 2 ? "strong" : "em";
+						$span = array_shift($text_stack);
+						$span = $this->runSpanGamut($span);
+						$span = "<$tag>$span</$tag>";
+						$text_stack[0] .= $this->hashPart($span);
+						$$tag = ''; // $$tag stands for $em or $strong
+					}
+				} else {
+					// Reached opening three-char emphasis marker. Push on token
+					// stack; will be handled by the special condition above.
+					$em = $token[0];
+					$strong = "$em$em";
+					array_unshift($token_stack, $token);
+					array_unshift($text_stack, '');
+					$tree_char_em = true;
+				}
+			} else if ($token_len == 2) {
+				if ($strong) {
+					// Unwind any dangling emphasis marker:
+					if (strlen($token_stack[0]) == 1) {
+						$text_stack[1] .= array_shift($token_stack);
+						$text_stack[0] .= array_shift($text_stack);
+						$em = '';
+					}
+					// Closing strong marker:
+					array_shift($token_stack);
+					$span = array_shift($text_stack);
+					$span = $this->runSpanGamut($span);
+					$span = "<strong>$span</strong>";
+					$text_stack[0] .= $this->hashPart($span);
+					$strong = '';
+				} else {
+					array_unshift($token_stack, $token);
+					array_unshift($text_stack, '');
+					$strong = $token;
+				}
+			} else {
+				// Here $token_len == 1
+				if ($em) {
+					if (strlen($token_stack[0]) == 1) {
+						// Closing emphasis marker:
+						array_shift($token_stack);
+						$span = array_shift($text_stack);
+						$span = $this->runSpanGamut($span);
+						$span = "<em>$span</em>";
+						$text_stack[0] .= $this->hashPart($span);
+						$em = '';
+					} else {
+						$text_stack[0] .= $token;
+					}
+				} else {
+					array_unshift($token_stack, $token);
+					array_unshift($text_stack, '');
+					$em = $token;
+				}
+			}
+		}
+		$this->in_emphasis_processing = false;
+		return $text_stack[0];
+	}
+
+	/**
+	 * Parse Markdown blockquotes to HTML
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doBlockQuotes($text) {
+		$text = preg_replace_callback('/
+			  (								# Wrap whole match in $1
+				(?>
+				  ^[ ]*>[ ]?			# ">" at the start of a line
+					.+\n					# rest of the first line
+				  (.+\n)*					# subsequent consecutive lines
+				  \n*						# blanks
+				)+
+			  )
+			/xm',
+			array($this, '_doBlockQuotes_callback'), $text);
+
+		return $text;
+	}
+
+	/**
+	 * Blockquote parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doBlockQuotes_callback($matches) {
+		$bq = $matches[1];
+		// trim one level of quoting - trim whitespace-only lines
+		$bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
+		$bq = $this->runBlockGamut($bq); // recurse
+
+		$bq = preg_replace('/^/m', "  ", $bq);
+		// These leading spaces cause problem with <pre> content,
+		// so we need to fix that:
+		$bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
+			array($this, '_doBlockQuotes_callback2'), $bq);
+
+		return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n";
+	}
+
+	/**
+	 * Blockquote parsing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doBlockQuotes_callback2($matches) {
+		$pre = $matches[1];
+		$pre = preg_replace('/^  /m', '', $pre);
+		return $pre;
+	}
+
+	/**
+	 * Parse paragraphs
+	 *
+	 * @param  string $text String to process in paragraphs
+	 * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
+	 * @return string
+	 */
+	protected function formParagraphs($text, $wrap_in_p = true) {
+		// Strip leading and trailing lines:
+		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+		// Wrap <p> tags and unhashify HTML blocks
+		foreach ($grafs as $key => $value) {
+			if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
+				// Is a paragraph.
+				$value = $this->runSpanGamut($value);
+				if ($wrap_in_p) {
+					$value = preg_replace('/^([ ]*)/', "<p>", $value);
+					$value .= "</p>";
+				}
+				$grafs[$key] = $this->unhash($value);
+			} else {
+				// Is a block.
+				// Modify elements of @grafs in-place...
+				$graf = $value;
+				$block = $this->html_hashes[$graf];
+				$graf = $block;
+//				if (preg_match('{
+//					\A
+//					(							# $1 = <div> tag
+//					  <div  \s+
+//					  [^>]*
+//					  \b
+//					  markdown\s*=\s*  ([\'"])	#	$2 = attr quote char
+//					  1
+//					  \2
+//					  [^>]*
+//					  >
+//					)
+//					(							# $3 = contents
+//					.*
+//					)
+//					(</div>)					# $4 = closing tag
+//					\z
+//					}xs', $block, $matches))
+//				{
+//					list(, $div_open, , $div_content, $div_close) = $matches;
+//
+//					// We can't call Markdown(), because that resets the hash;
+//					// that initialization code should be pulled into its own sub, though.
+//					$div_content = $this->hashHTMLBlocks($div_content);
+//
+//					// Run document gamut methods on the content.
+//					foreach ($this->document_gamut as $method => $priority) {
+//						$div_content = $this->$method($div_content);
+//					}
+//
+//					$div_open = preg_replace(
+//						'{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open);
+//
+//					$graf = $div_open . "\n" . $div_content . "\n" . $div_close;
+//				}
+				$grafs[$key] = $graf;
+			}
+		}
+
+		return implode("\n\n", $grafs);
+	}
+
+	/**
+	 * Encode text for a double-quoted HTML attribute. This function
+	 * is *not* suitable for attributes enclosed in single quotes.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function encodeAttribute($text) {
+		$text = $this->encodeAmpsAndAngles($text);
+		$text = str_replace('"', '&quot;', $text);
+		return $text;
+	}
+
+	/**
+	 * Encode text for a double-quoted HTML attribute containing a URL,
+	 * applying the URL filter if set. Also generates the textual
+	 * representation for the URL (removing mailto: or tel:) storing it in $text.
+	 * This function is *not* suitable for attributes enclosed in single quotes.
+	 *
+	 * @param  string $url
+	 * @param  string $text Passed by reference
+	 * @return string        URL
+	 */
+	protected function encodeURLAttribute($url, &$text = null) {
+		if (is_callable($this->url_filter_func)) {
+			$url = call_user_func($this->url_filter_func, $url);
+		}
+
+		if (preg_match('{^mailto:}i', $url)) {
+			$url = $this->encodeEntityObfuscatedAttribute($url, $text, 7);
+		} else if (preg_match('{^tel:}i', $url)) {
+			$url = $this->encodeAttribute($url);
+			$text = substr($url, 4);
+		} else {
+			$url = $this->encodeAttribute($url);
+			$text = $url;
+		}
+
+		return $url;
+	}
+
+	/**
+	 * Smart processing for ampersands and angle brackets that need to
+	 * be encoded. Valid character entities are left alone unless the
+	 * no-entities mode is set.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function encodeAmpsAndAngles($text) {
+		if ($this->no_entities) {
+			$text = str_replace('&', '&amp;', $text);
+		} else {
+			// Ampersand-encoding based entirely on Nat Irons's Amputator
+			// MT plugin: <http://bumppo.net/projects/amputator/>
+			$text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
+								'&amp;', $text);
+		}
+		// Encode remaining <'s
+		$text = str_replace('<', '&lt;', $text);
+
+		return $text;
+	}
+
+	/**
+	 * Parse Markdown automatic links to anchor HTML tags
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function doAutoLinks($text) {
+		$text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i',
+			array($this, '_doAutoLinks_url_callback'), $text);
+
+		// Email addresses: <address@domain.foo>
+		$text = preg_replace_callback('{
+			<
+			(?:mailto:)?
+			(
+				(?:
+					[-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+
+				|
+					".*?"
+				)
+				\@
+				(?:
+					[-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
+				|
+					\[[\d.a-fA-F:]+\]	# IPv4 & IPv6
+				)
+			)
+			>
+			}xi',
+			array($this, '_doAutoLinks_email_callback'), $text);
+
+		return $text;
+	}
+
+	/**
+	 * Parse URL callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doAutoLinks_url_callback($matches) {
+		$url = $this->encodeURLAttribute($matches[1], $text);
+		$link = "<a href=\"$url\">$text</a>";
+		return $this->hashPart($link);
+	}
+
+	/**
+	 * Parse email address callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _doAutoLinks_email_callback($matches) {
+		$addr = $matches[1];
+		$url = $this->encodeURLAttribute("mailto:$addr", $text);
+		$link = "<a href=\"$url\">$text</a>";
+		return $this->hashPart($link);
+	}
+
+	/**
+	 * Input: some text to obfuscate, e.g. "mailto:foo@example.com"
+	 *
+	 * Output: the same text but with most characters encoded as either a
+	 *         decimal or hex entity, in the hopes of foiling most address
+	 *         harvesting spam bots. E.g.:
+	 *
+	 *        &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
+	 *        &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
+	 *        &#x6d;
+	 *
+	 * Note: the additional output $tail is assigned the same value as the
+	 * ouput, minus the number of characters specified by $head_length.
+	 *
+	 * Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+	 * With some optimizations by Milian Wolff. Forced encoding of HTML
+	 * attribute special characters by Allan Odgaard.
+	 *
+	 * @param  string  $text
+	 * @param  string  $tail Passed by reference
+	 * @param  integer $head_length
+	 * @return string
+	 */
+	protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) {
+		if ($text == "") {
+			return $tail = "";
+		}
+
+		$chars = preg_split('/(?<!^)(?!$)/', $text);
+		$seed = (int)abs(crc32($text) / strlen($text)); // Deterministic seed.
+
+		foreach ($chars as $key => $char) {
+			$ord = ord($char);
+			// Ignore non-ascii chars.
+			if ($ord < 128) {
+				$r = ($seed * (1 + $key)) % 100; // Pseudo-random function.
+				// roughly 10% raw, 45% hex, 45% dec
+				// '@' *must* be encoded. I insist.
+				// '"' and '>' have to be encoded inside the attribute
+				if ($r > 90 && strpos('@"&>', $char) === false) {
+					/* do nothing */
+				} else if ($r < 45) {
+					$chars[$key] = '&#x'.dechex($ord).';';
+				} else {
+					$chars[$key] = '&#'.$ord.';';
+				}
+			}
+		}
+
+		$text = implode('', $chars);
+		$tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text;
+
+		return $text;
+	}
+
+	/**
+	 * Take the string $str and parse it into tokens, hashing embeded HTML,
+	 * escaped characters and handling code spans.
+	 * @param  string $str
+	 * @return string
+	 */
+	protected function parseSpan($str) {
+		$output = '';
+
+		$span_re = '{
+				(
+					\\\\'.$this->escape_chars_re.'
+				|
+					(?<![`\\\\])
+					`+						# code span marker
+			'.( $this->no_markup ? '' : '
+				|
+					<!--    .*?     -->		# comment
+				|
+					<\?.*?\?> | <%.*?%>		# processing instruction
+				|
+					<[!$]?[-a-zA-Z0-9:_]+	# regular tags
+					(?>
+						\s
+						(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+					)?
+					>
+				|
+					<[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag
+				|
+					</[-a-zA-Z0-9:_]+\s*> # closing tag
+			').'
+				)
+				}xs';
+
+		while (1) {
+			// Each loop iteration seach for either the next tag, the next
+			// openning code span marker, or the next escaped character.
+			// Each token is then passed to handleSpanToken.
+			$parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+			// Create token from text preceding tag.
+			if ($parts[0] != "") {
+				$output .= $parts[0];
+			}
+
+			// Check if we reach the end.
+			if (isset($parts[1])) {
+				$output .= $this->handleSpanToken($parts[1], $parts[2]);
+				$str = $parts[2];
+			} else {
+				break;
+			}
+		}
+
+		return $output;
+	}
+
+	/**
+	 * Handle $token provided by parseSpan by determining its nature and
+	 * returning the corresponding value that should replace it.
+	 * @param  string $token
+	 * @param  string $str Passed by reference
+	 * @return string
+	 */
+	protected function handleSpanToken($token, &$str) {
+		switch ($token[0]) {
+			case "\\":
+				return $this->hashPart("&#". ord($token[1]). ";");
+			case "`":
+				// Search for end marker in remaining text.
+				if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+					$str, $matches))
+				{
+					$str = $matches[2];
+					$codespan = $this->makeCodeSpan($matches[1]);
+					return $this->hashPart($codespan);
+				}
+				return $token; // Return as text since no ending marker found.
+			default:
+				return $this->hashPart($token);
+		}
+	}
+
+	/**
+	 * Remove one level of line-leading tabs or spaces
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function outdent($text) {
+		return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text);
+	}
+
+
+	/**
+	 * String length function for detab. `_initDetab` will create a function to
+	 * handle UTF-8 if the default function does not exist.
+	 * can be a string or function
+	 */
+	protected $utf8_strlen = 'mb_strlen';
+
+	/**
+	 * Replace tabs with the appropriate amount of spaces.
+	 *
+	 * For each line we separate the line in blocks delemited by tab characters.
+	 * Then we reconstruct every line by adding the  appropriate number of space
+	 * between each blocks.
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function detab($text) {
+		$text = preg_replace_callback('/^.*\t.*$/m',
+			array($this, '_detab_callback'), $text);
+
+		return $text;
+	}
+
+	/**
+	 * Replace tabs callback
+	 * @param  string $matches
+	 * @return string
+	 */
+	protected function _detab_callback($matches) {
+		$line = $matches[0];
+		$strlen = $this->utf8_strlen; // strlen function for UTF-8.
+
+		// Split in blocks.
+		$blocks = explode("\t", $line);
+		// Add each blocks to the line.
+		$line = $blocks[0];
+		unset($blocks[0]); // Do not add first block twice.
+		foreach ($blocks as $block) {
+			// Calculate amount of space, insert spaces, insert block.
+			$amount = $this->tab_width -
+				$strlen($line, 'UTF-8') % $this->tab_width;
+			$line .= str_repeat(" ", $amount) . $block;
+		}
+		return $line;
+	}
+
+	/**
+	 * Check for the availability of the function in the `utf8_strlen` property
+	 * (initially `mb_strlen`). If the function is not available, create a
+	 * function that will loosely count the number of UTF-8 characters with a
+	 * regular expression.
+	 * @return void
+	 */
+	protected function _initDetab() {
+
+		if (function_exists($this->utf8_strlen)) {
+			return;
+		}
+
+		$this->utf8_strlen = fn($text) => preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/', $text, $m);
+	}
+
+	/**
+	 * Swap back in all the tags hashed by _HashHTMLBlocks.
+	 * @param  string $text
+	 * @return string
+	 */
+	protected function unhash($text) {
+		return preg_replace_callback('/(.)\x1A[0-9]+\1/',
+			array($this, '_unhash_callback'), $text);
+	}
+
+	/**
+	 * Unhashing callback
+	 * @param  array $matches
+	 * @return string
+	 */
+	protected function _unhash_callback($matches) {
+		return $this->html_hashes[$matches[0]];
+	}
+}

+ 38 - 0
third_party/markdown/MarkdownInterface.php

@@ -0,0 +1,38 @@
+<?php
+/**
+ * Markdown  -  A text-to-HTML conversion tool for web writers
+ *
+ * @package   php-markdown
+ * @author    Michel Fortin <michel.fortin@michelf.com>
+ * @copyright 2004-2022 Michel Fortin <https://michelf.com/projects/php-markdown/>
+ * @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/>
+ */
+
+namespace Michelf;
+
+/**
+ * Markdown Parser Interface
+ */
+interface MarkdownInterface {
+	/**
+	 * Initialize the parser and return the result of its transform method.
+	 * This will work fine for derived classes too.
+	 *
+	 * @api
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	public static function defaultTransform(string $text): string;
+
+	/**
+	 * Main function. Performs some preprocessing on the input text
+	 * and pass it through the document gamut.
+	 *
+	 * @api
+	 *
+	 * @param  string $text
+	 * @return string
+	 */
+	public function transform(string $text): string;
+}

+ 106 - 0
third_party/smarty/Autoloader.php

@@ -0,0 +1,106 @@
+<?php
+/**
+ * Smarty Autoloader
+ *
+ * @package Smarty
+ */
+
+/**
+ * Smarty Autoloader
+ *
+ * @package Smarty
+ * @author  Uwe Tews
+ *             Usage:
+ *                  require_once '...path/Autoloader.php';
+ *                  Smarty_Autoloader::register();
+ *             or
+ *                  include '...path/bootstrap.php';
+ *
+ *                  $smarty = new Smarty();
+ */
+class Smarty_Autoloader
+{
+    /**
+     * Filepath to Smarty root
+     *
+     * @var string
+     */
+    public static $SMARTY_DIR = null;
+
+    /**
+     * Filepath to Smarty internal plugins
+     *
+     * @var string
+     */
+    public static $SMARTY_SYSPLUGINS_DIR = null;
+
+    /**
+     * Array with Smarty core classes and their filename
+     *
+     * @var array
+     */
+    public static $rootClasses = array('smarty' => 'Smarty.class.php');
+
+    /**
+     * Registers Smarty_Autoloader backward compatible to older installations.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not.
+     */
+    public static function registerBC($prepend = false)
+    {
+        /**
+         * register the class autoloader
+         */
+        if (!defined('SMARTY_SPL_AUTOLOAD')) {
+            define('SMARTY_SPL_AUTOLOAD', 0);
+        }
+        if (SMARTY_SPL_AUTOLOAD
+            && set_include_path(get_include_path() . PATH_SEPARATOR . SMARTY_SYSPLUGINS_DIR) !== false
+        ) {
+            $registeredAutoLoadFunctions = spl_autoload_functions();
+            if (!isset($registeredAutoLoadFunctions[ 'spl_autoload' ])) {
+                spl_autoload_register();
+            }
+        } else {
+            self::register($prepend);
+        }
+    }
+
+    /**
+     * Registers Smarty_Autoloader as an SPL autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not.
+     */
+    public static function register($prepend = false)
+    {
+        self::$SMARTY_DIR = defined('SMARTY_DIR') ? SMARTY_DIR : dirname(__FILE__) . DIRECTORY_SEPARATOR;
+        self::$SMARTY_SYSPLUGINS_DIR = defined('SMARTY_SYSPLUGINS_DIR') ? SMARTY_SYSPLUGINS_DIR :
+            self::$SMARTY_DIR . 'sysplugins' . DIRECTORY_SEPARATOR;
+        spl_autoload_register(array(__CLASS__, 'autoload'), true, $prepend);
+    }
+
+    /**
+     * Handles auto loading of classes.
+     *
+     * @param string $class A class name.
+     */
+    public static function autoload($class)
+    {
+        if ($class[ 0 ] !== 'S' || strpos($class, 'Smarty') !== 0) {
+            return;
+        }
+        $_class = strtolower($class);
+        if (isset(self::$rootClasses[ $_class ])) {
+            $file = self::$SMARTY_DIR . self::$rootClasses[ $_class ];
+            if (is_file($file)) {
+                include $file;
+            }
+        } else {
+            $file = self::$SMARTY_SYSPLUGINS_DIR . $_class . '.php';
+            if (is_file($file)) {
+                include $file;
+            }
+        }
+        return;
+    }
+}

+ 179 - 0
third_party/smarty/LICENSE

@@ -0,0 +1,179 @@
+Smarty: the PHP compiling template engine
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 3.0 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ See the GNU Lesser General Public License below for more details.
+
+
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.

+ 1397 - 0
third_party/smarty/Smarty.class.php

@@ -0,0 +1,1397 @@
+<?php
+/**
+ * Project:     Smarty: the PHP compiling template engine
+ * File:        Smarty.class.php
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * For questions, help, comments, discussion, etc., please join the
+ * Smarty mailing list. Send a blank e-mail to
+ * smarty-discussion-subscribe@googlegroups.com
+ *
+ * @link      https://www.smarty.net/
+ * @copyright 2018 New Digital Group, Inc.
+ * @copyright 2018 Uwe Tews
+ * @author    Monte Ohrt <monte at ohrt dot com>
+ * @author    Uwe Tews   <uwe dot tews at gmail dot com>
+ * @author    Rodney Rehm
+ * @package   Smarty
+ */
+/**
+ * set SMARTY_DIR to absolute path to Smarty library files.
+ * Sets SMARTY_DIR only if user application has not already defined it.
+ */
+if (!defined('SMARTY_DIR')) {
+    /**
+     *
+     */
+    define('SMARTY_DIR', dirname(__FILE__) . DIRECTORY_SEPARATOR);
+}
+/**
+ * set SMARTY_SYSPLUGINS_DIR to absolute path to Smarty internal plugins.
+ * Sets SMARTY_SYSPLUGINS_DIR only if user application has not already defined it.
+ */
+if (!defined('SMARTY_SYSPLUGINS_DIR')) {
+    /**
+     *
+     */
+    define('SMARTY_SYSPLUGINS_DIR', SMARTY_DIR . 'sysplugins' . DIRECTORY_SEPARATOR);
+}
+if (!defined('SMARTY_PLUGINS_DIR')) {
+    /**
+     *
+     */
+    define('SMARTY_PLUGINS_DIR', SMARTY_DIR . 'plugins' . DIRECTORY_SEPARATOR);
+}
+if (!defined('SMARTY_MBSTRING')) {
+    /**
+     *
+     */
+    define('SMARTY_MBSTRING', function_exists('mb_get_info'));
+}
+/**
+ * Load Smarty_Autoloader
+ */
+if (!class_exists('Smarty_Autoloader')) {
+    include dirname(__FILE__) . '/bootstrap.php';
+}
+/**
+ * Load always needed external class files
+ */
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_internal_data.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_internal_extension_handler.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_internal_templatebase.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_internal_template.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_resource.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_variable.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_template_source.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_template_resource_base.php';
+require_once SMARTY_SYSPLUGINS_DIR . 'smarty_internal_resource_file.php';
+
+/**
+ * This is the main Smarty class
+ *
+ * @package Smarty
+ *
+ * The following methods will be dynamically loaded by the extension handler when they are called.
+ * They are located in a corresponding Smarty_Internal_Method_xxxx class
+ *
+ * @method int clearAllCache(int $exp_time = null, string $type = null)
+ * @method int clearCache(string $template_name, string $cache_id = null, string $compile_id = null, int $exp_time = null, string $type = null)
+ * @method int compileAllTemplates(string $extension = '.tpl', bool $force_compile = false, int $time_limit = 0, $max_errors = null)
+ * @method int compileAllConfig(string $extension = '.conf', bool $force_compile = false, int $time_limit = 0, $max_errors = null)
+ * @method int clearCompiledTemplate($resource_name = null, $compile_id = null, $exp_time = null)
+ */
+class Smarty extends Smarty_Internal_TemplateBase
+{
+    /**
+     * smarty version
+     */
+    const SMARTY_VERSION = '4.1.0';
+    /**
+     * define variable scopes
+     */
+    const SCOPE_LOCAL    = 1;
+    const SCOPE_PARENT   = 2;
+    const SCOPE_TPL_ROOT = 4;
+    const SCOPE_ROOT     = 8;
+    const SCOPE_SMARTY   = 16;
+    const SCOPE_GLOBAL   = 32;
+    /**
+     * define caching modes
+     */
+    const CACHING_OFF              = 0;
+    const CACHING_LIFETIME_CURRENT = 1;
+    const CACHING_LIFETIME_SAVED   = 2;
+    /**
+     * define constant for clearing cache files be saved expiration dates
+     */
+    const CLEAR_EXPIRED = -1;
+    /**
+     * define compile check modes
+     */
+    const COMPILECHECK_OFF       = 0;
+    const COMPILECHECK_ON        = 1;
+    const COMPILECHECK_CACHEMISS = 2;
+    /**
+     * define debug modes
+     */
+    const DEBUG_OFF        = 0;
+    const DEBUG_ON         = 1;
+    const DEBUG_INDIVIDUAL = 2;
+
+    /**
+     * filter types
+     */
+    const FILTER_POST     = 'post';
+    const FILTER_PRE      = 'pre';
+    const FILTER_OUTPUT   = 'output';
+    const FILTER_VARIABLE = 'variable';
+    /**
+     * plugin types
+     */
+    const PLUGIN_FUNCTION         = 'function';
+    const PLUGIN_BLOCK            = 'block';
+    const PLUGIN_COMPILER         = 'compiler';
+    const PLUGIN_MODIFIER         = 'modifier';
+    const PLUGIN_MODIFIERCOMPILER = 'modifiercompiler';
+
+    /**
+     * assigned global tpl vars
+     */
+    public static $global_tpl_vars = array();
+
+    /**
+     * Flag denoting if Multibyte String functions are available
+     */
+    public static $_MBSTRING = SMARTY_MBSTRING;
+
+    /**
+     * The character set to adhere to (e.g. "UTF-8")
+     */
+    public static $_CHARSET = SMARTY_MBSTRING ? 'UTF-8' : 'ISO-8859-1';
+
+    /**
+     * The date format to be used internally
+     * (accepts date() and strftime())
+     */
+    public static $_DATE_FORMAT = '%b %e, %Y';
+
+    /**
+     * Flag denoting if PCRE should run in UTF-8 mode
+     */
+    public static $_UTF8_MODIFIER = 'u';
+
+    /**
+     * Flag denoting if operating system is windows
+     */
+    public static $_IS_WINDOWS = false;
+
+    /**
+     * auto literal on delimiters with whitespace
+     *
+     * @var boolean
+     */
+    public $auto_literal = true;
+
+    /**
+     * display error on not assigned variables
+     *
+     * @var boolean
+     */
+    public $error_unassigned = false;
+
+    /**
+     * look up relative file path in include_path
+     *
+     * @var boolean
+     */
+    public $use_include_path = false;
+
+    /**
+     * flag if template_dir is normalized
+     *
+     * @var bool
+     */
+    public $_templateDirNormalized = false;
+
+    /**
+     * joined template directory string used in cache keys
+     *
+     * @var string
+     */
+    public $_joined_template_dir = null;
+
+    /**
+     * flag if config_dir is normalized
+     *
+     * @var bool
+     */
+    public $_configDirNormalized = false;
+
+    /**
+     * joined config directory string used in cache keys
+     *
+     * @var string
+     */
+    public $_joined_config_dir = null;
+
+    /**
+     * default template handler
+     *
+     * @var callable
+     */
+    public $default_template_handler_func = null;
+
+    /**
+     * default config handler
+     *
+     * @var callable
+     */
+    public $default_config_handler_func = null;
+
+    /**
+     * default plugin handler
+     *
+     * @var callable
+     */
+    public $default_plugin_handler_func = null;
+
+    /**
+     * flag if template_dir is normalized
+     *
+     * @var bool
+     */
+    public $_compileDirNormalized = false;
+
+    /**
+     * flag if plugins_dir is normalized
+     *
+     * @var bool
+     */
+    public $_pluginsDirNormalized = false;
+
+    /**
+     * flag if template_dir is normalized
+     *
+     * @var bool
+     */
+    public $_cacheDirNormalized = false;
+
+    /**
+     * force template compiling?
+     *
+     * @var boolean
+     */
+    public $force_compile = false;
+
+    /**
+     * use sub dirs for compiled/cached files?
+     *
+     * @var boolean
+     */
+    public $use_sub_dirs = false;
+
+    /**
+     * allow ambiguous resources (that are made unique by the resource handler)
+     *
+     * @var boolean
+     */
+    public $allow_ambiguous_resources = false;
+
+    /**
+     * merge compiled includes
+     *
+     * @var boolean
+     */
+    public $merge_compiled_includes = false;
+
+    /*
+    * flag for behaviour when extends: resource  and {extends} tag are used simultaneous
+    *   if false disable execution of {extends} in templates called by extends resource.
+    *   (behaviour as versions < 3.1.28)
+    *
+    * @var boolean
+    */
+    public $extends_recursion = true;
+
+    /**
+     * force cache file creation
+     *
+     * @var boolean
+     */
+    public $force_cache = false;
+
+    /**
+     * template left-delimiter
+     *
+     * @var string
+     */
+    public $left_delimiter = "{";
+
+    /**
+     * template right-delimiter
+     *
+     * @var string
+     */
+    public $right_delimiter = "}";
+
+    /**
+     * array of strings which shall be treated as literal by compiler
+     *
+     * @var array string
+     */
+    public $literals = array();
+
+    /**
+     * class name
+     * This should be instance of Smarty_Security.
+     *
+     * @var string
+     * @see Smarty_Security
+     */
+    public $security_class = 'Smarty_Security';
+
+    /**
+     * implementation of security class
+     *
+     * @var Smarty_Security
+     */
+    public $security_policy = null;
+
+    /**
+     * controls if the php template file resource is allowed
+     *
+     * @var bool
+     */
+    public $allow_php_templates = false;
+
+    /**
+     * debug mode
+     * Setting this to true enables the debug-console.
+     *
+     * @var boolean
+     */
+    public $debugging = false;
+
+    /**
+     * This determines if debugging is enable-able from the browser.
+     * <ul>
+     *  <li>NONE => no debugging control allowed</li>
+     *  <li>URL => enable debugging when SMARTY_DEBUG is found in the URL.</li>
+     * </ul>
+     *
+     * @var string
+     */
+    public $debugging_ctrl = 'NONE';
+
+    /**
+     * Name of debugging URL-param.
+     * Only used when $debugging_ctrl is set to 'URL'.
+     * The name of the URL-parameter that activates debugging.
+     *
+     * @var string
+     */
+    public $smarty_debug_id = 'SMARTY_DEBUG';
+
+    /**
+     * Path of debug template.
+     *
+     * @var string
+     */
+    public $debug_tpl = null;
+
+    /**
+     * When set, smarty uses this value as error_reporting-level.
+     *
+     * @var int
+     */
+    public $error_reporting = null;
+
+    /**
+     * Controls whether variables with the same name overwrite each other.
+     *
+     * @var boolean
+     */
+    public $config_overwrite = true;
+
+    /**
+     * Controls whether config values of on/true/yes and off/false/no get converted to boolean.
+     *
+     * @var boolean
+     */
+    public $config_booleanize = true;
+
+    /**
+     * Controls whether hidden config sections/vars are read from the file.
+     *
+     * @var boolean
+     */
+    public $config_read_hidden = false;
+
+    /**
+     * locking concurrent compiles
+     *
+     * @var boolean
+     */
+    public $compile_locking = true;
+
+    /**
+     * Controls whether cache resources should use locking mechanism
+     *
+     * @var boolean
+     */
+    public $cache_locking = false;
+
+    /**
+     * seconds to wait for acquiring a lock before ignoring the write lock
+     *
+     * @var float
+     */
+    public $locking_timeout = 10;
+
+    /**
+     * resource type used if none given
+     * Must be an valid key of $registered_resources.
+     *
+     * @var string
+     */
+    public $default_resource_type = 'file';
+
+    /**
+     * caching type
+     * Must be an element of $cache_resource_types.
+     *
+     * @var string
+     */
+    public $caching_type = 'file';
+
+    /**
+     * config type
+     *
+     * @var string
+     */
+    public $default_config_type = 'file';
+
+    /**
+     * check If-Modified-Since headers
+     *
+     * @var boolean
+     */
+    public $cache_modified_check = false;
+
+    /**
+     * registered plugins
+     *
+     * @var array
+     */
+    public $registered_plugins = array();
+
+    /**
+     * registered objects
+     *
+     * @var array
+     */
+    public $registered_objects = array();
+
+    /**
+     * registered classes
+     *
+     * @var array
+     */
+    public $registered_classes = array();
+
+    /**
+     * registered filters
+     *
+     * @var array
+     */
+    public $registered_filters = array();
+
+    /**
+     * registered resources
+     *
+     * @var array
+     */
+    public $registered_resources = array();
+
+    /**
+     * registered cache resources
+     *
+     * @var array
+     */
+    public $registered_cache_resources = array();
+
+    /**
+     * autoload filter
+     *
+     * @var array
+     */
+    public $autoload_filters = array();
+
+    /**
+     * default modifier
+     *
+     * @var array
+     */
+    public $default_modifiers = array();
+
+    /**
+     * autoescape variable output
+     *
+     * @var boolean
+     */
+    public $escape_html = false;
+
+    /**
+     * start time for execution time calculation
+     *
+     * @var int
+     */
+    public $start_time = 0;
+
+    /**
+     * required by the compiler for BC
+     *
+     * @var string
+     */
+    public $_current_file = null;
+
+    /**
+     * internal flag to enable parser debugging
+     *
+     * @var bool
+     */
+    public $_parserdebug = false;
+
+    /**
+     * This object type (Smarty = 1, template = 2, data = 4)
+     *
+     * @var int
+     */
+    public $_objType = 1;
+
+    /**
+     * Debug object
+     *
+     * @var Smarty_Internal_Debug
+     */
+    public $_debug = null;
+
+    /**
+     * template directory
+     *
+     * @var array
+     */
+    protected $template_dir = array('./templates/');
+
+    /**
+     * flags for normalized template directory entries
+     *
+     * @var array
+     */
+    protected $_processedTemplateDir = array();
+
+    /**
+     * config directory
+     *
+     * @var array
+     */
+    protected $config_dir = array('./configs/');
+
+    /**
+     * flags for normalized template directory entries
+     *
+     * @var array
+     */
+    protected $_processedConfigDir = array();
+
+    /**
+     * compile directory
+     *
+     * @var string
+     */
+    protected $compile_dir = './templates_c/';
+
+    /**
+     * plugins directory
+     *
+     * @var array
+     */
+    protected $plugins_dir = array();
+
+    /**
+     * cache directory
+     *
+     * @var string
+     */
+    protected $cache_dir = './cache/';
+
+    /**
+     * removed properties
+     *
+     * @var string[]
+     */
+    protected $obsoleteProperties = array(
+        'resource_caching', 'template_resource_caching', 'direct_access_security',
+        '_dir_perms', '_file_perms', 'plugin_search_order',
+        'inheritance_merge_compiled_includes', 'resource_cache_mode',
+    );
+
+    /**
+     * List of private properties which will call getter/setter on a direct access
+     *
+     * @var string[]
+     */
+    protected $accessMap = array(
+        'template_dir' => 'TemplateDir', 'config_dir' => 'ConfigDir',
+        'plugins_dir'  => 'PluginsDir', 'compile_dir' => 'CompileDir',
+        'cache_dir'    => 'CacheDir',
+    );
+
+    /**
+     * PHP7 Compatibility mode
+     * @var bool
+     */
+    private $isMutingUndefinedOrNullWarnings = false;
+
+    /**
+     * Initialize new Smarty object
+     */
+    public function __construct()
+    {
+        $this->_clearTemplateCache();
+        parent::__construct();
+        if (is_callable('mb_internal_encoding')) {
+            mb_internal_encoding(Smarty::$_CHARSET);
+        }
+        $this->start_time = microtime(true);
+        if (isset($_SERVER[ 'SCRIPT_NAME' ])) {
+            Smarty::$global_tpl_vars[ 'SCRIPT_NAME' ] = new Smarty_Variable($_SERVER[ 'SCRIPT_NAME' ]);
+        }
+        // Check if we're running on windows
+        Smarty::$_IS_WINDOWS = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
+        // let PCRE (preg_*) treat strings as ISO-8859-1 if we're not dealing with UTF-8
+        if (Smarty::$_CHARSET !== 'UTF-8') {
+            Smarty::$_UTF8_MODIFIER = '';
+        }
+    }
+
+    /**
+     * Check if a template resource exists
+     *
+     * @param string $resource_name template name
+     *
+     * @return bool status
+     * @throws \SmartyException
+     */
+    public function templateExists($resource_name)
+    {
+        // create source object
+        $source = Smarty_Template_Source::load(null, $this, $resource_name);
+        return $source->exists;
+    }
+
+    /**
+     * Loads security class and enables security
+     *
+     * @param string|Smarty_Security $security_class if a string is used, it must be class-name
+     *
+     * @return Smarty                 current Smarty instance for chaining
+     * @throws \SmartyException
+     */
+    public function enableSecurity($security_class = null)
+    {
+        Smarty_Security::enableSecurity($this, $security_class);
+        return $this;
+    }
+
+    /**
+     * Disable security
+     *
+     * @return Smarty current Smarty instance for chaining
+     */
+    public function disableSecurity()
+    {
+        $this->security_policy = null;
+        return $this;
+    }
+
+    /**
+     * Add template directory(s)
+     *
+     * @param string|array $template_dir directory(s) of template sources
+     * @param string       $key          of the array element to assign the template dir to
+     * @param bool         $isConfig     true for config_dir
+     *
+     * @return Smarty          current Smarty instance for chaining
+     */
+    public function addTemplateDir($template_dir, $key = null, $isConfig = false)
+    {
+        if ($isConfig) {
+            $processed = &$this->_processedConfigDir;
+            $dir = &$this->config_dir;
+            $this->_configDirNormalized = false;
+        } else {
+            $processed = &$this->_processedTemplateDir;
+            $dir = &$this->template_dir;
+            $this->_templateDirNormalized = false;
+        }
+        if (is_array($template_dir)) {
+            foreach ($template_dir as $k => $v) {
+                if (is_int($k)) {
+                    // indexes are not merged but appended
+                    $dir[] = $v;
+                } else {
+                    // string indexes are overridden
+                    $dir[ $k ] = $v;
+                    unset($processed[ $key ]);
+                }
+            }
+        } else {
+            if ($key !== null) {
+                // override directory at specified index
+                $dir[ $key ] = $template_dir;
+                unset($processed[ $key ]);
+            } else {
+                // append new directory
+                $dir[] = $template_dir;
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Get template directories
+     *
+     * @param mixed $index    index of directory to get, null to get all
+     * @param bool  $isConfig true for config_dir
+     *
+     * @return array|string list of template directories, or directory of $index
+     */
+    public function getTemplateDir($index = null, $isConfig = false)
+    {
+        if ($isConfig) {
+            $dir = &$this->config_dir;
+        } else {
+            $dir = &$this->template_dir;
+        }
+        if ($isConfig ? !$this->_configDirNormalized : !$this->_templateDirNormalized) {
+            $this->_normalizeTemplateConfig($isConfig);
+        }
+        if ($index !== null) {
+            return isset($dir[ $index ]) ? $dir[ $index ] : null;
+        }
+        return $dir;
+    }
+
+    /**
+     * Set template directory
+     *
+     * @param string|array $template_dir directory(s) of template sources
+     * @param bool         $isConfig     true for config_dir
+     *
+     * @return \Smarty current Smarty instance for chaining
+     */
+    public function setTemplateDir($template_dir, $isConfig = false)
+    {
+        if ($isConfig) {
+            $this->config_dir = array();
+            $this->_processedConfigDir = array();
+        } else {
+            $this->template_dir = array();
+            $this->_processedTemplateDir = array();
+        }
+        $this->addTemplateDir($template_dir, null, $isConfig);
+        return $this;
+    }
+
+    /**
+     * Add config directory(s)
+     *
+     * @param string|array $config_dir directory(s) of config sources
+     * @param mixed        $key        key of the array element to assign the config dir to
+     *
+     * @return Smarty current Smarty instance for chaining
+     */
+    public function addConfigDir($config_dir, $key = null)
+    {
+        return $this->addTemplateDir($config_dir, $key, true);
+    }
+
+    /**
+     * Get config directory
+     *
+     * @param mixed $index index of directory to get, null to get all
+     *
+     * @return array configuration directory
+     */
+    public function getConfigDir($index = null)
+    {
+        return $this->getTemplateDir($index, true);
+    }
+
+    /**
+     * Set config directory
+     *
+     * @param $config_dir
+     *
+     * @return Smarty       current Smarty instance for chaining
+     */
+    public function setConfigDir($config_dir)
+    {
+        return $this->setTemplateDir($config_dir, true);
+    }
+
+    /**
+     * Adds directory of plugin files
+     *
+     * @param null|array|string $plugins_dir
+     *
+     * @return Smarty current Smarty instance for chaining
+     */
+    public function addPluginsDir($plugins_dir)
+    {
+        if (empty($this->plugins_dir)) {
+            $this->plugins_dir[] = SMARTY_PLUGINS_DIR;
+        }
+        $this->plugins_dir = array_merge($this->plugins_dir, (array)$plugins_dir);
+        $this->_pluginsDirNormalized = false;
+        return $this;
+    }
+
+    /**
+     * Get plugin directories
+     *
+     * @return array list of plugin directories
+     */
+    public function getPluginsDir()
+    {
+        if (empty($this->plugins_dir)) {
+            $this->plugins_dir[] = SMARTY_PLUGINS_DIR;
+            $this->_pluginsDirNormalized = false;
+        }
+        if (!$this->_pluginsDirNormalized) {
+            if (!is_array($this->plugins_dir)) {
+                $this->plugins_dir = (array)$this->plugins_dir;
+            }
+            foreach ($this->plugins_dir as $k => $v) {
+                $this->plugins_dir[ $k ] = $this->_realpath(rtrim($v, '/\\') . DIRECTORY_SEPARATOR, true);
+            }
+            $this->_cache[ 'plugin_files' ] = array();
+            $this->_pluginsDirNormalized = true;
+        }
+        return $this->plugins_dir;
+    }
+
+    /**
+     * Set plugins directory
+     *
+     * @param string|array $plugins_dir directory(s) of plugins
+     *
+     * @return Smarty       current Smarty instance for chaining
+     */
+    public function setPluginsDir($plugins_dir)
+    {
+        $this->plugins_dir = (array)$plugins_dir;
+        $this->_pluginsDirNormalized = false;
+        return $this;
+    }
+
+    /**
+     * Get compiled directory
+     *
+     * @return string path to compiled templates
+     */
+    public function getCompileDir()
+    {
+        if (!$this->_compileDirNormalized) {
+            $this->_normalizeDir('compile_dir', $this->compile_dir);
+            $this->_compileDirNormalized = true;
+        }
+        return $this->compile_dir;
+    }
+
+    /**
+     *
+     * @param  string $compile_dir directory to store compiled templates in
+     *
+     * @return Smarty current Smarty instance for chaining
+     */
+    public function setCompileDir($compile_dir)
+    {
+        $this->_normalizeDir('compile_dir', $compile_dir);
+        $this->_compileDirNormalized = true;
+        return $this;
+    }
+
+    /**
+     * Get cache directory
+     *
+     * @return string path of cache directory
+     */
+    public function getCacheDir()
+    {
+        if (!$this->_cacheDirNormalized) {
+            $this->_normalizeDir('cache_dir', $this->cache_dir);
+            $this->_cacheDirNormalized = true;
+        }
+        return $this->cache_dir;
+    }
+
+    /**
+     * Set cache directory
+     *
+     * @param string $cache_dir directory to store cached templates in
+     *
+     * @return Smarty current Smarty instance for chaining
+     */
+    public function setCacheDir($cache_dir)
+    {
+        $this->_normalizeDir('cache_dir', $cache_dir);
+        $this->_cacheDirNormalized = true;
+        return $this;
+    }
+
+    /**
+     * creates a template object
+     *
+     * @param string  $template   the resource handle of the template file
+     * @param mixed   $cache_id   cache id to be used with this template
+     * @param mixed   $compile_id compile id to be used with this template
+     * @param object  $parent     next higher level of Smarty variables
+     * @param boolean $do_clone   flag is Smarty object shall be cloned
+     *
+     * @return \Smarty_Internal_Template template object
+     * @throws \SmartyException
+     */
+    public function createTemplate($template, $cache_id = null, $compile_id = null, $parent = null, $do_clone = true)
+    {
+        if ($cache_id !== null && (is_object($cache_id) || is_array($cache_id))) {
+            $parent = $cache_id;
+            $cache_id = null;
+        }
+        if ($parent !== null && is_array($parent)) {
+            $data = $parent;
+            $parent = null;
+        } else {
+            $data = null;
+        }
+        if (!$this->_templateDirNormalized) {
+            $this->_normalizeTemplateConfig(false);
+        }
+        $_templateId = $this->_getTemplateId($template, $cache_id, $compile_id);
+        $tpl = null;
+        if ($this->caching && isset(Smarty_Internal_Template::$isCacheTplObj[ $_templateId ])) {
+            $tpl = $do_clone ? clone Smarty_Internal_Template::$isCacheTplObj[ $_templateId ] :
+                Smarty_Internal_Template::$isCacheTplObj[ $_templateId ];
+            $tpl->inheritance = null;
+            $tpl->tpl_vars = $tpl->config_vars = array();
+        } elseif (!$do_clone && isset(Smarty_Internal_Template::$tplObjCache[ $_templateId ])) {
+            $tpl = clone Smarty_Internal_Template::$tplObjCache[ $_templateId ];
+            $tpl->inheritance = null;
+            $tpl->tpl_vars = $tpl->config_vars = array();
+        } else {
+            /* @var Smarty_Internal_Template $tpl */
+            $tpl = new $this->template_class($template, $this, null, $cache_id, $compile_id, null, null);
+            $tpl->templateId = $_templateId;
+        }
+        if ($do_clone) {
+            $tpl->smarty = clone $tpl->smarty;
+        }
+        $tpl->parent = $parent ? $parent : $this;
+        // fill data if present
+        if (!empty($data) && is_array($data)) {
+            // set up variable values
+            foreach ($data as $_key => $_val) {
+                $tpl->tpl_vars[ $_key ] = new Smarty_Variable($_val);
+            }
+        }
+        if ($this->debugging || $this->debugging_ctrl === 'URL') {
+            $tpl->smarty->_debug = new Smarty_Internal_Debug();
+            // check URL debugging control
+            if (!$this->debugging && $this->debugging_ctrl === 'URL') {
+                $tpl->smarty->_debug->debugUrl($tpl->smarty);
+            }
+        }
+        return $tpl;
+    }
+
+    /**
+     * Takes unknown classes and loads plugin files for them
+     * class name format: Smarty_PluginType_PluginName
+     * plugin filename format: plugintype.pluginname.php
+     *
+     * @param string $plugin_name class plugin name to load
+     * @param bool   $check       check if already loaded
+     *
+     * @return string |boolean filepath of loaded file or false
+     * @throws \SmartyException
+     */
+    public function loadPlugin($plugin_name, $check = true)
+    {
+        return $this->ext->loadPlugin->loadPlugin($this, $plugin_name, $check);
+    }
+
+    /**
+     * Get unique template id
+     *
+     * @param string                    $template_name
+     * @param null|mixed                $cache_id
+     * @param null|mixed                $compile_id
+     * @param null                      $caching
+     * @param \Smarty_Internal_Template $template
+     *
+     * @return string
+     * @throws \SmartyException
+     */
+    public function _getTemplateId(
+        $template_name,
+        $cache_id = null,
+        $compile_id = null,
+        $caching = null,
+        Smarty_Internal_Template $template = null
+    ) {
+        $template_name = (strpos($template_name, ':') === false) ? "{$this->default_resource_type}:{$template_name}" :
+            $template_name;
+        $cache_id = $cache_id === null ? $this->cache_id : $cache_id;
+        $compile_id = $compile_id === null ? $this->compile_id : $compile_id;
+        $caching = (int)($caching === null ? $this->caching : $caching);
+        if ((isset($template) && strpos($template_name, ':.') !== false) || $this->allow_ambiguous_resources) {
+            $_templateId =
+                Smarty_Resource::getUniqueTemplateName((isset($template) ? $template : $this), $template_name) .
+                "#{$cache_id}#{$compile_id}#{$caching}";
+        } else {
+            $_templateId = $this->_joined_template_dir . "#{$template_name}#{$cache_id}#{$compile_id}#{$caching}";
+        }
+        if (isset($_templateId[ 150 ])) {
+            $_templateId = sha1($_templateId);
+        }
+        return $_templateId;
+    }
+
+    /**
+     * Normalize path
+     *  - remove /./ and /../
+     *  - make it absolute if required
+     *
+     * @param string $path     file path
+     * @param bool   $realpath if true - convert to absolute
+     *                         false - convert to relative
+     *                         null - keep as it is but
+     *                         remove /./ /../
+     *
+     * @return string
+     */
+    public function _realpath($path, $realpath = null)
+    {
+        $nds = array('/' => '\\', '\\' => '/');
+        preg_match(
+            '%^(?<root>(?:[[:alpha:]]:[\\\\/]|/|[\\\\]{2}[[:alpha:]]+|[[:print:]]{2,}:[/]{2}|[\\\\])?)(?<path>(.*))$%u',
+            $path,
+            $parts
+        );
+        $path = $parts[ 'path' ];
+        if ($parts[ 'root' ] === '\\') {
+            $parts[ 'root' ] = substr(getcwd(), 0, 2) . $parts[ 'root' ];
+        } else {
+            if ($realpath !== null && !$parts[ 'root' ]) {
+                $path = getcwd() . DIRECTORY_SEPARATOR . $path;
+            }
+        }
+        // normalize DIRECTORY_SEPARATOR
+        $path = str_replace($nds[ DIRECTORY_SEPARATOR ], DIRECTORY_SEPARATOR, $path);
+        $parts[ 'root' ] = str_replace($nds[ DIRECTORY_SEPARATOR ], DIRECTORY_SEPARATOR, $parts[ 'root' ]);
+        do {
+            $path = preg_replace(
+                array('#[\\\\/]{2}#', '#[\\\\/][.][\\\\/]#', '#[\\\\/]([^\\\\/.]+)[\\\\/][.][.][\\\\/]#'),
+                DIRECTORY_SEPARATOR,
+                $path,
+                -1,
+                $count
+            );
+        } while ($count > 0);
+        return $realpath !== false ? $parts[ 'root' ] . $path : str_ireplace(getcwd(), '.', $parts[ 'root' ] . $path);
+    }
+
+    /**
+     * Empty template objects cache
+     */
+    public function _clearTemplateCache()
+    {
+        Smarty_Internal_Template::$isCacheTplObj = array();
+        Smarty_Internal_Template::$tplObjCache = array();
+    }
+
+    /**
+     * @param boolean $use_sub_dirs
+     */
+    public function setUseSubDirs($use_sub_dirs)
+    {
+        $this->use_sub_dirs = $use_sub_dirs;
+    }
+
+    /**
+     * @param int $error_reporting
+     */
+    public function setErrorReporting($error_reporting)
+    {
+        $this->error_reporting = $error_reporting;
+    }
+
+    /**
+     * @param boolean $escape_html
+     */
+    public function setEscapeHtml($escape_html)
+    {
+        $this->escape_html = $escape_html;
+    }
+
+    /**
+     * Return auto_literal flag
+     *
+     * @return boolean
+     */
+    public function getAutoLiteral()
+    {
+        return $this->auto_literal;
+    }
+
+    /**
+     * Set auto_literal flag
+     *
+     * @param boolean $auto_literal
+     */
+    public function setAutoLiteral($auto_literal = true)
+    {
+        $this->auto_literal = $auto_literal;
+    }
+
+    /**
+     * @param boolean $force_compile
+     */
+    public function setForceCompile($force_compile)
+    {
+        $this->force_compile = $force_compile;
+    }
+
+    /**
+     * @param boolean $merge_compiled_includes
+     */
+    public function setMergeCompiledIncludes($merge_compiled_includes)
+    {
+        $this->merge_compiled_includes = $merge_compiled_includes;
+    }
+
+    /**
+     * Get left delimiter
+     *
+     * @return string
+     */
+    public function getLeftDelimiter()
+    {
+        return $this->left_delimiter;
+    }
+
+    /**
+     * Set left delimiter
+     *
+     * @param string $left_delimiter
+     */
+    public function setLeftDelimiter($left_delimiter)
+    {
+        $this->left_delimiter = $left_delimiter;
+    }
+
+    /**
+     * Get right delimiter
+     *
+     * @return string $right_delimiter
+     */
+    public function getRightDelimiter()
+    {
+        return $this->right_delimiter;
+    }
+
+    /**
+     * Set right delimiter
+     *
+     * @param string
+     */
+    public function setRightDelimiter($right_delimiter)
+    {
+        $this->right_delimiter = $right_delimiter;
+    }
+
+    /**
+     * @param boolean $debugging
+     */
+    public function setDebugging($debugging)
+    {
+        $this->debugging = $debugging;
+    }
+
+    /**
+     * @param boolean $config_overwrite
+     */
+    public function setConfigOverwrite($config_overwrite)
+    {
+        $this->config_overwrite = $config_overwrite;
+    }
+
+    /**
+     * @param boolean $config_booleanize
+     */
+    public function setConfigBooleanize($config_booleanize)
+    {
+        $this->config_booleanize = $config_booleanize;
+    }
+
+    /**
+     * @param boolean $config_read_hidden
+     */
+    public function setConfigReadHidden($config_read_hidden)
+    {
+        $this->config_read_hidden = $config_read_hidden;
+    }
+
+    /**
+     * @param boolean $compile_locking
+     */
+    public function setCompileLocking($compile_locking)
+    {
+        $this->compile_locking = $compile_locking;
+    }
+
+    /**
+     * @param string $default_resource_type
+     */
+    public function setDefaultResourceType($default_resource_type)
+    {
+        $this->default_resource_type = $default_resource_type;
+    }
+
+    /**
+     * @param string $caching_type
+     */
+    public function setCachingType($caching_type)
+    {
+        $this->caching_type = $caching_type;
+    }
+
+    /**
+     * Test install
+     *
+     * @param null $errors
+     */
+    public function testInstall(&$errors = null)
+    {
+        Smarty_Internal_TestInstall::testInstall($this, $errors);
+    }
+
+    /**
+     * Get Smarty object
+     *
+     * @return Smarty
+     */
+    public function _getSmartyObj()
+    {
+        return $this;
+    }
+
+    /**
+     * <<magic>> Generic getter.
+     * Calls the appropriate getter function.
+     * Issues an E_USER_NOTICE if no valid getter is found.
+     *
+     * @param string $name property name
+     *
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        if (isset($this->accessMap[ $name ])) {
+            $method = 'get' . $this->accessMap[ $name ];
+            return $this->{$method}();
+        } elseif (isset($this->_cache[ $name ])) {
+            return $this->_cache[ $name ];
+        } elseif (in_array($name, $this->obsoleteProperties)) {
+            return null;
+        } else {
+            trigger_error('Undefined property: ' . get_class($this) . '::$' . $name, E_USER_NOTICE);
+        }
+        return null;
+    }
+
+    /**
+     * <<magic>> Generic setter.
+     * Calls the appropriate setter function.
+     * Issues an E_USER_NOTICE if no valid setter is found.
+     *
+     * @param string $name  property name
+     * @param mixed  $value parameter passed to setter
+     *
+     */
+    public function __set($name, $value)
+    {
+        if (isset($this->accessMap[ $name ])) {
+            $method = 'set' . $this->accessMap[ $name ];
+            $this->{$method}($value);
+        } elseif (in_array($name, $this->obsoleteProperties)) {
+            return;
+        } elseif (is_object($value) && method_exists($value, $name)) {
+            $this->$name = $value;
+        } else {
+            trigger_error('Undefined property: ' . get_class($this) . '::$' . $name, E_USER_NOTICE);
+        }
+    }
+
+    /**
+     * Normalize and set directory string
+     *
+     * @param string $dirName cache_dir or compile_dir
+     * @param string $dir     filepath of folder
+     */
+    private function _normalizeDir($dirName, $dir)
+    {
+        $this->{$dirName} = $this->_realpath(rtrim($dir, "/\\") . DIRECTORY_SEPARATOR, true);
+    }
+
+    /**
+     * Normalize template_dir or config_dir
+     *
+     * @param bool $isConfig true for config_dir
+     */
+    private function _normalizeTemplateConfig($isConfig)
+    {
+        if ($isConfig) {
+            $processed = &$this->_processedConfigDir;
+            $dir = &$this->config_dir;
+        } else {
+            $processed = &$this->_processedTemplateDir;
+            $dir = &$this->template_dir;
+        }
+        if (!is_array($dir)) {
+            $dir = (array)$dir;
+        }
+        foreach ($dir as $k => $v) {
+            if (!isset($processed[ $k ])) {
+                $dir[ $k ] = $v = $this->_realpath(rtrim($v, "/\\") . DIRECTORY_SEPARATOR, true);
+                $processed[ $k ] = true;
+            }
+        }
+        $isConfig ? $this->_configDirNormalized = true : $this->_templateDirNormalized = true;
+        $isConfig ? $this->_joined_config_dir = join('#', $this->config_dir) :
+            $this->_joined_template_dir = join('#', $this->template_dir);
+    }
+
+    /**
+     * Activates PHP7 compatibility mode:
+     * - converts E_WARNINGS for "undefined array key" and "trying to read property of null" errors to E_NOTICE
+     *
+     * @void
+     */
+    public function muteUndefinedOrNullWarnings(): void {
+        $this->isMutingUndefinedOrNullWarnings = true;
+    }
+
+    /**
+     * Indicates if PHP7 compatibility mode is set.
+     * @bool
+     */
+    public function isMutingUndefinedOrNullWarnings(): bool {
+        return $this->isMutingUndefinedOrNullWarnings;
+    }
+
+}

+ 16 - 0
third_party/smarty/bootstrap.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * This file is part of the Smarty package.
+ *
+ * (c) Sebastian Bergmann <sebastian@phpunit.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+/**
+ * Load and register Smarty Autoloader
+ */
+if (!class_exists('Smarty_Autoloader')) {
+    include dirname(__FILE__) . '/Autoloader.php';
+}
+Smarty_Autoloader::register(true);

+ 160 - 0
third_party/smarty/debug.tpl

@@ -0,0 +1,160 @@
+{capture name='_smarty_debug' assign=debug_output}
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>Smarty Debug Console</title>
+        <style type="text/css">
+            {literal}
+            body, h1, h2, h3, td, th, p {
+                font-family: sans-serif;
+                font-weight: normal;
+                font-size: 0.9em;
+                margin: 1px;
+                padding: 0;
+            }
+
+            h1 {
+                margin: 0;
+                text-align: left;
+                padding: 2px;
+                background-color: #f0c040;
+                color: black;
+                font-weight: bold;
+                font-size: 1.2em;
+            }
+
+            h2 {
+                background-color: #9B410E;
+                color: white;
+                text-align: left;
+                font-weight: bold;
+                padding: 2px;
+                border-top: 1px solid black;
+            }
+            h3 {
+                text-align: left;
+                font-weight: bold;
+                color: black;
+                font-size: 0.7em;
+                padding: 2px;
+            }
+
+            body {
+                background: black;
+            }
+
+            p, table, div {
+                background: #f0ead8;
+            }
+
+            p {
+                margin: 0;
+                font-style: italic;
+                text-align: center;
+            }
+
+            table {
+                width: 100%;
+            }
+
+            th, td {
+                font-family: monospace;
+                vertical-align: top;
+                text-align: left;
+            }
+
+            td {
+                color: green;
+            }
+
+            .odd {
+                background-color: #eeeeee;
+            }
+
+            .even {
+                background-color: #fafafa;
+            }
+
+            .exectime {
+                font-size: 0.8em;
+                font-style: italic;
+            }
+
+            #bold div {
+                color: black;
+                font-weight: bold;
+            }
+            #blue h3 {
+                color: blue;
+            }
+            #normal div {
+                color: black;
+                font-weight: normal;
+            }
+            #table_assigned_vars th {
+                color: blue;
+                font-weight: bold;
+            }
+
+            #table_config_vars th {
+                color: maroon;
+            }
+
+            {/literal}
+        </style>
+    </head>
+    <body>
+
+    <h1>Smarty {Smarty::SMARTY_VERSION} Debug Console
+        -  {if isset($template_name)}{$template_name|debug_print_var nofilter} {/if}{if !empty($template_data)}Total Time {$execution_time|string_format:"%.5f"}{/if}</h1>
+
+    {if !empty($template_data)}
+        <h2>included templates &amp; config files (load time in seconds)</h2>
+        <div>
+            {foreach $template_data as $template}
+                <font color=brown>{$template.name}</font>
+                <br />&nbsp;&nbsp;<span class="exectime">
+                (compile {$template['compile_time']|string_format:"%.5f"}) (render {$template['render_time']|string_format:"%.5f"}) (cache {$template['cache_time']|string_format:"%.5f"})
+                 </span>
+                <br />
+            {/foreach}
+        </div>
+    {/if}
+
+    <h2>assigned template variables</h2>
+
+    <table id="table_assigned_vars">
+        {foreach $assigned_vars as $vars}
+            <tr class="{if $vars@iteration % 2 eq 0}odd{else}even{/if}">
+                <td><h3><font color=blue>${$vars@key}</font></h3>
+                    {if isset($vars['nocache'])}<b>Nocache</b><br />{/if}
+                    {if isset($vars['scope'])}<b>Origin:</b> {$vars['scope']|debug_print_var nofilter}{/if}
+                </td>
+                <td><h3>Value</h3>{$vars['value']|debug_print_var:10:80 nofilter}</td>
+                <td>{if isset($vars['attributes'])}<h3>Attributes</h3>{$vars['attributes']|debug_print_var nofilter} {/if}</td>
+         {/foreach}
+    </table>
+
+    <h2>assigned config file variables</h2>
+
+    <table id="table_config_vars">
+        {foreach $config_vars as $vars}
+            <tr class="{if $vars@iteration % 2 eq 0}odd{else}even{/if}">
+                <td><h3><font color=blue>#{$vars@key}#</font></h3>
+                    {if isset($vars['scope'])}<b>Origin:</b> {$vars['scope']|debug_print_var nofilter}{/if}
+                </td>
+                <td>{$vars['value']|debug_print_var:10:80 nofilter}</td>
+            </tr>
+        {/foreach}
+
+    </table>
+    </body>
+    </html>
+{/capture}
+<script type="text/javascript">
+    {$id = '__Smarty__'}
+    {if $display_mode}{$id = "$offset$template_name"|md5}{/if}
+    _smarty_console = window.open("", "console{$id}", "width=1024,height=600,left={$offset},top={$offset},resizable,scrollbars=yes");
+    _smarty_console.document.write("{$debug_output|escape:'javascript' nofilter}");
+    _smarty_console.document.close();
+</script>

+ 121 - 0
third_party/smarty/plugins/block.textformat.php

@@ -0,0 +1,121 @@
+<?php
+/**
+ * Smarty plugin to format text blocks
+ *
+ * @package    Smarty
+ * @subpackage PluginsBlock
+ */
+/**
+ * Smarty {textformat}{/textformat} block plugin
+ * Type:     block function
+ * Name:     textformat
+ * Purpose:  format text a certain way with preset styles
+ *           or custom wrap/indent settings
+ * Params:
+ *
+ * - style         - string (email)
+ * - indent        - integer (0)
+ * - wrap          - integer (80)
+ * - wrap_char     - string ("\n")
+ * - indent_char   - string (" ")
+ * - wrap_boundary - boolean (true)
+ *
+ * @link   https://www.smarty.net/manual/en/language.function.textformat.php {textformat}
+ *         (Smarty online manual)
+ *
+ * @param array                    $params   parameters
+ * @param string                   $content  contents of the block
+ * @param Smarty_Internal_Template $template template object
+ * @param boolean                  &$repeat  repeat flag
+ *
+ * @return string content re-formatted
+ * @author Monte Ohrt <monte at ohrt dot com>
+ * @throws \SmartyException
+ */
+function smarty_block_textformat($params, $content, Smarty_Internal_Template $template, &$repeat)
+{
+    if (is_null($content)) {
+        return;
+    }
+    if (Smarty::$_MBSTRING) {
+        $template->_checkPlugins(
+            array(
+                array(
+                    'function' => 'smarty_modifier_mb_wordwrap',
+                    'file'     => SMARTY_PLUGINS_DIR . 'modifier.mb_wordwrap.php'
+                )
+            )
+        );
+    }
+    $style = null;
+    $indent = 0;
+    $indent_first = 0;
+    $indent_char = ' ';
+    $wrap = 80;
+    $wrap_char = "\n";
+    $wrap_cut = false;
+    $assign = null;
+    foreach ($params as $_key => $_val) {
+        switch ($_key) {
+            case 'style':
+            case 'indent_char':
+            case 'wrap_char':
+            case 'assign':
+                $$_key = (string)$_val;
+                break;
+            case 'indent':
+            case 'indent_first':
+            case 'wrap':
+                $$_key = (int)$_val;
+                break;
+            case 'wrap_cut':
+                $$_key = (bool)$_val;
+                break;
+            default:
+                trigger_error("textformat: unknown attribute '{$_key}'");
+        }
+    }
+    if ($style === 'email') {
+        $wrap = 72;
+    }
+    // split into paragraphs
+    $_paragraphs = preg_split('![\r\n]{2}!', $content);
+    foreach ($_paragraphs as &$_paragraph) {
+        if (!$_paragraph) {
+            continue;
+        }
+        // convert mult. spaces & special chars to single space
+        $_paragraph =
+            preg_replace(
+                array(
+                    '!\s+!' . Smarty::$_UTF8_MODIFIER,
+                    '!(^\s+)|(\s+$)!' . Smarty::$_UTF8_MODIFIER
+                ),
+                array(
+                    ' ',
+                    ''
+                ),
+                $_paragraph
+            );
+        // indent first line
+        if ($indent_first > 0) {
+            $_paragraph = str_repeat($indent_char, $indent_first) . $_paragraph;
+        }
+        // wordwrap sentences
+        if (Smarty::$_MBSTRING) {
+            $_paragraph = smarty_modifier_mb_wordwrap($_paragraph, $wrap - $indent, $wrap_char, $wrap_cut);
+        } else {
+            $_paragraph = wordwrap($_paragraph, $wrap - $indent, $wrap_char, $wrap_cut);
+        }
+        // indent lines
+        if ($indent > 0) {
+            $_paragraph = preg_replace('!^!m', str_repeat($indent_char, $indent), $_paragraph);
+        }
+    }
+    $_output = implode($wrap_char . $wrap_char, $_paragraphs);
+    if ($assign) {
+        $template->assign($assign, $_output);
+    } else {
+        return $_output;
+    }
+}

+ 62 - 0
third_party/smarty/plugins/function.counter.php

@@ -0,0 +1,62 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {counter} function plugin
+ * Type:     function
+ * Name:     counter
+ * Purpose:  print out a counter value
+ *
+ * @author Monte Ohrt <monte at ohrt dot com>
+ * @link   https://www.smarty.net/manual/en/language.function.counter.php {counter}
+ *         (Smarty online manual)
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @return string|null
+ */
+function smarty_function_counter($params, $template)
+{
+    static $counters = array();
+    $name = (isset($params[ 'name' ])) ? $params[ 'name' ] : 'default';
+    if (!isset($counters[ $name ])) {
+        $counters[ $name ] = array('start' => 1, 'skip' => 1, 'direction' => 'up', 'count' => 1);
+    }
+    $counter =& $counters[ $name ];
+    if (isset($params[ 'start' ])) {
+        $counter[ 'start' ] = $counter[ 'count' ] = (int)$params[ 'start' ];
+    }
+    if (!empty($params[ 'assign' ])) {
+        $counter[ 'assign' ] = $params[ 'assign' ];
+    }
+    if (isset($counter[ 'assign' ])) {
+        $template->assign($counter[ 'assign' ], $counter[ 'count' ]);
+    }
+    if (isset($params[ 'print' ])) {
+        $print = (bool)$params[ 'print' ];
+    } else {
+        $print = empty($counter[ 'assign' ]);
+    }
+    if ($print) {
+        $retval = $counter[ 'count' ];
+    } else {
+        $retval = null;
+    }
+    if (isset($params[ 'skip' ])) {
+        $counter[ 'skip' ] = $params[ 'skip' ];
+    }
+    if (isset($params[ 'direction' ])) {
+        $counter[ 'direction' ] = $params[ 'direction' ];
+    }
+    if ($counter[ 'direction' ] === 'down') {
+        $counter[ 'count' ] -= $counter[ 'skip' ];
+    } else {
+        $counter[ 'count' ] += $counter[ 'skip' ];
+    }
+    return $retval;
+}

+ 92 - 0
third_party/smarty/plugins/function.cycle.php

@@ -0,0 +1,92 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {cycle} function plugin
+ * Type:     function
+ * Name:     cycle
+ * Date:     May 3, 2002
+ * Purpose:  cycle through given values
+ * Params:
+ *
+ * - name      - name of cycle (optional)
+ * - values    - comma separated list of values to cycle, or an array of values to cycle
+ *               (this can be left out for subsequent calls)
+ * - reset     - boolean - resets given var to true
+ * - print     - boolean - print var or not. default is true
+ * - advance   - boolean - whether or not to advance the cycle
+ * - delimiter - the value delimiter, default is ","
+ * - assign    - boolean, assigns to template var instead of printed.
+ *
+ * Examples:
+ *
+ * {cycle values="#eeeeee,#d0d0d0d"}
+ * {cycle name=row values="one,two,three" reset=true}
+ * {cycle name=row}
+ *
+ * @link    https://www.smarty.net/manual/en/language.function.cycle.php {cycle}
+ *           (Smarty online manual)
+ * @author  Monte Ohrt <monte at ohrt dot com>
+ * @author  credit to Mark Priatel <mpriatel@rogers.com>
+ * @author  credit to Gerard <gerard@interfold.com>
+ * @author  credit to Jason Sweat <jsweat_php@yahoo.com>
+ * @version 1.3
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @return string|null
+ */
+function smarty_function_cycle($params, $template)
+{
+    static $cycle_vars;
+    $name = (empty($params[ 'name' ])) ? 'default' : $params[ 'name' ];
+    $print = (isset($params[ 'print' ])) ? (bool)$params[ 'print' ] : true;
+    $advance = (isset($params[ 'advance' ])) ? (bool)$params[ 'advance' ] : true;
+    $reset = (isset($params[ 'reset' ])) ? (bool)$params[ 'reset' ] : false;
+    if (!isset($params[ 'values' ])) {
+        if (!isset($cycle_vars[ $name ][ 'values' ])) {
+            trigger_error('cycle: missing \'values\' parameter');
+            return;
+        }
+    } else {
+        if (isset($cycle_vars[ $name ][ 'values' ]) && $cycle_vars[ $name ][ 'values' ] !== $params[ 'values' ]) {
+            $cycle_vars[ $name ][ 'index' ] = 0;
+        }
+        $cycle_vars[ $name ][ 'values' ] = $params[ 'values' ];
+    }
+    if (isset($params[ 'delimiter' ])) {
+        $cycle_vars[ $name ][ 'delimiter' ] = $params[ 'delimiter' ];
+    } elseif (!isset($cycle_vars[ $name ][ 'delimiter' ])) {
+        $cycle_vars[ $name ][ 'delimiter' ] = ',';
+    }
+    if (is_array($cycle_vars[ $name ][ 'values' ])) {
+        $cycle_array = $cycle_vars[ $name ][ 'values' ];
+    } else {
+        $cycle_array = explode($cycle_vars[ $name ][ 'delimiter' ], $cycle_vars[ $name ][ 'values' ]);
+    }
+    if (!isset($cycle_vars[ $name ][ 'index' ]) || $reset) {
+        $cycle_vars[ $name ][ 'index' ] = 0;
+    }
+    if (isset($params[ 'assign' ])) {
+        $print = false;
+        $template->assign($params[ 'assign' ], $cycle_array[ $cycle_vars[ $name ][ 'index' ] ]);
+    }
+    if ($print) {
+        $retval = $cycle_array[ $cycle_vars[ $name ][ 'index' ] ];
+    } else {
+        $retval = null;
+    }
+    if ($advance) {
+        if ($cycle_vars[ $name ][ 'index' ] >= count($cycle_array) - 1) {
+            $cycle_vars[ $name ][ 'index' ] = 0;
+        } else {
+            $cycle_vars[ $name ][ 'index' ]++;
+        }
+    }
+    return $retval;
+}

+ 204 - 0
third_party/smarty/plugins/function.fetch.php

@@ -0,0 +1,204 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {fetch} plugin
+ * Type:     function
+ * Name:     fetch
+ * Purpose:  fetch file, web or ftp data and display results
+ *
+ * @link   https://www.smarty.net/manual/en/language.function.fetch.php {fetch}
+ *         (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @throws SmartyException
+ * @return string|null if the assign parameter is passed, Smarty assigns the result to a template variable
+ */
+function smarty_function_fetch($params, $template)
+{
+    if (empty($params[ 'file' ])) {
+        trigger_error('[plugin] fetch parameter \'file\' cannot be empty', E_USER_NOTICE);
+        return;
+    }
+    // strip file protocol
+    if (stripos($params[ 'file' ], 'file://') === 0) {
+        $params[ 'file' ] = substr($params[ 'file' ], 7);
+    }
+    $protocol = strpos($params[ 'file' ], '://');
+    if ($protocol !== false) {
+        $protocol = strtolower(substr($params[ 'file' ], 0, $protocol));
+    }
+    if (isset($template->smarty->security_policy)) {
+        if ($protocol) {
+            // remote resource (or php stream, …)
+            if (!$template->smarty->security_policy->isTrustedUri($params[ 'file' ])) {
+                return;
+            }
+        } else {
+            // local file
+            if (!$template->smarty->security_policy->isTrustedResourceDir($params[ 'file' ])) {
+                return;
+            }
+        }
+    }
+    $content = '';
+    if ($protocol === 'http') {
+        // http fetch
+        if ($uri_parts = parse_url($params[ 'file' ])) {
+            // set defaults
+            $host = $server_name = $uri_parts[ 'host' ];
+            $timeout = 30;
+            $accept = 'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*';
+            $agent = 'Smarty Template Engine ' . Smarty::SMARTY_VERSION;
+            $referer = '';
+            $uri = !empty($uri_parts[ 'path' ]) ? $uri_parts[ 'path' ] : '/';
+            $uri .= !empty($uri_parts[ 'query' ]) ? '?' . $uri_parts[ 'query' ] : '';
+            $_is_proxy = false;
+            if (empty($uri_parts[ 'port' ])) {
+                $port = 80;
+            } else {
+                $port = $uri_parts[ 'port' ];
+            }
+            if (!empty($uri_parts[ 'user' ])) {
+                $user = $uri_parts[ 'user' ];
+            }
+            if (!empty($uri_parts[ 'pass' ])) {
+                $pass = $uri_parts[ 'pass' ];
+            }
+            // loop through parameters, setup headers
+            foreach ($params as $param_key => $param_value) {
+                switch ($param_key) {
+                    case 'file':
+                    case 'assign':
+                    case 'assign_headers':
+                        break;
+                    case 'user':
+                        if (!empty($param_value)) {
+                            $user = $param_value;
+                        }
+                        break;
+                    case 'pass':
+                        if (!empty($param_value)) {
+                            $pass = $param_value;
+                        }
+                        break;
+                    case 'accept':
+                        if (!empty($param_value)) {
+                            $accept = $param_value;
+                        }
+                        break;
+                    case 'header':
+                        if (!empty($param_value)) {
+                            if (!preg_match('![\w\d-]+: .+!', $param_value)) {
+                                trigger_error("[plugin] invalid header format '{$param_value}'", E_USER_NOTICE);
+                                return;
+                            } else {
+                                $extra_headers[] = $param_value;
+                            }
+                        }
+                        break;
+                    case 'proxy_host':
+                        if (!empty($param_value)) {
+                            $proxy_host = $param_value;
+                        }
+                        break;
+                    case 'proxy_port':
+                        if (!preg_match('!\D!', $param_value)) {
+                            $proxy_port = (int)$param_value;
+                        } else {
+                            trigger_error("[plugin] invalid value for attribute '{$param_key }'", E_USER_NOTICE);
+                            return;
+                        }
+                        break;
+                    case 'agent':
+                        if (!empty($param_value)) {
+                            $agent = $param_value;
+                        }
+                        break;
+                    case 'referer':
+                        if (!empty($param_value)) {
+                            $referer = $param_value;
+                        }
+                        break;
+                    case 'timeout':
+                        if (!preg_match('!\D!', $param_value)) {
+                            $timeout = (int)$param_value;
+                        } else {
+                            trigger_error("[plugin] invalid value for attribute '{$param_key}'", E_USER_NOTICE);
+                            return;
+                        }
+                        break;
+                    default:
+                        trigger_error("[plugin] unrecognized attribute '{$param_key}'", E_USER_NOTICE);
+                        return;
+                }
+            }
+            if (!empty($proxy_host) && !empty($proxy_port)) {
+                $_is_proxy = true;
+                $fp = fsockopen($proxy_host, $proxy_port, $errno, $errstr, $timeout);
+            } else {
+                $fp = fsockopen($server_name, $port, $errno, $errstr, $timeout);
+            }
+            if (!$fp) {
+                trigger_error("[plugin] unable to fetch: $errstr ($errno)", E_USER_NOTICE);
+                return;
+            } else {
+                if ($_is_proxy) {
+                    fputs($fp, 'GET ' . $params[ 'file' ] . " HTTP/1.0\r\n");
+                } else {
+                    fputs($fp, "GET $uri HTTP/1.0\r\n");
+                }
+                if (!empty($host)) {
+                    fputs($fp, "Host: $host\r\n");
+                }
+                if (!empty($accept)) {
+                    fputs($fp, "Accept: $accept\r\n");
+                }
+                if (!empty($agent)) {
+                    fputs($fp, "User-Agent: $agent\r\n");
+                }
+                if (!empty($referer)) {
+                    fputs($fp, "Referer: $referer\r\n");
+                }
+                if (isset($extra_headers) && is_array($extra_headers)) {
+                    foreach ($extra_headers as $curr_header) {
+                        fputs($fp, $curr_header . "\r\n");
+                    }
+                }
+                if (!empty($user) && !empty($pass)) {
+                    fputs($fp, 'Authorization: BASIC ' . base64_encode("$user:$pass") . "\r\n");
+                }
+                fputs($fp, "\r\n");
+                while (!feof($fp)) {
+                    $content .= fgets($fp, 4096);
+                }
+                fclose($fp);
+                $csplit = preg_split("!\r\n\r\n!", $content, 2);
+                $content = $csplit[ 1 ];
+                if (!empty($params[ 'assign_headers' ])) {
+                    $template->assign($params[ 'assign_headers' ], preg_split("!\r\n!", $csplit[ 0 ]));
+                }
+            }
+        } else {
+            trigger_error("[plugin fetch] unable to parse URL, check syntax", E_USER_NOTICE);
+            return;
+        }
+    } else {
+        $content = @file_get_contents($params[ 'file' ]);
+        if ($content === false) {
+            throw new SmartyException("{fetch} cannot read resource '" . $params[ 'file' ] . "'");
+        }
+    }
+    if (!empty($params[ 'assign' ])) {
+        $template->assign($params[ 'assign' ], $content);
+    } else {
+        return $content;
+    }
+}

+ 286 - 0
third_party/smarty/plugins/function.html_checkboxes.php

@@ -0,0 +1,286 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_checkboxes} function plugin
+ * File:       function.html_checkboxes.php
+ * Type:       function
+ * Name:       html_checkboxes
+ * Date:       24.Feb.2003
+ * Purpose:    Prints out a list of checkbox input types
+ * Examples:
+ *
+ * {html_checkboxes values=$ids output=$names}
+ * {html_checkboxes values=$ids name='box' separator='<br>' output=$names}
+ * {html_checkboxes values=$ids checked=$checked separator='<br>' output=$names}
+ *
+ * Params:
+ *
+ * - name       (optional) - string default "checkbox"
+ * - values     (required) - array
+ * - options    (optional) - associative array
+ * - checked    (optional) - array default not set
+ * - separator  (optional) - ie <br> or &nbsp;
+ * - output     (optional) - the output next to each checkbox
+ * - assign     (optional) - assign the output as an array to this variable
+ * - escape     (optional) - escape the content (not value), defaults to true
+ *
+ * @link    https://www.smarty.net/manual/en/language.function.html.checkboxes.php {html_checkboxes}
+ *             (Smarty online manual)
+ * @author  Christopher Kvarme <christopher.kvarme@flashjab.com>
+ * @author  credits to Monte Ohrt <monte at ohrt dot com>
+ * @version 1.0
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @return string
+ * @uses    smarty_function_escape_special_chars()
+ * @throws \SmartyException
+ */
+function smarty_function_html_checkboxes($params, Smarty_Internal_Template $template)
+{
+    $template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_function_escape_special_chars',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.escape_special_chars.php'
+            )
+        )
+    );
+    $name = 'checkbox';
+    $values = null;
+    $options = null;
+    $selected = array();
+    $separator = '';
+    $escape = true;
+    $labels = true;
+    $label_ids = false;
+    $output = null;
+    $extra = '';
+    foreach ($params as $_key => $_val) {
+        switch ($_key) {
+            case 'name':
+            case 'separator':
+                $$_key = (string)$_val;
+                break;
+            case 'escape':
+            case 'labels':
+            case 'label_ids':
+                $$_key = (bool)$_val;
+                break;
+            case 'options':
+                $$_key = (array)$_val;
+                break;
+            case 'values':
+            case 'output':
+                $$_key = array_values((array)$_val);
+                break;
+            case 'checked':
+            case 'selected':
+                if (is_array($_val)) {
+                    $selected = array();
+                    foreach ($_val as $_sel) {
+                        if (is_object($_sel)) {
+                            if (method_exists($_sel, '__toString')) {
+                                $_sel = smarty_function_escape_special_chars((string)$_sel->__toString());
+                            } else {
+                                trigger_error(
+                                    'html_checkboxes: selected attribute contains an object of class \'' .
+                                    get_class($_sel) . '\' without __toString() method',
+                                    E_USER_NOTICE
+                                );
+                                continue;
+                            }
+                        } else {
+                            $_sel = smarty_function_escape_special_chars((string)$_sel);
+                        }
+                        $selected[ $_sel ] = true;
+                    }
+                } elseif (is_object($_val)) {
+                    if (method_exists($_val, '__toString')) {
+                        $selected = smarty_function_escape_special_chars((string)$_val->__toString());
+                    } else {
+                        trigger_error(
+                            'html_checkboxes: selected attribute is an object of class \'' . get_class($_val) .
+                            '\' without __toString() method',
+                            E_USER_NOTICE
+                        );
+                    }
+                } else {
+                    $selected = smarty_function_escape_special_chars((string)$_val);
+                }
+                break;
+            case 'checkboxes':
+                trigger_error(
+                    'html_checkboxes: the use of the "checkboxes" attribute is deprecated, use "options" instead',
+                    E_USER_WARNING
+                );
+                $options = (array)$_val;
+                break;
+            case 'assign':
+                break;
+            case 'strict':
+                break;
+            case 'disabled':
+            case 'readonly':
+                if (!empty($params[ 'strict' ])) {
+                    if (!is_scalar($_val)) {
+                        trigger_error(
+                            "html_options: {$_key} attribute must be a scalar, only boolean true or string '{$_key}' will actually add the attribute",
+                            E_USER_NOTICE
+                        );
+                    }
+                    if ($_val === true || $_val === $_key) {
+                        $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_key) . '"';
+                    }
+                    break;
+                }
+            // omit break; to fall through!
+            // no break
+            default:
+                if (!is_array($_val)) {
+                    $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_val) . '"';
+                } else {
+                    trigger_error("html_checkboxes: extra attribute '{$_key}' cannot be an array", E_USER_NOTICE);
+                }
+                break;
+        }
+    }
+    if (!isset($options) && !isset($values)) {
+        return '';
+    } /* raise error here? */
+    $_html_result = array();
+    if (isset($options)) {
+        foreach ($options as $_key => $_val) {
+            $_html_result[] =
+                smarty_function_html_checkboxes_output(
+                    $name,
+                    $_key,
+                    $_val,
+                    $selected,
+                    $extra,
+                    $separator,
+                    $labels,
+                    $label_ids,
+                    $escape
+                );
+        }
+    } else {
+        foreach ($values as $_i => $_key) {
+            $_val = isset($output[ $_i ]) ? $output[ $_i ] : '';
+            $_html_result[] =
+                smarty_function_html_checkboxes_output(
+                    $name,
+                    $_key,
+                    $_val,
+                    $selected,
+                    $extra,
+                    $separator,
+                    $labels,
+                    $label_ids,
+                    $escape
+                );
+        }
+    }
+    if (!empty($params[ 'assign' ])) {
+        $template->assign($params[ 'assign' ], $_html_result);
+    } else {
+        return implode("\n", $_html_result);
+    }
+}
+
+/**
+ * @param      $name
+ * @param      $value
+ * @param      $output
+ * @param      $selected
+ * @param      $extra
+ * @param      $separator
+ * @param      $labels
+ * @param      $label_ids
+ * @param bool $escape
+ *
+ * @return string
+ */
+function smarty_function_html_checkboxes_output(
+    $name,
+    $value,
+    $output,
+    $selected,
+    $extra,
+    $separator,
+    $labels,
+    $label_ids,
+    $escape = true
+) {
+    $_output = '';
+    if (is_object($value)) {
+        if (method_exists($value, '__toString')) {
+            $value = (string)$value->__toString();
+        } else {
+            trigger_error(
+                'html_options: value is an object of class \'' . get_class($value) .
+                '\' without __toString() method',
+                E_USER_NOTICE
+            );
+            return '';
+        }
+    } else {
+        $value = (string)$value;
+    }
+    if (is_object($output)) {
+        if (method_exists($output, '__toString')) {
+            $output = (string)$output->__toString();
+        } else {
+            trigger_error(
+                'html_options: output is an object of class \'' . get_class($output) .
+                '\' without __toString() method',
+                E_USER_NOTICE
+            );
+            return '';
+        }
+    } else {
+        $output = (string)$output;
+    }
+    if ($labels) {
+        if ($label_ids) {
+            $_id = smarty_function_escape_special_chars(
+                preg_replace(
+                    '![^\w\-\.]!' . Smarty::$_UTF8_MODIFIER,
+                    '_',
+                    $name . '_' . $value
+                )
+            );
+            $_output .= '<label for="' . $_id . '">';
+        } else {
+            $_output .= '<label>';
+        }
+    }
+    $name = smarty_function_escape_special_chars($name);
+    $value = smarty_function_escape_special_chars($value);
+    if ($escape) {
+        $output = smarty_function_escape_special_chars($output);
+    }
+    $_output .= '<input type="checkbox" name="' . $name . '[]" value="' . $value . '"';
+    if ($labels && $label_ids) {
+        $_output .= ' id="' . $_id . '"';
+    }
+    if (is_array($selected)) {
+        if (isset($selected[ $value ])) {
+            $_output .= ' checked="checked"';
+        }
+    } elseif ($value === $selected) {
+        $_output .= ' checked="checked"';
+    }
+    $_output .= $extra . ' />' . $output;
+    if ($labels) {
+        $_output .= '</label>';
+    }
+    $_output .= $separator;
+    return $_output;
+}

+ 158 - 0
third_party/smarty/plugins/function.html_image.php

@@ -0,0 +1,158 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_image} function plugin
+ * Type:     function
+ * Name:     html_image
+ * Date:     Feb 24, 2003
+ * Purpose:  format HTML tags for the image
+ * Examples: {html_image file="/images/masthead.gif"}
+ * Output:   <img src="/images/masthead.gif" width=400 height=23>
+ * Params:
+ *
+ * - file        - (required) - file (and path) of image
+ * - height      - (optional) - image height (default actual height)
+ * - width       - (optional) - image width (default actual width)
+ * - basedir     - (optional) - base directory for absolute paths, default is environment variable DOCUMENT_ROOT
+ * - path_prefix - prefix for path output (optional, default empty)
+ *
+ * @link    https://www.smarty.net/manual/en/language.function.html.image.php {html_image}
+ *          (Smarty online manual)
+ * @author  Monte Ohrt <monte at ohrt dot com>
+ * @author  credits to Duda <duda@big.hu>
+ * @version 1.0
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @throws SmartyException
+ * @return string
+ * @uses    smarty_function_escape_special_chars()
+ */
+function smarty_function_html_image($params, Smarty_Internal_Template $template)
+{
+    $template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_function_escape_special_chars',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.escape_special_chars.php'
+            )
+        )
+    );
+    $alt = '';
+    $file = '';
+    $height = '';
+    $width = '';
+    $extra = '';
+    $prefix = '';
+    $suffix = '';
+    $path_prefix = '';
+    $basedir = isset($_SERVER[ 'DOCUMENT_ROOT' ]) ? $_SERVER[ 'DOCUMENT_ROOT' ] : '';
+    foreach ($params as $_key => $_val) {
+        switch ($_key) {
+            case 'file':
+            case 'height':
+            case 'width':
+            case 'dpi':
+            case 'path_prefix':
+            case 'basedir':
+                $$_key = $_val;
+                break;
+            case 'alt':
+                if (!is_array($_val)) {
+                    $$_key = smarty_function_escape_special_chars($_val);
+                } else {
+                    throw new SmartyException(
+                        "html_image: extra attribute '{$_key}' cannot be an array",
+                        E_USER_NOTICE
+                    );
+                }
+                break;
+            case 'link':
+            case 'href':
+                $prefix = '<a href="' . $_val . '">';
+                $suffix = '</a>';
+                break;
+            default:
+                if (!is_array($_val)) {
+                    $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_val) . '"';
+                } else {
+                    throw new SmartyException(
+                        "html_image: extra attribute '{$_key}' cannot be an array",
+                        E_USER_NOTICE
+                    );
+                }
+                break;
+        }
+    }
+    if (empty($file)) {
+        trigger_error('html_image: missing \'file\' parameter', E_USER_NOTICE);
+        return;
+    }
+    if ($file[ 0 ] === '/') {
+        $_image_path = $basedir . $file;
+    } else {
+        $_image_path = $file;
+    }
+    // strip file protocol
+    if (stripos($params[ 'file' ], 'file://') === 0) {
+        $params[ 'file' ] = substr($params[ 'file' ], 7);
+    }
+    $protocol = strpos($params[ 'file' ], '://');
+    if ($protocol !== false) {
+        $protocol = strtolower(substr($params[ 'file' ], 0, $protocol));
+    }
+    if (isset($template->smarty->security_policy)) {
+        if ($protocol) {
+            // remote resource (or php stream, …)
+            if (!$template->smarty->security_policy->isTrustedUri($params[ 'file' ])) {
+                return;
+            }
+        } else {
+            // local file
+            if (!$template->smarty->security_policy->isTrustedResourceDir($_image_path)) {
+                return;
+            }
+        }
+    }
+    if (!isset($params[ 'width' ]) || !isset($params[ 'height' ])) {
+        // FIXME: (rodneyrehm) getimagesize() loads the complete file off a remote resource, use custom [jpg,png,gif]header reader!
+        if (!$_image_data = @getimagesize($_image_path)) {
+            if (!file_exists($_image_path)) {
+                trigger_error("html_image: unable to find '{$_image_path}'", E_USER_NOTICE);
+                return;
+            } elseif (!is_readable($_image_path)) {
+                trigger_error("html_image: unable to read '{$_image_path}'", E_USER_NOTICE);
+                return;
+            } else {
+                trigger_error("html_image: '{$_image_path}' is not a valid image file", E_USER_NOTICE);
+                return;
+            }
+        }
+        if (!isset($params[ 'width' ])) {
+            $width = $_image_data[ 0 ];
+        }
+        if (!isset($params[ 'height' ])) {
+            $height = $_image_data[ 1 ];
+        }
+    }
+    if (isset($params[ 'dpi' ])) {
+        if (strstr($_SERVER[ 'HTTP_USER_AGENT' ], 'Mac')) {
+            // FIXME: (rodneyrehm) wrong dpi assumption
+            // don't know who thought this up… even if it was true in 1998, it's definitely wrong in 2011.
+            $dpi_default = 72;
+        } else {
+            $dpi_default = 96;
+        }
+        $_resize = $dpi_default / $params[ 'dpi' ];
+        $width = round($width * $_resize);
+        $height = round($height * $_resize);
+    }
+    return $prefix . '<img src="' . $path_prefix . $file . '" alt="' . $alt . '" width="' . $width . '" height="' .
+           $height . '"' . $extra . ' />' . $suffix;
+}

+ 230 - 0
third_party/smarty/plugins/function.html_options.php

@@ -0,0 +1,230 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_options} function plugin
+ * Type:     function
+ * Name:     html_options
+ * Purpose:  Prints the list of <option> tags generated from
+ *           the passed parameters
+ * Params:
+ *
+ * - name       (optional) - string default "select"
+ * - values     (required) - if no options supplied) - array
+ * - options    (required) - if no values supplied) - associative array
+ * - selected   (optional) - string default not set
+ * - output     (required) - if not options supplied) - array
+ * - id         (optional) - string default not set
+ * - class      (optional) - string default not set
+ *
+ * @link   https://www.smarty.net/manual/en/language.function.html.options.php {html_image}
+ *           (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ * @author Ralf Strehle (minor optimization) <ralf dot strehle at yahoo dot de>
+ *
+ * @param array                     $params parameters
+ *
+ * @param \Smarty_Internal_Template $template
+ *
+ * @return string
+ * @uses   smarty_function_escape_special_chars()
+ * @throws \SmartyException
+ */
+function smarty_function_html_options($params, Smarty_Internal_Template $template)
+{
+    $template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_function_escape_special_chars',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.escape_special_chars.php'
+            )
+        )
+    );
+    $name = null;
+    $values = null;
+    $options = null;
+    $selected = null;
+    $output = null;
+    $id = null;
+    $class = null;
+    $extra = '';
+    foreach ($params as $_key => $_val) {
+        switch ($_key) {
+            case 'name':
+            case 'class':
+            case 'id':
+                $$_key = (string)$_val;
+                break;
+            case 'options':
+                $options = (array)$_val;
+                break;
+            case 'values':
+            case 'output':
+                $$_key = array_values((array)$_val);
+                break;
+            case 'selected':
+                if (is_array($_val)) {
+                    $selected = array();
+                    foreach ($_val as $_sel) {
+                        if (is_object($_sel)) {
+                            if (method_exists($_sel, '__toString')) {
+                                $_sel = smarty_function_escape_special_chars((string)$_sel->__toString());
+                            } else {
+                                trigger_error(
+                                    'html_options: selected attribute contains an object of class \'' .
+                                    get_class($_sel) . '\' without __toString() method',
+                                    E_USER_NOTICE
+                                );
+                                continue;
+                            }
+                        } else {
+                            $_sel = smarty_function_escape_special_chars((string)$_sel);
+                        }
+                        $selected[ $_sel ] = true;
+                    }
+                } elseif (is_object($_val)) {
+                    if (method_exists($_val, '__toString')) {
+                        $selected = smarty_function_escape_special_chars((string)$_val->__toString());
+                    } else {
+                        trigger_error(
+                            'html_options: selected attribute is an object of class \'' . get_class($_val) .
+                            '\' without __toString() method',
+                            E_USER_NOTICE
+                        );
+                    }
+                } else {
+                    $selected = smarty_function_escape_special_chars((string)$_val);
+                }
+                break;
+            case 'strict':
+                break;
+            case 'disabled':
+            case 'readonly':
+                if (!empty($params[ 'strict' ])) {
+                    if (!is_scalar($_val)) {
+                        trigger_error(
+                            "html_options: {$_key} attribute must be a scalar, only boolean true or string '{$_key}' will actually add the attribute",
+                            E_USER_NOTICE
+                        );
+                    }
+                    if ($_val === true || $_val === $_key) {
+                        $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_key) . '"';
+                    }
+                    break;
+                }
+            // omit break; to fall through!
+            // no break
+            default:
+                if (!is_array($_val)) {
+                    $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_val) . '"';
+                } else {
+                    trigger_error("html_options: extra attribute '{$_key}' cannot be an array", E_USER_NOTICE);
+                }
+                break;
+        }
+    }
+    if (!isset($options) && !isset($values)) {
+        /* raise error here? */
+        return '';
+    }
+    $_html_result = '';
+    $_idx = 0;
+    if (isset($options)) {
+        foreach ($options as $_key => $_val) {
+            $_html_result .= smarty_function_html_options_optoutput($_key, $_val, $selected, $id, $class, $_idx);
+        }
+    } else {
+        foreach ($values as $_i => $_key) {
+            $_val = isset($output[ $_i ]) ? $output[ $_i ] : '';
+            $_html_result .= smarty_function_html_options_optoutput($_key, $_val, $selected, $id, $class, $_idx);
+        }
+    }
+    if (!empty($name)) {
+        $_html_class = !empty($class) ? ' class="' . $class . '"' : '';
+        $_html_id = !empty($id) ? ' id="' . $id . '"' : '';
+        $_html_result =
+            '<select name="' . $name . '"' . $_html_class . $_html_id . $extra . '>' . "\n" . $_html_result .
+            '</select>' . "\n";
+    }
+    return $_html_result;
+}
+
+/**
+ * @param $key
+ * @param $value
+ * @param $selected
+ * @param $id
+ * @param $class
+ * @param $idx
+ *
+ * @return string
+ */
+function smarty_function_html_options_optoutput($key, $value, $selected, $id, $class, &$idx)
+{
+    if (!is_array($value)) {
+        $_key = smarty_function_escape_special_chars($key);
+        $_html_result = '<option value="' . $_key . '"';
+        if (is_array($selected)) {
+            if (isset($selected[ $_key ])) {
+                $_html_result .= ' selected="selected"';
+            }
+        } elseif ($_key === $selected) {
+            $_html_result .= ' selected="selected"';
+        }
+        $_html_class = !empty($class) ? ' class="' . $class . ' option"' : '';
+        $_html_id = !empty($id) ? ' id="' . $id . '-' . $idx . '"' : '';
+        if (is_object($value)) {
+            if (method_exists($value, '__toString')) {
+                $value = smarty_function_escape_special_chars((string)$value->__toString());
+            } else {
+                trigger_error(
+                    'html_options: value is an object of class \'' . get_class($value) .
+                    '\' without __toString() method',
+                    E_USER_NOTICE
+                );
+                return '';
+            }
+        } else {
+            $value = smarty_function_escape_special_chars((string)$value);
+        }
+        $_html_result .= $_html_class . $_html_id . '>' . $value . '</option>' . "\n";
+        $idx++;
+    } else {
+        $_idx = 0;
+        $_html_result =
+            smarty_function_html_options_optgroup(
+                $key,
+                $value,
+                $selected,
+                !empty($id) ? ($id . '-' . $idx) : null,
+                $class,
+                $_idx
+            );
+        $idx++;
+    }
+    return $_html_result;
+}
+
+/**
+ * @param $key
+ * @param $values
+ * @param $selected
+ * @param $id
+ * @param $class
+ * @param $idx
+ *
+ * @return string
+ */
+function smarty_function_html_options_optgroup($key, $values, $selected, $id, $class, &$idx)
+{
+    $optgroup_html = '<optgroup label="' . smarty_function_escape_special_chars($key) . '">' . "\n";
+    foreach ($values as $key => $value) {
+        $optgroup_html .= smarty_function_html_options_optoutput($key, $value, $selected, $id, $class, $idx);
+    }
+    $optgroup_html .= "</optgroup>\n";
+    return $optgroup_html;
+}

+ 266 - 0
third_party/smarty/plugins/function.html_radios.php

@@ -0,0 +1,266 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_radios} function plugin
+ * File:       function.html_radios.php
+ * Type:       function
+ * Name:       html_radios
+ * Date:       24.Feb.2003
+ * Purpose:    Prints out a list of radio input types
+ * Params:
+ *
+ * - name       (optional) - string default "radio"
+ * - values     (required) - array
+ * - options    (required) - associative array
+ * - checked    (optional) - array default not set
+ * - separator  (optional) - ie <br> or &nbsp;
+ * - output     (optional) - the output next to each radio button
+ * - assign     (optional) - assign the output as an array to this variable
+ * - escape     (optional) - escape the content (not value), defaults to true
+ *
+ * Examples:
+ *
+ * {html_radios values=$ids output=$names}
+ * {html_radios values=$ids name='box' separator='<br>' output=$names}
+ * {html_radios values=$ids checked=$checked separator='<br>' output=$names}
+ *
+ * @link    https://www.smarty.net/manual/en/language.function.html.radios.php {html_radios}
+ *          (Smarty online manual)
+ * @author  Christopher Kvarme <christopher.kvarme@flashjab.com>
+ * @author  credits to Monte Ohrt <monte at ohrt dot com>
+ * @version 1.0
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @return string
+ * @uses    smarty_function_escape_special_chars()
+ * @throws \SmartyException
+ */
+function smarty_function_html_radios($params, Smarty_Internal_Template $template)
+{
+    $template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_function_escape_special_chars',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.escape_special_chars.php'
+            )
+        )
+    );
+    $name = 'radio';
+    $values = null;
+    $options = null;
+    $selected = null;
+    $separator = '';
+    $escape = true;
+    $labels = true;
+    $label_ids = false;
+    $output = null;
+    $extra = '';
+    foreach ($params as $_key => $_val) {
+        switch ($_key) {
+            case 'name':
+            case 'separator':
+                $$_key = (string)$_val;
+                break;
+            case 'checked':
+            case 'selected':
+                if (is_array($_val)) {
+                    trigger_error('html_radios: the "' . $_key . '" attribute cannot be an array', E_USER_WARNING);
+                } elseif (is_object($_val)) {
+                    if (method_exists($_val, '__toString')) {
+                        $selected = smarty_function_escape_special_chars((string)$_val->__toString());
+                    } else {
+                        trigger_error(
+                            'html_radios: selected attribute is an object of class \'' . get_class($_val) .
+                            '\' without __toString() method',
+                            E_USER_NOTICE
+                        );
+                    }
+                } else {
+                    $selected = (string)$_val;
+                }
+                break;
+            case 'escape':
+            case 'labels':
+            case 'label_ids':
+                $$_key = (bool)$_val;
+                break;
+            case 'options':
+                $$_key = (array)$_val;
+                break;
+            case 'values':
+            case 'output':
+                $$_key = array_values((array)$_val);
+                break;
+            case 'radios':
+                trigger_error(
+                    'html_radios: the use of the "radios" attribute is deprecated, use "options" instead',
+                    E_USER_WARNING
+                );
+                $options = (array)$_val;
+                break;
+            case 'assign':
+                break;
+            case 'strict':
+                break;
+            case 'disabled':
+            case 'readonly':
+                if (!empty($params[ 'strict' ])) {
+                    if (!is_scalar($_val)) {
+                        trigger_error(
+                            "html_options: {$_key} attribute must be a scalar, only boolean true or string '$_key' will actually add the attribute",
+                            E_USER_NOTICE
+                        );
+                    }
+                    if ($_val === true || $_val === $_key) {
+                        $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_key) . '"';
+                    }
+                    break;
+                }
+            // omit break; to fall through!
+            // no break
+            default:
+                if (!is_array($_val)) {
+                    $extra .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_val) . '"';
+                } else {
+                    trigger_error("html_radios: extra attribute '{$_key}' cannot be an array", E_USER_NOTICE);
+                }
+                break;
+        }
+    }
+    if (!isset($options) && !isset($values)) {
+        /* raise error here? */
+        return '';
+    }
+    $_html_result = array();
+    if (isset($options)) {
+        foreach ($options as $_key => $_val) {
+            $_html_result[] =
+                smarty_function_html_radios_output(
+                    $name,
+                    $_key,
+                    $_val,
+                    $selected,
+                    $extra,
+                    $separator,
+                    $labels,
+                    $label_ids,
+                    $escape
+                );
+        }
+    } else {
+        foreach ($values as $_i => $_key) {
+            $_val = isset($output[ $_i ]) ? $output[ $_i ] : '';
+            $_html_result[] =
+                smarty_function_html_radios_output(
+                    $name,
+                    $_key,
+                    $_val,
+                    $selected,
+                    $extra,
+                    $separator,
+                    $labels,
+                    $label_ids,
+                    $escape
+                );
+        }
+    }
+    if (!empty($params[ 'assign' ])) {
+        $template->assign($params[ 'assign' ], $_html_result);
+    } else {
+        return implode("\n", $_html_result);
+    }
+}
+
+/**
+ * @param $name
+ * @param $value
+ * @param $output
+ * @param $selected
+ * @param $extra
+ * @param $separator
+ * @param $labels
+ * @param $label_ids
+ * @param $escape
+ *
+ * @return string
+ */
+function smarty_function_html_radios_output(
+    $name,
+    $value,
+    $output,
+    $selected,
+    $extra,
+    $separator,
+    $labels,
+    $label_ids,
+    $escape
+) {
+    $_output = '';
+    if (is_object($value)) {
+        if (method_exists($value, '__toString')) {
+            $value = (string)$value->__toString();
+        } else {
+            trigger_error(
+                'html_options: value is an object of class \'' . get_class($value) .
+                '\' without __toString() method',
+                E_USER_NOTICE
+            );
+            return '';
+        }
+    } else {
+        $value = (string)$value;
+    }
+    if (is_object($output)) {
+        if (method_exists($output, '__toString')) {
+            $output = (string)$output->__toString();
+        } else {
+            trigger_error(
+                'html_options: output is an object of class \'' . get_class($output) .
+                '\' without __toString() method',
+                E_USER_NOTICE
+            );
+            return '';
+        }
+    } else {
+        $output = (string)$output;
+    }
+    if ($labels) {
+        if ($label_ids) {
+            $_id = smarty_function_escape_special_chars(
+                preg_replace(
+                    '![^\w\-\.]!' . Smarty::$_UTF8_MODIFIER,
+                    '_',
+                    $name . '_' . $value
+                )
+            );
+            $_output .= '<label for="' . $_id . '">';
+        } else {
+            $_output .= '<label>';
+        }
+    }
+    $name = smarty_function_escape_special_chars($name);
+    $value = smarty_function_escape_special_chars($value);
+    if ($escape) {
+        $output = smarty_function_escape_special_chars($output);
+    }
+    $_output .= '<input type="radio" name="' . $name . '" value="' . $value . '"';
+    if ($labels && $label_ids) {
+        $_output .= ' id="' . $_id . '"';
+    }
+    if ($value === $selected) {
+        $_output .= ' checked="checked"';
+    }
+    $_output .= $extra . ' />' . $output;
+    if ($labels) {
+        $_output .= '</label>';
+    }
+    $_output .= $separator;
+    return $_output;
+}

+ 388 - 0
third_party/smarty/plugins/function.html_select_date.php

@@ -0,0 +1,388 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_select_date} plugin
+ * Type:     function
+ * Name:     html_select_date
+ * Purpose:  Prints the dropdowns for date selection.
+ * ChangeLog:
+ *
+ *            - 1.0 initial release
+ *            - 1.1 added support for +/- N syntax for begin
+ *              and end year values. (Monte)
+ *            - 1.2 added support for yyyy-mm-dd syntax for
+ *              time value. (Jan Rosier)
+ *            - 1.3 added support for choosing format for
+ *              month values (Gary Loescher)
+ *            - 1.3.1 added support for choosing format for
+ *              day values (Marcus Bointon)
+ *            - 1.3.2 support negative timestamps, force year
+ *              dropdown to include given date unless explicitly set (Monte)
+ *            - 1.3.4 fix behaviour of 0000-00-00 00:00:00 dates to match that
+ *              of 0000-00-00 dates (cybot, boots)
+ *            - 2.0 complete rewrite for performance,
+ *              added attributes month_names, *_id
+ *
+ * @link    https://www.smarty.net/manual/en/language.function.html.select.date.php {html_select_date}
+ *           (Smarty online manual)
+ * @version 2.0
+ * @author  Andrei Zmievski
+ * @author  Monte Ohrt <monte at ohrt dot com>
+ * @author  Rodney Rehm
+ *
+ * @param array                     $params parameters
+ *
+ * @param \Smarty_Internal_Template $template
+ *
+ * @return string
+ * @throws \SmartyException
+ */
+function smarty_function_html_select_date($params, Smarty_Internal_Template $template)
+{
+    $template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_function_escape_special_chars',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.escape_special_chars.php'
+            )
+        )
+    );
+    // generate timestamps used for month names only
+    static $_month_timestamps = null;
+    static $_current_year = null;
+    if ($_month_timestamps === null) {
+        $_current_year = date('Y');
+        $_month_timestamps = array();
+        for ($i = 1; $i <= 12; $i++) {
+            $_month_timestamps[ $i ] = mktime(0, 0, 0, $i, 1, 2000);
+        }
+    }
+    /* Default values. */
+    $prefix = 'Date_';
+    $start_year = null;
+    $end_year = null;
+    $display_days = true;
+    $display_months = true;
+    $display_years = true;
+    $month_format = '%B';
+    /* Write months as numbers by default  GL */
+    $month_value_format = '%m';
+    $day_format = '%02d';
+    /* Write day values using this format MB */
+    $day_value_format = '%d';
+    $year_as_text = false;
+    /* Display years in reverse order? Ie. 2000,1999,.... */
+    $reverse_years = false;
+    /* Should the select boxes be part of an array when returned from PHP?
+       e.g. setting it to "birthday", would create "birthday[Day]",
+       "birthday[Month]" & "birthday[Year]". Can be combined with prefix */
+    $field_array = null;
+    /* <select size>'s of the different <select> tags.
+       If not set, uses default dropdown. */
+    $day_size = null;
+    $month_size = null;
+    $year_size = null;
+    /* Unparsed attributes common to *ALL* the <select>/<input> tags.
+       An example might be in the template: all_extra ='class ="foo"'. */
+    $all_extra = null;
+    /* Separate attributes for the tags. */
+    $day_extra = null;
+    $month_extra = null;
+    $year_extra = null;
+    /* Order in which to display the fields.
+       "D" -> day, "M" -> month, "Y" -> year. */
+    $field_order = 'MDY';
+    /* String printed between the different fields. */
+    $field_separator = "\n";
+    $option_separator = "\n";
+    $time = null;
+    // $all_empty = null;
+    // $day_empty = null;
+    // $month_empty = null;
+    // $year_empty = null;
+    $extra_attrs = '';
+    $all_id = null;
+    $day_id = null;
+    $month_id = null;
+    $year_id = null;
+    foreach ($params as $_key => $_value) {
+        switch ($_key) {
+            case 'time':
+                if (!is_array($_value) && $_value !== null) {
+                    $template->_checkPlugins(
+                        array(
+                            array(
+                                'function' => 'smarty_make_timestamp',
+                                'file'     => SMARTY_PLUGINS_DIR . 'shared.make_timestamp.php'
+                            )
+                        )
+                    );
+                    $time = smarty_make_timestamp($_value);
+                }
+                break;
+            case 'month_names':
+                if (is_array($_value) && count($_value) === 12) {
+                    $$_key = $_value;
+                } else {
+                    trigger_error('html_select_date: month_names must be an array of 12 strings', E_USER_NOTICE);
+                }
+                break;
+            case 'prefix':
+            case 'field_array':
+            case 'start_year':
+            case 'end_year':
+            case 'day_format':
+            case 'day_value_format':
+            case 'month_format':
+            case 'month_value_format':
+            case 'day_size':
+            case 'month_size':
+            case 'year_size':
+            case 'all_extra':
+            case 'day_extra':
+            case 'month_extra':
+            case 'year_extra':
+            case 'field_order':
+            case 'field_separator':
+            case 'option_separator':
+            case 'all_empty':
+            case 'month_empty':
+            case 'day_empty':
+            case 'year_empty':
+            case 'all_id':
+            case 'month_id':
+            case 'day_id':
+            case 'year_id':
+                $$_key = (string)$_value;
+                break;
+            case 'display_days':
+            case 'display_months':
+            case 'display_years':
+            case 'year_as_text':
+            case 'reverse_years':
+                $$_key = (bool)$_value;
+                break;
+            default:
+                if (!is_array($_value)) {
+                    $extra_attrs .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_value) . '"';
+                } else {
+                    trigger_error("html_select_date: extra attribute '{$_key}' cannot be an array", E_USER_NOTICE);
+                }
+                break;
+        }
+    }
+    // Note: date() is faster than strftime()
+    // Note: explode(date()) is faster than date() date() date()
+    if (isset($params[ 'time' ]) && is_array($params[ 'time' ])) {
+        if (isset($params[ 'time' ][ $prefix . 'Year' ])) {
+            // $_REQUEST[$field_array] given
+            foreach (array(
+                'Y' => 'Year',
+                'm' => 'Month',
+                'd' => 'Day'
+            ) as $_elementKey => $_elementName) {
+                $_variableName = '_' . strtolower($_elementName);
+                $$_variableName =
+                    isset($params[ 'time' ][ $prefix . $_elementName ]) ? $params[ 'time' ][ $prefix . $_elementName ] :
+                        date($_elementKey);
+            }
+        } elseif (isset($params[ 'time' ][ $field_array ][ $prefix . 'Year' ])) {
+            // $_REQUEST given
+            foreach (array(
+                'Y' => 'Year',
+                'm' => 'Month',
+                'd' => 'Day'
+            ) as $_elementKey => $_elementName) {
+                $_variableName = '_' . strtolower($_elementName);
+                $$_variableName = isset($params[ 'time' ][ $field_array ][ $prefix . $_elementName ]) ?
+                    $params[ 'time' ][ $field_array ][ $prefix . $_elementName ] : date($_elementKey);
+            }
+        } else {
+            // no date found, use NOW
+            list($_year, $_month, $_day) = $time = explode('-', date('Y-m-d'));
+        }
+    } elseif ($time === null) {
+        if (array_key_exists('time', $params)) {
+            $_year = $_month = $_day = $time = null;
+        } else {
+            list($_year, $_month, $_day) = $time = explode('-', date('Y-m-d'));
+        }
+    } else {
+        list($_year, $_month, $_day) = $time = explode('-', date('Y-m-d', $time));
+    }
+    // make syntax "+N" or "-N" work with $start_year and $end_year
+    // Note preg_match('!^(\+|\-)\s*(\d+)$!', $end_year, $match) is slower than trim+substr
+    foreach (array(
+        'start',
+        'end'
+    ) as $key) {
+        $key .= '_year';
+        $t = $$key;
+        if ($t === null) {
+            $$key = (int)$_current_year;
+        } elseif ($t[ 0 ] === '+') {
+            $$key = (int)($_current_year + (int)trim(substr($t, 1)));
+        } elseif ($t[ 0 ] === '-') {
+            $$key = (int)($_current_year - (int)trim(substr($t, 1)));
+        } else {
+            $$key = (int)$$key;
+        }
+    }
+    // flip for ascending or descending
+    if (($start_year > $end_year && !$reverse_years) || ($start_year < $end_year && $reverse_years)) {
+        $t = $end_year;
+        $end_year = $start_year;
+        $start_year = $t;
+    }
+    // generate year <select> or <input>
+    if ($display_years) {
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Year]') : ($prefix . 'Year');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($year_extra) {
+            $_extra .= ' ' . $year_extra;
+        }
+        if ($year_as_text) {
+            $_html_years =
+                '<input type="text" name="' . $_name . '" value="' . $_year . '" size="4" maxlength="4"' . $_extra .
+                $extra_attrs . ' />';
+        } else {
+            $_html_years = '<select name="' . $_name . '"';
+            if ($year_id !== null || $all_id !== null) {
+                $_html_years .= ' id="' . smarty_function_escape_special_chars(
+                        $year_id !== null ?
+                            ($year_id ? $year_id : $_name) :
+                            ($all_id ? ($all_id . $_name) :
+                                $_name)
+                    ) . '"';
+            }
+            if ($year_size) {
+                $_html_years .= ' size="' . $year_size . '"';
+            }
+            $_html_years .= $_extra . $extra_attrs . '>' . $option_separator;
+            if (isset($year_empty) || isset($all_empty)) {
+                $_html_years .= '<option value="">' . (isset($year_empty) ? $year_empty : $all_empty) . '</option>' .
+                                $option_separator;
+            }
+            $op = $start_year > $end_year ? -1 : 1;
+            for ($i = $start_year; $op > 0 ? $i <= $end_year : $i >= $end_year; $i += $op) {
+                $_html_years .= '<option value="' . $i . '"' . ($_year == $i ? ' selected="selected"' : '') . '>' . $i .
+                                '</option>' . $option_separator;
+            }
+            $_html_years .= '</select>';
+        }
+    }
+    // generate month <select> or <input>
+    if ($display_months) {
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Month]') : ($prefix . 'Month');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($month_extra) {
+            $_extra .= ' ' . $month_extra;
+        }
+        $_html_months = '<select name="' . $_name . '"';
+        if ($month_id !== null || $all_id !== null) {
+            $_html_months .= ' id="' . smarty_function_escape_special_chars(
+                    $month_id !== null ?
+                        ($month_id ? $month_id : $_name) :
+                        ($all_id ? ($all_id . $_name) :
+                            $_name)
+                ) . '"';
+        }
+        if ($month_size) {
+            $_html_months .= ' size="' . $month_size . '"';
+        }
+        $_html_months .= $_extra . $extra_attrs . '>' . $option_separator;
+        if (isset($month_empty) || isset($all_empty)) {
+            $_html_months .= '<option value="">' . (isset($month_empty) ? $month_empty : $all_empty) . '</option>' .
+                             $option_separator;
+        }
+        for ($i = 1; $i <= 12; $i++) {
+            $_val = sprintf('%02d', $i);
+            $_text = isset($month_names) ? smarty_function_escape_special_chars($month_names[ $i ]) :
+                ($month_format === '%m' ? $_val : strftime($month_format, $_month_timestamps[ $i ]));
+            $_value = $month_value_format === '%m' ? $_val : strftime($month_value_format, $_month_timestamps[ $i ]);
+            $_html_months .= '<option value="' . $_value . '"' . ($_val == $_month ? ' selected="selected"' : '') .
+                             '>' . $_text . '</option>' . $option_separator;
+        }
+        $_html_months .= '</select>';
+    }
+    // generate day <select> or <input>
+    if ($display_days) {
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Day]') : ($prefix . 'Day');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($day_extra) {
+            $_extra .= ' ' . $day_extra;
+        }
+        $_html_days = '<select name="' . $_name . '"';
+        if ($day_id !== null || $all_id !== null) {
+            $_html_days .= ' id="' .
+                           smarty_function_escape_special_chars(
+                               $day_id !== null ? ($day_id ? $day_id : $_name) :
+                                   ($all_id ? ($all_id . $_name) : $_name)
+                           ) . '"';
+        }
+        if ($day_size) {
+            $_html_days .= ' size="' . $day_size . '"';
+        }
+        $_html_days .= $_extra . $extra_attrs . '>' . $option_separator;
+        if (isset($day_empty) || isset($all_empty)) {
+            $_html_days .= '<option value="">' . (isset($day_empty) ? $day_empty : $all_empty) . '</option>' .
+                           $option_separator;
+        }
+        for ($i = 1; $i <= 31; $i++) {
+            $_val = sprintf('%02d', $i);
+            $_text = $day_format === '%02d' ? $_val : sprintf($day_format, $i);
+            $_value = $day_value_format === '%02d' ? $_val : sprintf($day_value_format, $i);
+            $_html_days .= '<option value="' . $_value . '"' . ($_val == $_day ? ' selected="selected"' : '') . '>' .
+                           $_text . '</option>' . $option_separator;
+        }
+        $_html_days .= '</select>';
+    }
+    // order the fields for output
+    $_html = '';
+    for ($i = 0; $i <= 2; $i++) {
+        switch ($field_order[ $i ]) {
+            case 'Y':
+            case 'y':
+                if (isset($_html_years)) {
+                    if ($_html) {
+                        $_html .= $field_separator;
+                    }
+                    $_html .= $_html_years;
+                }
+                break;
+            case 'm':
+            case 'M':
+                if (isset($_html_months)) {
+                    if ($_html) {
+                        $_html .= $field_separator;
+                    }
+                    $_html .= $_html_months;
+                }
+                break;
+            case 'd':
+            case 'D':
+                if (isset($_html_days)) {
+                    if ($_html) {
+                        $_html .= $field_separator;
+                    }
+                    $_html .= $_html_days;
+                }
+                break;
+        }
+    }
+    return $_html;
+}

+ 354 - 0
third_party/smarty/plugins/function.html_select_time.php

@@ -0,0 +1,354 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_select_time} function plugin
+ * Type:     function
+ * Name:     html_select_time
+ * Purpose:  Prints the dropdowns for time selection
+ *
+ * @link   https://www.smarty.net/manual/en/language.function.html.select.time.php {html_select_time}
+ *           (Smarty online manual)
+ * @author Roberto Berto <roberto@berto.net>
+ * @author Monte Ohrt <monte AT ohrt DOT com>
+ *
+ * @param array                     $params parameters
+ *
+ * @param \Smarty_Internal_Template $template
+ *
+ * @return string
+ * @uses   smarty_make_timestamp()
+ * @throws \SmartyException
+ */
+function smarty_function_html_select_time($params, Smarty_Internal_Template $template)
+{
+    $template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_function_escape_special_chars',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.escape_special_chars.php'
+            )
+        )
+    );
+    $prefix = 'Time_';
+    $field_array = null;
+    $field_separator = "\n";
+    $option_separator = "\n";
+    $time = null;
+    $display_hours = true;
+    $display_minutes = true;
+    $display_seconds = true;
+    $display_meridian = true;
+    $hour_format = '%02d';
+    $hour_value_format = '%02d';
+    $minute_format = '%02d';
+    $minute_value_format = '%02d';
+    $second_format = '%02d';
+    $second_value_format = '%02d';
+    $hour_size = null;
+    $minute_size = null;
+    $second_size = null;
+    $meridian_size = null;
+    $all_empty = null;
+    $hour_empty = null;
+    $minute_empty = null;
+    $second_empty = null;
+    $meridian_empty = null;
+    $all_id = null;
+    $hour_id = null;
+    $minute_id = null;
+    $second_id = null;
+    $meridian_id = null;
+    $use_24_hours = true;
+    $minute_interval = 1;
+    $second_interval = 1;
+    $extra_attrs = '';
+    $all_extra = null;
+    $hour_extra = null;
+    $minute_extra = null;
+    $second_extra = null;
+    $meridian_extra = null;
+    foreach ($params as $_key => $_value) {
+        switch ($_key) {
+            case 'time':
+                if (!is_array($_value) && $_value !== null) {
+                    $template->_checkPlugins(
+                        array(
+                            array(
+                                'function' => 'smarty_make_timestamp',
+                                'file'     => SMARTY_PLUGINS_DIR . 'shared.make_timestamp.php'
+                            )
+                        )
+                    );
+                    $time = smarty_make_timestamp($_value);
+                }
+                break;
+            case 'prefix':
+            case 'field_array':
+            case 'field_separator':
+            case 'option_separator':
+            case 'all_extra':
+            case 'hour_extra':
+            case 'minute_extra':
+            case 'second_extra':
+            case 'meridian_extra':
+            case 'all_empty':
+            case 'hour_empty':
+            case 'minute_empty':
+            case 'second_empty':
+            case 'meridian_empty':
+            case 'all_id':
+            case 'hour_id':
+            case 'minute_id':
+            case 'second_id':
+            case 'meridian_id':
+            case 'hour_format':
+            case 'hour_value_format':
+            case 'minute_format':
+            case 'minute_value_format':
+            case 'second_format':
+            case 'second_value_format':
+                $$_key = (string)$_value;
+                break;
+            case 'display_hours':
+            case 'display_minutes':
+            case 'display_seconds':
+            case 'display_meridian':
+            case 'use_24_hours':
+                $$_key = (bool)$_value;
+                break;
+            case 'minute_interval':
+            case 'second_interval':
+            case 'hour_size':
+            case 'minute_size':
+            case 'second_size':
+            case 'meridian_size':
+                $$_key = (int)$_value;
+                break;
+            default:
+                if (!is_array($_value)) {
+                    $extra_attrs .= ' ' . $_key . '="' . smarty_function_escape_special_chars($_value) . '"';
+                } else {
+                    trigger_error("html_select_date: extra attribute '{$_key}' cannot be an array", E_USER_NOTICE);
+                }
+                break;
+        }
+    }
+    if (isset($params[ 'time' ]) && is_array($params[ 'time' ])) {
+        if (isset($params[ 'time' ][ $prefix . 'Hour' ])) {
+            // $_REQUEST[$field_array] given
+            foreach (array(
+                'H' => 'Hour',
+                'i' => 'Minute',
+                's' => 'Second'
+            ) as $_elementKey => $_elementName) {
+                $_variableName = '_' . strtolower($_elementName);
+                $$_variableName =
+                    isset($params[ 'time' ][ $prefix . $_elementName ]) ? $params[ 'time' ][ $prefix . $_elementName ] :
+                        date($_elementKey);
+            }
+            $_meridian =
+                isset($params[ 'time' ][ $prefix . 'Meridian' ]) ? (' ' . $params[ 'time' ][ $prefix . 'Meridian' ]) :
+                    '';
+            $time = strtotime($_hour . ':' . $_minute . ':' . $_second . $_meridian);
+            list($_hour, $_minute, $_second) = $time = explode('-', date('H-i-s', $time));
+        } elseif (isset($params[ 'time' ][ $field_array ][ $prefix . 'Hour' ])) {
+            // $_REQUEST given
+            foreach (array(
+                'H' => 'Hour',
+                'i' => 'Minute',
+                's' => 'Second'
+            ) as $_elementKey => $_elementName) {
+                $_variableName = '_' . strtolower($_elementName);
+                $$_variableName = isset($params[ 'time' ][ $field_array ][ $prefix . $_elementName ]) ?
+                    $params[ 'time' ][ $field_array ][ $prefix . $_elementName ] : date($_elementKey);
+            }
+            $_meridian = isset($params[ 'time' ][ $field_array ][ $prefix . 'Meridian' ]) ?
+                (' ' . $params[ 'time' ][ $field_array ][ $prefix . 'Meridian' ]) : '';
+            $time = strtotime($_hour . ':' . $_minute . ':' . $_second . $_meridian);
+            list($_hour, $_minute, $_second) = $time = explode('-', date('H-i-s', $time));
+        } else {
+            // no date found, use NOW
+            list($_year, $_month, $_day) = $time = explode('-', date('Y-m-d'));
+        }
+    } elseif ($time === null) {
+        if (array_key_exists('time', $params)) {
+            $_hour = $_minute = $_second = $time = null;
+        } else {
+            list($_hour, $_minute, $_second) = $time = explode('-', date('H-i-s'));
+        }
+    } else {
+        list($_hour, $_minute, $_second) = $time = explode('-', date('H-i-s', $time));
+    }
+    // generate hour <select>
+    if ($display_hours) {
+        $_html_hours = '';
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Hour]') : ($prefix . 'Hour');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($hour_extra) {
+            $_extra .= ' ' . $hour_extra;
+        }
+        $_html_hours = '<select name="' . $_name . '"';
+        if ($hour_id !== null || $all_id !== null) {
+            $_html_hours .= ' id="' .
+                            smarty_function_escape_special_chars(
+                                $hour_id !== null ? ($hour_id ? $hour_id : $_name) :
+                                    ($all_id ? ($all_id . $_name) : $_name)
+                            ) . '"';
+        }
+        if ($hour_size) {
+            $_html_hours .= ' size="' . $hour_size . '"';
+        }
+        $_html_hours .= $_extra . $extra_attrs . '>' . $option_separator;
+        if (isset($hour_empty) || isset($all_empty)) {
+            $_html_hours .= '<option value="">' . (isset($hour_empty) ? $hour_empty : $all_empty) . '</option>' .
+                            $option_separator;
+        }
+        $start = $use_24_hours ? 0 : 1;
+        $end = $use_24_hours ? 23 : 12;
+        for ($i = $start; $i <= $end; $i++) {
+            $_val = sprintf('%02d', $i);
+            $_text = $hour_format === '%02d' ? $_val : sprintf($hour_format, $i);
+            $_value = $hour_value_format === '%02d' ? $_val : sprintf($hour_value_format, $i);
+            if (!$use_24_hours) {
+                $_hour12 = $_hour == 0 ? 12 : ($_hour <= 12 ? $_hour : $_hour - 12);
+            }
+            $selected = $_hour !== null ? ($use_24_hours ? $_hour == $_val : $_hour12 == $_val) : null;
+            $_html_hours .= '<option value="' . $_value . '"' . ($selected ? ' selected="selected"' : '') . '>' .
+                            $_text . '</option>' . $option_separator;
+        }
+        $_html_hours .= '</select>';
+    }
+    // generate minute <select>
+    if ($display_minutes) {
+        $_html_minutes = '';
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Minute]') : ($prefix . 'Minute');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($minute_extra) {
+            $_extra .= ' ' . $minute_extra;
+        }
+        $_html_minutes = '<select name="' . $_name . '"';
+        if ($minute_id !== null || $all_id !== null) {
+            $_html_minutes .= ' id="' . smarty_function_escape_special_chars(
+                    $minute_id !== null ?
+                        ($minute_id ? $minute_id : $_name) :
+                        ($all_id ? ($all_id . $_name) :
+                            $_name)
+                ) . '"';
+        }
+        if ($minute_size) {
+            $_html_minutes .= ' size="' . $minute_size . '"';
+        }
+        $_html_minutes .= $_extra . $extra_attrs . '>' . $option_separator;
+        if (isset($minute_empty) || isset($all_empty)) {
+            $_html_minutes .= '<option value="">' . (isset($minute_empty) ? $minute_empty : $all_empty) . '</option>' .
+                              $option_separator;
+        }
+        $selected = $_minute !== null ? ($_minute - $_minute % $minute_interval) : null;
+        for ($i = 0; $i <= 59; $i += $minute_interval) {
+            $_val = sprintf('%02d', $i);
+            $_text = $minute_format === '%02d' ? $_val : sprintf($minute_format, $i);
+            $_value = $minute_value_format === '%02d' ? $_val : sprintf($minute_value_format, $i);
+            $_html_minutes .= '<option value="' . $_value . '"' . ($selected === $i ? ' selected="selected"' : '') .
+                              '>' . $_text . '</option>' . $option_separator;
+        }
+        $_html_minutes .= '</select>';
+    }
+    // generate second <select>
+    if ($display_seconds) {
+        $_html_seconds = '';
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Second]') : ($prefix . 'Second');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($second_extra) {
+            $_extra .= ' ' . $second_extra;
+        }
+        $_html_seconds = '<select name="' . $_name . '"';
+        if ($second_id !== null || $all_id !== null) {
+            $_html_seconds .= ' id="' . smarty_function_escape_special_chars(
+                    $second_id !== null ?
+                        ($second_id ? $second_id : $_name) :
+                        ($all_id ? ($all_id . $_name) :
+                            $_name)
+                ) . '"';
+        }
+        if ($second_size) {
+            $_html_seconds .= ' size="' . $second_size . '"';
+        }
+        $_html_seconds .= $_extra . $extra_attrs . '>' . $option_separator;
+        if (isset($second_empty) || isset($all_empty)) {
+            $_html_seconds .= '<option value="">' . (isset($second_empty) ? $second_empty : $all_empty) . '</option>' .
+                              $option_separator;
+        }
+        $selected = $_second !== null ? ($_second - $_second % $second_interval) : null;
+        for ($i = 0; $i <= 59; $i += $second_interval) {
+            $_val = sprintf('%02d', $i);
+            $_text = $second_format === '%02d' ? $_val : sprintf($second_format, $i);
+            $_value = $second_value_format === '%02d' ? $_val : sprintf($second_value_format, $i);
+            $_html_seconds .= '<option value="' . $_value . '"' . ($selected === $i ? ' selected="selected"' : '') .
+                              '>' . $_text . '</option>' . $option_separator;
+        }
+        $_html_seconds .= '</select>';
+    }
+    // generate meridian <select>
+    if ($display_meridian && !$use_24_hours) {
+        $_html_meridian = '';
+        $_extra = '';
+        $_name = $field_array ? ($field_array . '[' . $prefix . 'Meridian]') : ($prefix . 'Meridian');
+        if ($all_extra) {
+            $_extra .= ' ' . $all_extra;
+        }
+        if ($meridian_extra) {
+            $_extra .= ' ' . $meridian_extra;
+        }
+        $_html_meridian = '<select name="' . $_name . '"';
+        if ($meridian_id !== null || $all_id !== null) {
+            $_html_meridian .= ' id="' . smarty_function_escape_special_chars(
+                    $meridian_id !== null ?
+                        ($meridian_id ? $meridian_id :
+                            $_name) :
+                        ($all_id ? ($all_id . $_name) :
+                            $_name)
+                ) . '"';
+        }
+        if ($meridian_size) {
+            $_html_meridian .= ' size="' . $meridian_size . '"';
+        }
+        $_html_meridian .= $_extra . $extra_attrs . '>' . $option_separator;
+        if (isset($meridian_empty) || isset($all_empty)) {
+            $_html_meridian .= '<option value="">' . (isset($meridian_empty) ? $meridian_empty : $all_empty) .
+                               '</option>' . $option_separator;
+        }
+        $_html_meridian .= '<option value="am"' . ($_hour > 0 && $_hour < 12 ? ' selected="selected"' : '') .
+                           '>AM</option>' . $option_separator . '<option value="pm"' .
+                           ($_hour < 12 ? '' : ' selected="selected"') . '>PM</option>' . $option_separator .
+                           '</select>';
+    }
+    $_html = '';
+    foreach (array(
+        '_html_hours',
+        '_html_minutes',
+        '_html_seconds',
+        '_html_meridian'
+    ) as $k) {
+        if (isset($$k)) {
+            if ($_html) {
+                $_html .= $field_separator;
+            }
+            $_html .= $$k;
+        }
+    }
+    return $_html;
+}

+ 164 - 0
third_party/smarty/plugins/function.html_table.php

@@ -0,0 +1,164 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {html_table} function plugin
+ * Type:     function
+ * Name:     html_table
+ * Date:     Feb 17, 2003
+ * Purpose:  make an html table from an array of data
+ * Params:
+ *
+ * - loop       - array to loop through
+ * - cols       - number of columns, comma separated list of column names
+ *                or array of column names
+ * - rows       - number of rows
+ * - table_attr - table attributes
+ * - th_attr    - table heading attributes (arrays are cycled)
+ * - tr_attr    - table row attributes (arrays are cycled)
+ * - td_attr    - table cell attributes (arrays are cycled)
+ * - trailpad   - value to pad trailing cells with
+ * - caption    - text for caption element
+ * - vdir       - vertical direction (default: "down", means top-to-bottom)
+ * - hdir       - horizontal direction (default: "right", means left-to-right)
+ * - inner      - inner loop (default "cols": print $loop line by line,
+ *                $loop will be printed column by column otherwise)
+ *
+ * Examples:
+ *
+ * {table loop=$data}
+ * {table loop=$data cols=4 tr_attr='"bgcolor=red"'}
+ * {table loop=$data cols="first,second,third" tr_attr=$colors}
+ *
+ * @author  Monte Ohrt <monte at ohrt dot com>
+ * @author  credit to Messju Mohr <messju at lammfellpuschen dot de>
+ * @author  credit to boots <boots dot smarty at yahoo dot com>
+ * @version 1.1
+ * @link    https://www.smarty.net/manual/en/language.function.html.table.php {html_table}
+ *           (Smarty online manual)
+ *
+ * @param array $params parameters
+ *
+ * @return string
+ */
+function smarty_function_html_table($params)
+{
+    $table_attr = 'border="1"';
+    $tr_attr = '';
+    $th_attr = '';
+    $td_attr = '';
+    $cols = $cols_count = 3;
+    $rows = 3;
+    $trailpad = '&nbsp;';
+    $vdir = 'down';
+    $hdir = 'right';
+    $inner = 'cols';
+    $caption = '';
+    $loop = null;
+    if (!isset($params[ 'loop' ])) {
+        trigger_error("html_table: missing 'loop' parameter", E_USER_WARNING);
+        return;
+    }
+    foreach ($params as $_key => $_value) {
+        switch ($_key) {
+            case 'loop':
+                $$_key = (array)$_value;
+                break;
+            case 'cols':
+                if (is_array($_value) && !empty($_value)) {
+                    $cols = $_value;
+                    $cols_count = count($_value);
+                } elseif (!is_numeric($_value) && is_string($_value) && !empty($_value)) {
+                    $cols = explode(',', $_value);
+                    $cols_count = count($cols);
+                } elseif (!empty($_value)) {
+                    $cols_count = (int)$_value;
+                } else {
+                    $cols_count = $cols;
+                }
+                break;
+            case 'rows':
+                $$_key = (int)$_value;
+                break;
+            case 'table_attr':
+            case 'trailpad':
+            case 'hdir':
+            case 'vdir':
+            case 'inner':
+            case 'caption':
+                $$_key = (string)$_value;
+                break;
+            case 'tr_attr':
+            case 'td_attr':
+            case 'th_attr':
+                $$_key = $_value;
+                break;
+        }
+    }
+    $loop_count = count($loop);
+    if (empty($params[ 'rows' ])) {
+        /* no rows specified */
+        $rows = ceil($loop_count / $cols_count);
+    } elseif (empty($params[ 'cols' ])) {
+        if (!empty($params[ 'rows' ])) {
+            /* no cols specified, but rows */
+            $cols_count = ceil($loop_count / $rows);
+        }
+    }
+    $output = "<table $table_attr>\n";
+    if (!empty($caption)) {
+        $output .= '<caption>' . $caption . "</caption>\n";
+    }
+    if (is_array($cols)) {
+        $cols = ($hdir === 'right') ? $cols : array_reverse($cols);
+        $output .= "<thead><tr>\n";
+        for ($r = 0; $r < $cols_count; $r++) {
+            $output .= '<th' . smarty_function_html_table_cycle('th', $th_attr, $r) . '>';
+            $output .= $cols[ $r ];
+            $output .= "</th>\n";
+        }
+        $output .= "</tr></thead>\n";
+    }
+    $output .= "<tbody>\n";
+    for ($r = 0; $r < $rows; $r++) {
+        $output .= "<tr" . smarty_function_html_table_cycle('tr', $tr_attr, $r) . ">\n";
+        $rx = ($vdir === 'down') ? $r * $cols_count : ($rows - 1 - $r) * $cols_count;
+        for ($c = 0; $c < $cols_count; $c++) {
+            $x = ($hdir === 'right') ? $rx + $c : $rx + $cols_count - 1 - $c;
+            if ($inner !== 'cols') {
+                /* shuffle x to loop over rows*/
+                $x = floor($x / $cols_count) + ($x % $cols_count) * $rows;
+            }
+            if ($x < $loop_count) {
+                $output .= "<td" . smarty_function_html_table_cycle('td', $td_attr, $c) . ">" . $loop[ $x ] . "</td>\n";
+            } else {
+                $output .= "<td" . smarty_function_html_table_cycle('td', $td_attr, $c) . ">$trailpad</td>\n";
+            }
+        }
+        $output .= "</tr>\n";
+    }
+    $output .= "</tbody>\n";
+    $output .= "</table>\n";
+    return $output;
+}
+
+/**
+ * @param $name
+ * @param $var
+ * @param $no
+ *
+ * @return string
+ */
+function smarty_function_html_table_cycle($name, $var, $no)
+{
+    if (!is_array($var)) {
+        $ret = $var;
+    } else {
+        $ret = $var[ $no % count($var) ];
+    }
+    return ($ret) ? ' ' . $ret : '';
+}

+ 134 - 0
third_party/smarty/plugins/function.mailto.php

@@ -0,0 +1,134 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {mailto} function plugin
+ * Type:     function
+ * Name:     mailto
+ * Date:     May 21, 2002
+ * Purpose:  automate mailto address link creation, and optionally encode them.
+ * Params:
+ *
+ * - address    - (required) - e-mail address
+ * - text       - (optional) - text to display, default is address
+ * - encode     - (optional) - can be one of:
+ *                             * none : no encoding (default)
+ *                             * javascript : encode with javascript
+ *                             * javascript_charcode : encode with javascript charcode
+ *                             * hex : encode with hexadecimal (no javascript)
+ * - cc         - (optional) - address(es) to carbon copy
+ * - bcc        - (optional) - address(es) to blind carbon copy
+ * - subject    - (optional) - e-mail subject
+ * - newsgroups - (optional) - newsgroup(s) to post to
+ * - followupto - (optional) - address(es) to follow up to
+ * - extra      - (optional) - extra tags for the href link
+ *
+ * Examples:
+ *
+ * {mailto address="me@domain.com"}
+ * {mailto address="me@domain.com" encode="javascript"}
+ * {mailto address="me@domain.com" encode="hex"}
+ * {mailto address="me@domain.com" subject="Hello to you!"}
+ * {mailto address="me@domain.com" cc="you@domain.com,they@domain.com"}
+ * {mailto address="me@domain.com" extra='class="mailto"'}
+ *
+ * @link    https://www.smarty.net/manual/en/language.function.mailto.php {mailto}
+ *           (Smarty online manual)
+ * @version 1.2
+ * @author  Monte Ohrt <monte at ohrt dot com>
+ * @author  credits to Jason Sweat (added cc, bcc and subject functionality)
+ *
+ * @param array $params parameters
+ *
+ * @return string
+ */
+function smarty_function_mailto($params)
+{
+    static $_allowed_encoding =
+        array('javascript' => true, 'javascript_charcode' => true, 'hex' => true, 'none' => true);
+    $extra = '';
+    if (empty($params[ 'address' ])) {
+        trigger_error("mailto: missing 'address' parameter", E_USER_WARNING);
+        return;
+    } else {
+        $address = $params[ 'address' ];
+    }
+    $text = $address;
+    // netscape and mozilla do not decode %40 (@) in BCC field (bug?)
+    // so, don't encode it.
+    $search = array('%40', '%2C');
+    $replace = array('@', ',');
+    $mail_parms = array();
+    foreach ($params as $var => $value) {
+        switch ($var) {
+            case 'cc':
+            case 'bcc':
+            case 'followupto':
+                if (!empty($value)) {
+                    $mail_parms[] = $var . '=' . str_replace($search, $replace, rawurlencode($value));
+                }
+                break;
+            case 'subject':
+            case 'newsgroups':
+                $mail_parms[] = $var . '=' . rawurlencode($value);
+                break;
+            case 'extra':
+            case 'text':
+                $$var = $value;
+            // no break
+            default:
+        }
+    }
+    if ($mail_parms) {
+        $address .= '?' . join('&', $mail_parms);
+    }
+    $encode = (empty($params[ 'encode' ])) ? 'none' : $params[ 'encode' ];
+    if (!isset($_allowed_encoding[ $encode ])) {
+        trigger_error(
+            "mailto: 'encode' parameter must be none, javascript, javascript_charcode or hex",
+            E_USER_WARNING
+        );
+        return;
+    }
+    if ($encode === 'javascript') {
+	    $string = '<a href="mailto:' . $address . '" ' . $extra . '>' . $text . '</a>';
+        $js_encode = '';
+        for ($x = 0, $_length = strlen($string); $x < $_length; $x++) {
+            $js_encode .= '%' . bin2hex($string[ $x ]);
+        }
+        return '<script type="text/javascript">document.write(unescape(\'' . $js_encode . '\'))</script>';
+    } elseif ($encode === 'javascript_charcode') {
+        $string = '<a href="mailto:' . $address . '" ' . $extra . '>' . $text . '</a>';
+        for ($x = 0, $_length = strlen($string); $x < $_length; $x++) {
+            $ord[] = ord($string[ $x ]);
+        }
+	    return '<script type="text/javascript">document.write(String.fromCharCode(' . implode(',', $ord) . '))</script>';
+    } elseif ($encode === 'hex') {
+        preg_match('!^(.*)(\?.*)$!', $address, $match);
+        if (!empty($match[ 2 ])) {
+            trigger_error("mailto: hex encoding does not work with extra attributes. Try javascript.", E_USER_WARNING);
+            return;
+        }
+        $address_encode = '';
+        for ($x = 0, $_length = strlen($address); $x < $_length; $x++) {
+            if (preg_match('!\w!' . Smarty::$_UTF8_MODIFIER, $address[ $x ])) {
+                $address_encode .= '%' . bin2hex($address[ $x ]);
+            } else {
+                $address_encode .= $address[ $x ];
+            }
+        }
+        $text_encode = '';
+        for ($x = 0, $_length = strlen($text); $x < $_length; $x++) {
+            $text_encode .= '&#x' . bin2hex($text[ $x ]) . ';';
+        }
+        $mailto = "&#109;&#97;&#105;&#108;&#116;&#111;&#58;";
+        return '<a href="' . $mailto . $address_encode . '" ' . $extra . '>' . $text_encode . '</a>';
+    } else {
+        // no encoding
+        return '<a href="mailto:' . $address . '" ' . $extra . '>' . $text . '</a>';
+    }
+}

+ 142 - 0
third_party/smarty/plugins/function.math.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * Smarty plugin
+ * This plugin is only for Smarty2 BC
+ *
+ * @package    Smarty
+ * @subpackage PluginsFunction
+ */
+/**
+ * Smarty {math} function plugin
+ * Type:     function
+ * Name:     math
+ * Purpose:  handle math computations in template
+ *
+ * @link   https://www.smarty.net/manual/en/language.function.math.php {math}
+ *           (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param array                    $params   parameters
+ * @param Smarty_Internal_Template $template template object
+ *
+ * @return string|null
+ */
+function smarty_function_math($params, $template)
+{
+    static $_allowed_funcs =
+        array(
+            'int'   => true,
+            'abs'   => true,
+            'ceil'  => true,
+            'acos'   => true,
+            'acosh'   => true,
+            'cos'   => true,
+            'cosh'   => true,
+            'deg2rad'   => true,
+            'rad2deg'   => true,
+            'exp'   => true,
+            'floor' => true,
+            'log'   => true,
+            'log10' => true,
+            'max'   => true,
+            'min'   => true,
+            'pi'    => true,
+            'pow'   => true,
+            'rand'  => true,
+            'round' => true,
+            'asin'   => true,
+            'asinh'   => true,
+            'sin'   => true,
+            'sinh'   => true,
+            'sqrt'  => true,
+            'srand' => true,
+            'atan'   => true,
+            'atanh'   => true,
+            'tan'   => true,
+            'tanh'   => true
+        );
+
+    // be sure equation parameter is present
+    if (empty($params[ 'equation' ])) {
+        trigger_error("math: missing equation parameter", E_USER_WARNING);
+        return;
+    }
+    $equation = $params[ 'equation' ];
+
+    // Remove whitespaces
+    $equation = preg_replace('/\s+/', '', $equation);
+
+    // Adapted from https://www.php.net/manual/en/function.eval.php#107377
+    $number = '(?:\d+(?:[,.]\d+)?|pi|π)'; // What is a number
+    $functionsOrVars = '((?:0x[a-fA-F0-9]+)|([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))';
+    $operators = '[+\/*\^%-]'; // Allowed math operators
+    $regexp = '/^(('.$number.'|'.$functionsOrVars.'|('.$functionsOrVars.'\s*\((?1)+\)|\((?1)+\)))(?:'.$operators.'(?1))?)+$/';
+
+    if (!preg_match($regexp, $equation)) {
+        trigger_error("math: illegal characters", E_USER_WARNING);
+        return;
+    }
+
+    // make sure parenthesis are balanced
+    if (substr_count($equation, '(') !== substr_count($equation, ')')) {
+        trigger_error("math: unbalanced parenthesis", E_USER_WARNING);
+        return;
+    }
+
+    // disallow backticks
+    if (strpos($equation, '`') !== false) {
+        trigger_error("math: backtick character not allowed in equation", E_USER_WARNING);
+        return;
+    }
+
+    // also disallow dollar signs
+    if (strpos($equation, '$') !== false) {
+        trigger_error("math: dollar signs not allowed in equation", E_USER_WARNING);
+        return;
+    }
+    foreach ($params as $key => $val) {
+        if ($key !== 'equation' && $key !== 'format' && $key !== 'assign') {
+            // make sure value is not empty
+            if (strlen($val) === 0) {
+                trigger_error("math: parameter '{$key}' is empty", E_USER_WARNING);
+                return;
+            }
+            if (!is_numeric($val)) {
+                trigger_error("math: parameter '{$key}' is not numeric", E_USER_WARNING);
+                return;
+            }
+        }
+    }
+    // match all vars in equation, make sure all are passed
+    preg_match_all('!(?:0x[a-fA-F0-9]+)|([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)!', $equation, $match);
+    foreach ($match[ 1 ] as $curr_var) {
+        if ($curr_var && !isset($params[ $curr_var ]) && !isset($_allowed_funcs[ $curr_var ])) {
+            trigger_error(
+                "math: function call '{$curr_var}' not allowed, or missing parameter '{$curr_var}'",
+                E_USER_WARNING
+            );
+            return;
+        }
+    }
+    foreach ($params as $key => $val) {
+        if ($key !== 'equation' && $key !== 'format' && $key !== 'assign') {
+            $equation = preg_replace("/\b$key\b/", " \$params['$key'] ", $equation);
+        }
+    }
+    $smarty_math_result = null;
+    eval("\$smarty_math_result = " . $equation . ";");
+
+    if (empty($params[ 'format' ])) {
+        if (empty($params[ 'assign' ])) {
+            return $smarty_math_result;
+        } else {
+            $template->assign($params[ 'assign' ], $smarty_math_result);
+        }
+    } else {
+        if (empty($params[ 'assign' ])) {
+            printf($params[ 'format' ], $smarty_math_result);
+        } else {
+            $template->assign($params[ 'assign' ], sprintf($params[ 'format' ], $smarty_math_result));
+        }
+    }
+}

+ 145 - 0
third_party/smarty/plugins/modifier.capitalize.php

@@ -0,0 +1,145 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty capitalize modifier plugin
+ * Type:     modifier
+ * Name:     capitalize
+ * Purpose:  capitalize words in the string
+ * {@internal {$string|capitalize:true:true} is the fastest option for MBString enabled systems }}
+ *
+ * @param string  $string    string to capitalize
+ * @param boolean $uc_digits also capitalize "x123" to "X123"
+ * @param boolean $lc_rest   capitalize first letters, lowercase all following letters "aAa" to "Aaa"
+ *
+ * @return string capitalized string
+ * @author Monte Ohrt <monte at ohrt dot com>
+ * @author Rodney Rehm
+ */
+function smarty_modifier_capitalize($string, $uc_digits = false, $lc_rest = false)
+{
+    if (Smarty::$_MBSTRING) {
+        if ($lc_rest) {
+            // uppercase (including hyphenated words)
+            $upper_string = mb_convert_case($string, MB_CASE_TITLE, Smarty::$_CHARSET);
+        } else {
+            // uppercase word breaks
+            $upper_string = preg_replace_callback(
+                "!(^|[^\p{L}'])([\p{Ll}])!S" . Smarty::$_UTF8_MODIFIER,
+                'smarty_mod_cap_mbconvert_cb',
+                $string
+            );
+        }
+        // check uc_digits case
+        if (!$uc_digits) {
+            if (preg_match_all(
+                "!\b([\p{L}]*[\p{N}]+[\p{L}]*)\b!" . Smarty::$_UTF8_MODIFIER,
+                $string,
+                $matches,
+                PREG_OFFSET_CAPTURE
+            )
+            ) {
+                foreach ($matches[ 1 ] as $match) {
+                    $upper_string =
+                        substr_replace(
+                            $upper_string,
+                            mb_strtolower($match[ 0 ], Smarty::$_CHARSET),
+                            $match[ 1 ],
+                            strlen($match[ 0 ])
+                        );
+                }
+            }
+        }
+        $upper_string =
+            preg_replace_callback(
+                "!((^|\s)['\"])(\w)!" . Smarty::$_UTF8_MODIFIER,
+                'smarty_mod_cap_mbconvert2_cb',
+                $upper_string
+            );
+        return $upper_string;
+    }
+    // lowercase first
+    if ($lc_rest) {
+        $string = strtolower($string);
+    }
+    // uppercase (including hyphenated words)
+    $upper_string =
+        preg_replace_callback(
+            "!(^|[^\p{L}'])([\p{Ll}])!S" . Smarty::$_UTF8_MODIFIER,
+            'smarty_mod_cap_ucfirst_cb',
+            $string
+        );
+    // check uc_digits case
+    if (!$uc_digits) {
+        if (preg_match_all(
+            "!\b([\p{L}]*[\p{N}]+[\p{L}]*)\b!" . Smarty::$_UTF8_MODIFIER,
+            $string,
+            $matches,
+            PREG_OFFSET_CAPTURE
+        )
+        ) {
+            foreach ($matches[ 1 ] as $match) {
+                $upper_string =
+                    substr_replace($upper_string, strtolower($match[ 0 ]), $match[ 1 ], strlen($match[ 0 ]));
+            }
+        }
+    }
+    $upper_string = preg_replace_callback(
+        "!((^|\s)['\"])(\w)!" . Smarty::$_UTF8_MODIFIER,
+        'smarty_mod_cap_ucfirst2_cb',
+        $upper_string
+    );
+    return $upper_string;
+}
+
+/**
+ *
+ * Bug: create_function() use exhausts memory when used in long loops
+ * Fix: use declared functions for callbacks instead of using create_function()
+ * Note: This can be fixed using anonymous functions instead, but that requires PHP >= 5.3
+ *
+ * @author Kyle Renfrow
+ */
+/**
+ * @param $matches
+ *
+ * @return string
+ */
+function smarty_mod_cap_mbconvert_cb($matches)
+{
+    return stripslashes($matches[ 1 ]) . mb_convert_case(stripslashes($matches[ 2 ]), MB_CASE_UPPER, Smarty::$_CHARSET);
+}
+
+/**
+ * @param $matches
+ *
+ * @return string
+ */
+function smarty_mod_cap_mbconvert2_cb($matches)
+{
+    return stripslashes($matches[ 1 ]) . mb_convert_case(stripslashes($matches[ 3 ]), MB_CASE_UPPER, Smarty::$_CHARSET);
+}
+
+/**
+ * @param $matches
+ *
+ * @return string
+ */
+function smarty_mod_cap_ucfirst_cb($matches)
+{
+    return stripslashes($matches[ 1 ]) . ucfirst(stripslashes($matches[ 2 ]));
+}
+
+/**
+ * @param $matches
+ *
+ * @return string
+ */
+function smarty_mod_cap_ucfirst2_cb($matches)
+{
+    return stripslashes($matches[ 1 ]) . ucfirst(stripslashes($matches[ 3 ]));
+}

+ 85 - 0
third_party/smarty/plugins/modifier.date_format.php

@@ -0,0 +1,85 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty date_format modifier plugin
+ * Type:     modifier
+ * Name:     date_format
+ * Purpose:  format datestamps via strftime
+ * Input:
+ *          - string: input date string
+ *          - format: strftime format for output
+ *          - default_date: default date if $string is empty
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.date.format.php date_format (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param string $string       input date string
+ * @param string $format       strftime format for output
+ * @param string $default_date default date if $string is empty
+ * @param string $formatter    either 'strftime' or 'auto'
+ *
+ * @return string |void
+ * @uses   smarty_make_timestamp()
+ */
+function smarty_modifier_date_format($string, $format = null, $default_date = '', $formatter = 'auto')
+{
+    if ($format === null) {
+        $format = Smarty::$_DATE_FORMAT;
+    }
+    /**
+     * require_once the {@link shared.make_timestamp.php} plugin
+     */
+    static $is_loaded = false;
+    if (!$is_loaded) {
+        if (!is_callable('smarty_make_timestamp')) {
+            include_once SMARTY_PLUGINS_DIR . 'shared.make_timestamp.php';
+        }
+        $is_loaded = true;
+    }
+    if (!empty($string) && $string !== '0000-00-00' && $string !== '0000-00-00 00:00:00') {
+        $timestamp = smarty_make_timestamp($string);
+    } elseif (!empty($default_date)) {
+        $timestamp = smarty_make_timestamp($default_date);
+    } else {
+        return;
+    }
+    if ($formatter === 'strftime' || ($formatter === 'auto' && strpos($format, '%') !== false)) {
+        if (Smarty::$_IS_WINDOWS) {
+            $_win_from = array(
+                '%D',
+                '%h',
+                '%n',
+                '%r',
+                '%R',
+                '%t',
+                '%T'
+            );
+            $_win_to = array(
+                '%m/%d/%y',
+                '%b',
+                "\n",
+                '%I:%M:%S %p',
+                '%H:%M',
+                "\t",
+                '%H:%M:%S'
+            );
+            if (strpos($format, '%e') !== false) {
+                $_win_from[] = '%e';
+                $_win_to[] = sprintf('%\' 2d', date('j', $timestamp));
+            }
+            if (strpos($format, '%l') !== false) {
+                $_win_from[] = '%l';
+                $_win_to[] = sprintf('%\' 2d', date('h', $timestamp));
+            }
+            $format = str_replace($_win_from, $_win_to, $format);
+        }
+        return strftime($format, $timestamp);
+    } else {
+        return date($format, $timestamp);
+    }
+}

+ 103 - 0
third_party/smarty/plugins/modifier.debug_print_var.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage Debug
+ */
+/**
+ * Smarty debug_print_var modifier plugin
+ * Type:     modifier
+ * Name:     debug_print_var
+ * Purpose:  formats variable contents for display in the console
+ *
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param array|object $var     variable to be formatted
+ * @param int          $max     maximum recursion depth if $var is an array or object
+ * @param int          $length  maximum string length if $var is a string
+ * @param int          $depth   actual recursion depth
+ * @param array        $objects processed objects in actual depth to prevent recursive object processing
+ *
+ * @return string
+ */
+function smarty_modifier_debug_print_var($var, $max = 10, $length = 40, $depth = 0, $objects = array())
+{
+    $_replace = array("\n" => '\n', "\r" => '\r', "\t" => '\t');
+    switch (gettype($var)) {
+        case 'array':
+            $results = '<b>Array (' . count($var) . ')</b>';
+            if ($depth === $max) {
+                break;
+            }
+            foreach ($var as $curr_key => $curr_val) {
+                $results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b>' . strtr($curr_key, $_replace) .
+                            '</b> =&gt; ' .
+                            smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
+                $depth--;
+            }
+            break;
+        case 'object':
+            $object_vars = get_object_vars($var);
+            $results = '<b>' . get_class($var) . ' Object (' . count($object_vars) . ')</b>';
+            if (in_array($var, $objects)) {
+                $results .= ' called recursive';
+                break;
+            }
+            if ($depth === $max) {
+                break;
+            }
+            $objects[] = $var;
+            foreach ($object_vars as $curr_key => $curr_val) {
+                $results .= '<br>' . str_repeat('&nbsp;', $depth * 2) . '<b> -&gt;' . strtr($curr_key, $_replace) .
+                            '</b> = ' . smarty_modifier_debug_print_var($curr_val, $max, $length, ++$depth, $objects);
+                $depth--;
+            }
+            break;
+        case 'boolean':
+        case 'NULL':
+        case 'resource':
+            if (true === $var) {
+                $results = 'true';
+            } elseif (false === $var) {
+                $results = 'false';
+            } elseif (null === $var) {
+                $results = 'null';
+            } else {
+                $results = htmlspecialchars((string)$var);
+            }
+            $results = '<i>' . $results . '</i>';
+            break;
+        case 'integer':
+        case 'float':
+            $results = htmlspecialchars((string)$var);
+            break;
+        case 'string':
+            $results = strtr($var, $_replace);
+            if (Smarty::$_MBSTRING) {
+                if (mb_strlen($var, Smarty::$_CHARSET) > $length) {
+                    $results = mb_substr($var, 0, $length - 3, Smarty::$_CHARSET) . '...';
+                }
+            } else {
+                if (isset($var[ $length ])) {
+                    $results = substr($var, 0, $length - 3) . '...';
+                }
+            }
+            $results = htmlspecialchars('"' . $results . '"', ENT_QUOTES, Smarty::$_CHARSET);
+            break;
+        case 'unknown type':
+        default:
+            $results = strtr((string)$var, $_replace);
+            if (Smarty::$_MBSTRING) {
+                if (mb_strlen($results, Smarty::$_CHARSET) > $length) {
+                    $results = mb_substr($results, 0, $length - 3, Smarty::$_CHARSET) . '...';
+                }
+            } else {
+                if (strlen($results) > $length) {
+                    $results = substr($results, 0, $length - 3) . '...';
+                }
+            }
+            $results = htmlspecialchars($results, ENT_QUOTES, Smarty::$_CHARSET);
+    }
+    return $results;
+}

+ 257 - 0
third_party/smarty/plugins/modifier.escape.php

@@ -0,0 +1,257 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty escape modifier plugin
+ * Type:     modifier
+ * Name:     escape
+ * Purpose:  escape string for output
+ *
+ * @link   https://www.smarty.net/docs/en/language.modifier.escape
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param string  $string        input string
+ * @param string  $esc_type      escape type
+ * @param string  $char_set      character set, used for htmlspecialchars() or htmlentities()
+ * @param boolean $double_encode encode already encoded entitites again, used for htmlspecialchars() or htmlentities()
+ *
+ * @return string escaped input string
+ */
+function smarty_modifier_escape($string, $esc_type = 'html', $char_set = null, $double_encode = true)
+{
+    static $_double_encode = true;
+    static $is_loaded_1 = false;
+    static $is_loaded_2 = false;
+    if (!$char_set) {
+        $char_set = Smarty::$_CHARSET;
+    }
+    switch ($esc_type) {
+        case 'html':
+            if ($_double_encode) {
+                // php >=5.3.2 - go native
+                return htmlspecialchars($string, ENT_QUOTES, $char_set, $double_encode);
+            } else {
+                if ($double_encode) {
+                    // php <5.2.3 - only handle double encoding
+                    return htmlspecialchars($string, ENT_QUOTES, $char_set);
+                } else {
+                    // php <5.2.3 - prevent double encoding
+                    $string = preg_replace('!&(#?\w+);!', '%%%SMARTY_START%%%\\1%%%SMARTY_END%%%', $string);
+                    $string = htmlspecialchars($string, ENT_QUOTES, $char_set);
+                    $string = str_replace(
+                        array(
+                            '%%%SMARTY_START%%%',
+                            '%%%SMARTY_END%%%'
+                        ),
+                        array(
+                            '&',
+                            ';'
+                        ),
+                        $string
+                    );
+                    return $string;
+                }
+            }
+        // no break
+        case 'htmlall':
+            if (Smarty::$_MBSTRING) {
+                // mb_convert_encoding ignores htmlspecialchars()
+                if ($_double_encode) {
+                    // php >=5.3.2 - go native
+                    $string = htmlspecialchars($string, ENT_QUOTES, $char_set, $double_encode);
+                } else {
+                    if ($double_encode) {
+                        // php <5.2.3 - only handle double encoding
+                        $string = htmlspecialchars($string, ENT_QUOTES, $char_set);
+                    } else {
+                        // php <5.2.3 - prevent double encoding
+                        $string = preg_replace('!&(#?\w+);!', '%%%SMARTY_START%%%\\1%%%SMARTY_END%%%', $string);
+                        $string = htmlspecialchars($string, ENT_QUOTES, $char_set);
+                        $string =
+                            str_replace(
+                                array(
+                                    '%%%SMARTY_START%%%',
+                                    '%%%SMARTY_END%%%'
+                                ),
+                                array(
+                                    '&',
+                                    ';'
+                                ),
+                                $string
+                            );
+                        return $string;
+                    }
+                }
+                // htmlentities() won't convert everything, so use mb_convert_encoding
+                return mb_convert_encoding($string, 'HTML-ENTITIES', $char_set);
+            }
+            // no MBString fallback
+            if ($_double_encode) {
+                return htmlentities($string, ENT_QUOTES, $char_set, $double_encode);
+            } else {
+                if ($double_encode) {
+                    return htmlentities($string, ENT_QUOTES, $char_set);
+                } else {
+                    $string = preg_replace('!&(#?\w+);!', '%%%SMARTY_START%%%\\1%%%SMARTY_END%%%', $string);
+                    $string = htmlentities($string, ENT_QUOTES, $char_set);
+                    $string = str_replace(
+                        array(
+                            '%%%SMARTY_START%%%',
+                            '%%%SMARTY_END%%%'
+                        ),
+                        array(
+                            '&',
+                            ';'
+                        ),
+                        $string
+                    );
+                    return $string;
+                }
+            }
+        // no break
+        case 'url':
+            return rawurlencode($string);
+        case 'urlpathinfo':
+            return str_replace('%2F', '/', rawurlencode($string));
+        case 'quotes':
+            // escape unescaped single quotes
+            return preg_replace("%(?<!\\\\)'%", "\\'", $string);
+        case 'hex':
+            // escape every byte into hex
+            // Note that the UTF-8 encoded character ä will be represented as %c3%a4
+            $return = '';
+            $_length = strlen($string);
+            for ($x = 0; $x < $_length; $x++) {
+                $return .= '%' . bin2hex($string[ $x ]);
+            }
+            return $return;
+        case 'hexentity':
+            $return = '';
+            if (Smarty::$_MBSTRING) {
+                if (!$is_loaded_1) {
+                    if (!is_callable('smarty_mb_to_unicode')) {
+                        include_once SMARTY_PLUGINS_DIR . 'shared.mb_unicode.php';
+                    }
+                    $is_loaded_1 = true;
+                }
+                $return = '';
+                foreach (smarty_mb_to_unicode($string, Smarty::$_CHARSET) as $unicode) {
+                    $return .= '&#x' . strtoupper(dechex($unicode)) . ';';
+                }
+                return $return;
+            }
+            // no MBString fallback
+            $_length = strlen($string);
+            for ($x = 0; $x < $_length; $x++) {
+                $return .= '&#x' . bin2hex($string[ $x ]) . ';';
+            }
+            return $return;
+        case 'decentity':
+            $return = '';
+            if (Smarty::$_MBSTRING) {
+                if (!$is_loaded_1) {
+                    if (!is_callable('smarty_mb_to_unicode')) {
+                        include_once SMARTY_PLUGINS_DIR . 'shared.mb_unicode.php';
+                    }
+                    $is_loaded_1 = true;
+                }
+                $return = '';
+                foreach (smarty_mb_to_unicode($string, Smarty::$_CHARSET) as $unicode) {
+                    $return .= '&#' . $unicode . ';';
+                }
+                return $return;
+            }
+            // no MBString fallback
+            $_length = strlen($string);
+            for ($x = 0; $x < $_length; $x++) {
+                $return .= '&#' . ord($string[ $x ]) . ';';
+            }
+            return $return;
+        case 'javascript':
+            // escape quotes and backslashes, newlines, etc.
+            return strtr(
+                $string,
+                array(
+                    '\\' => '\\\\',
+                    "'"  => "\\'",
+                    '"'  => '\\"',
+                    "\r" => '\\r',
+                    "\n" => '\\n',
+                    '</' => '<\/',
+                    // see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
+                    '<!--' => '<\!--',
+                    '<s'   => '<\s',
+                    '<S'   => '<\S'
+                )
+            );
+        case 'mail':
+            if (Smarty::$_MBSTRING) {
+                if (!$is_loaded_2) {
+                    if (!is_callable('smarty_mb_str_replace')) {
+                        include_once SMARTY_PLUGINS_DIR . 'shared.mb_str_replace.php';
+                    }
+                    $is_loaded_2 = true;
+                }
+                return smarty_mb_str_replace(
+                    array(
+                        '@',
+                        '.'
+                    ),
+                    array(
+                        ' [AT] ',
+                        ' [DOT] '
+                    ),
+                    $string
+                );
+            }
+            // no MBString fallback
+            return str_replace(
+                array(
+                    '@',
+                    '.'
+                ),
+                array(
+                    ' [AT] ',
+                    ' [DOT] '
+                ),
+                $string
+            );
+        case 'nonstd':
+            // escape non-standard chars, such as ms document quotes
+            $return = '';
+            if (Smarty::$_MBSTRING) {
+                if (!$is_loaded_1) {
+                    if (!is_callable('smarty_mb_to_unicode')) {
+                        include_once SMARTY_PLUGINS_DIR . 'shared.mb_unicode.php';
+                    }
+                    $is_loaded_1 = true;
+                }
+                foreach (smarty_mb_to_unicode($string, Smarty::$_CHARSET) as $unicode) {
+                    if ($unicode >= 126) {
+                        $return .= '&#' . $unicode . ';';
+                    } else {
+                        $return .= chr($unicode);
+                    }
+                }
+                return $return;
+            }
+            $_length = strlen($string);
+            for ($_i = 0; $_i < $_length; $_i++) {
+                $_ord = ord(substr($string, $_i, 1));
+                // non-standard char, escape it
+                if ($_ord >= 126) {
+                    $return .= '&#' . $_ord . ';';
+                } else {
+                    $return .= substr($string, $_i, 1);
+                }
+            }
+            return $return;
+        default:
+            trigger_error("escape: unsupported type: $esc_type - returning unmodified string", E_USER_NOTICE);
+            return $string;
+    }
+}

+ 71 - 0
third_party/smarty/plugins/modifier.mb_wordwrap.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty wordwrap modifier plugin
+ * Type:     modifier
+ * Name:     mb_wordwrap
+ * Purpose:  Wrap a string to a given number of characters
+ *
+ * @link   https://php.net/manual/en/function.wordwrap.php for similarity
+ *
+ * @param string  $str   the string to wrap
+ * @param int     $width the width of the output
+ * @param string  $break the character used to break the line
+ * @param boolean $cut   ignored parameter, just for the sake of
+ *
+ * @return string  wrapped string
+ * @author Rodney Rehm
+ */
+function smarty_modifier_mb_wordwrap($str, $width = 75, $break = "\n", $cut = false)
+{
+    // break words into tokens using white space as a delimiter
+    $tokens = preg_split('!(\s)!S' . Smarty::$_UTF8_MODIFIER, $str, -1, PREG_SPLIT_NO_EMPTY + PREG_SPLIT_DELIM_CAPTURE);
+    $length = 0;
+    $t = '';
+    $_previous = false;
+    $_space = false;
+    foreach ($tokens as $_token) {
+        $token_length = mb_strlen($_token, Smarty::$_CHARSET);
+        $_tokens = array($_token);
+        if ($token_length > $width) {
+            if ($cut) {
+                $_tokens = preg_split(
+                    '!(.{' . $width . '})!S' . Smarty::$_UTF8_MODIFIER,
+                    $_token,
+                    -1,
+                    PREG_SPLIT_NO_EMPTY + PREG_SPLIT_DELIM_CAPTURE
+                );
+            }
+        }
+        foreach ($_tokens as $token) {
+            $_space = !!preg_match('!^\s$!S' . Smarty::$_UTF8_MODIFIER, $token);
+            $token_length = mb_strlen($token, Smarty::$_CHARSET);
+            $length += $token_length;
+            if ($length > $width) {
+                // remove space before inserted break
+                if ($_previous) {
+                    $t = mb_substr($t, 0, -1, Smarty::$_CHARSET);
+                }
+                if (!$_space) {
+                    // add the break before the token
+                    if (!empty($t)) {
+                        $t .= $break;
+                    }
+                    $length = $token_length;
+                }
+            } elseif ($token === "\n") {
+                // hard break must reset counters
+                $length = 0;
+            }
+            $_previous = $_space;
+            // add the token
+            $t .= $token;
+        }
+    }
+    return $t;
+}

+ 55 - 0
third_party/smarty/plugins/modifier.regex_replace.php

@@ -0,0 +1,55 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty regex_replace modifier plugin
+ * Type:     modifier
+ * Name:     regex_replace
+ * Purpose:  regular expression search/replace
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.regex.replace.php
+ *          regex_replace (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param string       $string  input string
+ * @param string|array $search  regular expression(s) to search for
+ * @param string|array $replace string(s) that should be replaced
+ * @param int          $limit   the maximum number of replacements
+ *
+ * @return string
+ */
+function smarty_modifier_regex_replace($string, $search, $replace, $limit = -1)
+{
+    if (is_array($search)) {
+        foreach ($search as $idx => $s) {
+            $search[ $idx ] = _smarty_regex_replace_check($s);
+        }
+    } else {
+        $search = _smarty_regex_replace_check($search);
+    }
+    return preg_replace($search, $replace, $string, $limit);
+}
+
+/**
+ * @param  string $search string(s) that should be replaced
+ *
+ * @return string
+ * @ignore
+ */
+function _smarty_regex_replace_check($search)
+{
+    // null-byte injection detection
+    // anything behind the first null-byte is ignored
+    if (($pos = strpos($search, "\0")) !== false) {
+        $search = substr($search, 0, $pos);
+    }
+    // remove eval-modifier from $search
+    if (preg_match('!([a-zA-Z\s]+)$!s', $search, $match) && (strpos($match[ 1 ], 'e') !== false)) {
+        $search = substr($search, 0, -strlen($match[ 1 ])) . preg_replace('![e\s]+!', '', $match[ 1 ]);
+    }
+    return $search;
+}

+ 37 - 0
third_party/smarty/plugins/modifier.replace.php

@@ -0,0 +1,37 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty replace modifier plugin
+ * Type:     modifier
+ * Name:     replace
+ * Purpose:  simple search/replace
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.replace.php replace (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ * @author Uwe Tews
+ *
+ * @param string $string  input string
+ * @param string $search  text to search for
+ * @param string $replace replacement text
+ *
+ * @return string
+ */
+function smarty_modifier_replace($string, $search, $replace)
+{
+    static $is_loaded = false;
+    if (Smarty::$_MBSTRING) {
+        if (!$is_loaded) {
+            if (!is_callable('smarty_mb_str_replace')) {
+                include_once SMARTY_PLUGINS_DIR . 'shared.mb_str_replace.php';
+            }
+            $is_loaded = true;
+        }
+        return smarty_mb_str_replace($search, $replace, $string);
+    }
+    return str_replace($search, $replace, $string);
+}

+ 26 - 0
third_party/smarty/plugins/modifier.spacify.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty spacify modifier plugin
+ * Type:     modifier
+ * Name:     spacify
+ * Purpose:  add spaces between characters in a string
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.spacify.php spacify (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param string $string       input string
+ * @param string $spacify_char string to insert between characters.
+ *
+ * @return string
+ */
+function smarty_modifier_spacify($string, $spacify_char = ' ')
+{
+    // well… what about charsets besides latin and UTF-8?
+    return implode($spacify_char, preg_split('//' . Smarty::$_UTF8_MODIFIER, $string, -1, PREG_SPLIT_NO_EMPTY));
+}

+ 62 - 0
third_party/smarty/plugins/modifier.truncate.php

@@ -0,0 +1,62 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifier
+ */
+/**
+ * Smarty truncate modifier plugin
+ * Type:     modifier
+ * Name:     truncate
+ * Purpose:  Truncate a string to a certain length if necessary,
+ *               optionally splitting in the middle of a word, and
+ *               appending the $etc string or inserting $etc into the middle.
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.truncate.php truncate (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param string  $string      input string
+ * @param integer $length      length of truncated text
+ * @param string  $etc         end string
+ * @param boolean $break_words truncate at word boundary
+ * @param boolean $middle      truncate in the middle of text
+ *
+ * @return string truncated string
+ */
+function smarty_modifier_truncate($string, $length = 80, $etc = '...', $break_words = false, $middle = false)
+{
+    if ($length === 0) {
+        return '';
+    }
+    if (Smarty::$_MBSTRING) {
+        if (mb_strlen($string, Smarty::$_CHARSET) > $length) {
+            $length -= min($length, mb_strlen($etc, Smarty::$_CHARSET));
+            if (!$break_words && !$middle) {
+                $string = preg_replace(
+                    '/\s+?(\S+)?$/' . Smarty::$_UTF8_MODIFIER,
+                    '',
+                    mb_substr($string, 0, $length + 1, Smarty::$_CHARSET)
+                );
+            }
+            if (!$middle) {
+                return mb_substr($string, 0, $length, Smarty::$_CHARSET) . $etc;
+            }
+            return mb_substr($string, 0, $length / 2, Smarty::$_CHARSET) . $etc .
+                   mb_substr($string, -$length / 2, $length, Smarty::$_CHARSET);
+        }
+        return $string;
+    }
+    // no MBString fallback
+    if (isset($string[ $length ])) {
+        $length -= min($length, strlen($etc));
+        if (!$break_words && !$middle) {
+            $string = preg_replace('/\s+?(\S+)?$/', '', substr($string, 0, $length + 1));
+        }
+        if (!$middle) {
+            return substr($string, 0, $length) . $etc;
+        }
+        return substr($string, 0, $length / 2) . $etc . substr($string, -$length / 2);
+    }
+    return $string;
+}

+ 28 - 0
third_party/smarty/plugins/modifiercompiler.cat.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty cat modifier plugin
+ * Type:     modifier
+ * Name:     cat
+ * Date:     Feb 24, 2003
+ * Purpose:  catenate a value to a variable
+ * Input:    string to catenate
+ * Example:  {$var|cat:"foo"}
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.cat.php cat
+ *           (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_cat($params)
+{
+    return '(' . implode(').(', $params) . ')';
+}

+ 32 - 0
third_party/smarty/plugins/modifiercompiler.count_characters.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty count_characters modifier plugin
+ * Type:     modifier
+ * Name:     count_characters
+ * Purpose:  count the number of characters in a text
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.count.characters.php count_characters (Smarty online
+ *         manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_count_characters($params)
+{
+    if (!isset($params[ 1 ]) || $params[ 1 ] !== 'true') {
+        return 'preg_match_all(\'/[^\s]/' . Smarty::$_UTF8_MODIFIER . '\',' . $params[ 0 ] . ', $tmp)';
+    }
+    if (Smarty::$_MBSTRING) {
+        return 'mb_strlen(' . $params[ 0 ] . ', \'' . addslashes(Smarty::$_CHARSET) . '\')';
+    }
+    // no MBString fallback
+    return 'strlen(' . $params[ 0 ] . ')';
+}

+ 26 - 0
third_party/smarty/plugins/modifiercompiler.count_paragraphs.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty count_paragraphs modifier plugin
+ * Type:     modifier
+ * Name:     count_paragraphs
+ * Purpose:  count the number of paragraphs in a text
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.count.paragraphs.php
+ *          count_paragraphs (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_count_paragraphs($params)
+{
+    // count \r or \n characters
+    return '(preg_match_all(\'#[\r\n]+#\', ' . $params[ 0 ] . ', $tmp)+1)';
+}

+ 26 - 0
third_party/smarty/plugins/modifiercompiler.count_sentences.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty count_sentences modifier plugin
+ * Type:     modifier
+ * Name:     count_sentences
+ * Purpose:  count the number of sentences in a text
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.count.paragraphs.php
+ *          count_sentences (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_count_sentences($params)
+{
+    // find periods, question marks, exclamation marks with a word before but not after.
+    return 'preg_match_all("#\w[\.\?\!](\W|$)#S' . Smarty::$_UTF8_MODIFIER . '", ' . $params[ 0 ] . ', $tmp)';
+}

+ 31 - 0
third_party/smarty/plugins/modifiercompiler.count_words.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty count_words modifier plugin
+ * Type:     modifier
+ * Name:     count_words
+ * Purpose:  count the number of words in a text
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.count.words.php count_words (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_count_words($params)
+{
+    if (Smarty::$_MBSTRING) {
+        // return 'preg_match_all(\'#[\w\pL]+#' . Smarty::$_UTF8_MODIFIER . '\', ' . $params[0] . ', $tmp)';
+        // expression taken from http://de.php.net/manual/en/function.str-word-count.php#85592
+        return 'preg_match_all(\'/\p{L}[\p{L}\p{Mn}\p{Pd}\\\'\x{2019}]*/' . Smarty::$_UTF8_MODIFIER . '\', ' .
+               $params[ 0 ] . ', $tmp)';
+    }
+    // no MBString fallback
+    return 'str_word_count(' . $params[ 0 ] . ')';
+}

+ 32 - 0
third_party/smarty/plugins/modifiercompiler.default.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty default modifier plugin
+ * Type:     modifier
+ * Name:     default
+ * Purpose:  designate default value for empty variables
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.default.php default (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_default($params)
+{
+    $output = $params[ 0 ];
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = "''";
+    }
+    array_shift($params);
+    foreach ($params as $param) {
+        $output = '(($tmp = ' . $output . ' ?? null)===null||$tmp===\'\' ? ' . $param . ' ?? null : $tmp)';
+    }
+    return $output;
+}

+ 110 - 0
third_party/smarty/plugins/modifiercompiler.escape.php

@@ -0,0 +1,110 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty escape modifier plugin
+ * Type:     modifier
+ * Name:     escape
+ * Purpose:  escape string for output
+ *
+ * @link   https://www.smarty.net/docsv2/en/language.modifier.escape count_characters (Smarty online manual)
+ * @author Rodney Rehm
+ *
+ * @param array                                $params parameters
+ * @param Smarty_Internal_TemplateCompilerBase $compiler
+ *
+ * @return string with compiled code
+ * @throws \SmartyException
+ */
+function smarty_modifiercompiler_escape($params, Smarty_Internal_TemplateCompilerBase $compiler)
+{
+    static $_double_encode = true;
+    static $is_loaded = false;
+    $compiler->template->_checkPlugins(
+        array(
+            array(
+                'function' => 'smarty_literal_compiler_param',
+                'file'     => SMARTY_PLUGINS_DIR . 'shared.literal_compiler_param.php'
+            )
+        )
+    );
+    try {
+        $esc_type = smarty_literal_compiler_param($params, 1, 'html');
+        $char_set = smarty_literal_compiler_param($params, 2, Smarty::$_CHARSET);
+        $double_encode = smarty_literal_compiler_param($params, 3, true);
+        if (!$char_set) {
+            $char_set = Smarty::$_CHARSET;
+        }
+        switch ($esc_type) {
+            case 'html':
+                if ($_double_encode) {
+                    return 'htmlspecialchars(' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ', ' .
+                           var_export($double_encode, true) . ')';
+                } elseif ($double_encode) {
+                    return 'htmlspecialchars(' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ')';
+                } else {
+                    // fall back to modifier.escape.php
+                }
+            // no break
+            case 'htmlall':
+                if (Smarty::$_MBSTRING) {
+                    if ($_double_encode) {
+                        // php >=5.2.3 - go native
+                        return 'mb_convert_encoding(htmlspecialchars(' . $params[ 0 ] . ', ENT_QUOTES, ' .
+                               var_export($char_set, true) . ', ' . var_export($double_encode, true) .
+                               '), "HTML-ENTITIES", ' . var_export($char_set, true) . ')';
+                    } elseif ($double_encode) {
+                        // php <5.2.3 - only handle double encoding
+                        return 'mb_convert_encoding(htmlspecialchars(' . $params[ 0 ] . ', ENT_QUOTES, ' .
+                               var_export($char_set, true) . '), "HTML-ENTITIES", ' . var_export($char_set, true) . ')';
+                    } else {
+                        // fall back to modifier.escape.php
+                    }
+                }
+                // no MBString fallback
+                if ($_double_encode) {
+                    // php >=5.2.3 - go native
+                    return 'htmlentities(' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ', ' .
+                           var_export($double_encode, true) . ')';
+                } elseif ($double_encode) {
+                    // php <5.2.3 - only handle double encoding
+                    return 'htmlentities(' . $params[ 0 ] . ', ENT_QUOTES, ' . var_export($char_set, true) . ')';
+                } else {
+                    // fall back to modifier.escape.php
+                }
+            // no break
+            case 'url':
+                return 'rawurlencode(' . $params[ 0 ] . ')';
+            case 'urlpathinfo':
+                return 'str_replace("%2F", "/", rawurlencode(' . $params[ 0 ] . '))';
+            case 'quotes':
+                // escape unescaped single quotes
+                return 'preg_replace("%(?<!\\\\\\\\)\'%", "\\\'",' . $params[ 0 ] . ')';
+            case 'javascript':
+                // escape quotes and backslashes, newlines, etc.
+                // see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
+                return 'strtr(' .
+                       $params[ 0 ] .
+                       ', array("\\\\" => "\\\\\\\\", "\'" => "\\\\\'", "\"" => "\\\\\"", "\\r" => "\\\\r", "\\n" => "\\\n", "</" => "<\/", "<!--" => "<\!--", "<s" => "<\s", "<S" => "<\S" ))';
+        }
+    } catch (SmartyException $e) {
+        // pass through to regular plugin fallback
+    }
+    // could not optimize |escape call, so fallback to regular plugin
+    if ($compiler->template->caching && ($compiler->tag_nocache | $compiler->nocache)) {
+        $compiler->required_plugins[ 'nocache' ][ 'escape' ][ 'modifier' ][ 'file' ] =
+            SMARTY_PLUGINS_DIR . 'modifier.escape.php';
+        $compiler->required_plugins[ 'nocache' ][ 'escape' ][ 'modifier' ][ 'function' ] =
+            'smarty_modifier_escape';
+    } else {
+        $compiler->required_plugins[ 'compiled' ][ 'escape' ][ 'modifier' ][ 'file' ] =
+            SMARTY_PLUGINS_DIR . 'modifier.escape.php';
+        $compiler->required_plugins[ 'compiled' ][ 'escape' ][ 'modifier' ][ 'function' ] =
+            'smarty_modifier_escape';
+    }
+    return 'smarty_modifier_escape(' . join(', ', $params) . ')';
+}

+ 30 - 0
third_party/smarty/plugins/modifiercompiler.from_charset.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty from_charset modifier plugin
+ * Type:     modifier
+ * Name:     from_charset
+ * Purpose:  convert character encoding from $charset to internal encoding
+ *
+ * @author Rodney Rehm
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_from_charset($params)
+{
+    if (!Smarty::$_MBSTRING) {
+        // FIXME: (rodneyrehm) shouldn't this throw an error?
+        return $params[ 0 ];
+    }
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = '"ISO-8859-1"';
+    }
+    return 'mb_convert_encoding(' . $params[ 0 ] . ', "' . addslashes(Smarty::$_CHARSET) . '", ' . $params[ 1 ] . ')';
+}

+ 30 - 0
third_party/smarty/plugins/modifiercompiler.indent.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty indent modifier plugin
+ * Type:     modifier
+ * Name:     indent
+ * Purpose:  indent lines of text
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.indent.php indent (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_indent($params)
+{
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = 4;
+    }
+    if (!isset($params[ 2 ])) {
+        $params[ 2 ] = "' '";
+    }
+    return 'preg_replace(\'!^!m\',str_repeat(' . $params[ 2 ] . ',' . $params[ 1 ] . '),' . $params[ 0 ] . ')';
+}

+ 29 - 0
third_party/smarty/plugins/modifiercompiler.lower.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty lower modifier plugin
+ * Type:     modifier
+ * Name:     lower
+ * Purpose:  convert string to lowercase
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.lower.php lower (Smarty online manual)
+ * @author Monte Ohrt <monte at ohrt dot com>
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_lower($params)
+{
+    if (Smarty::$_MBSTRING) {
+        return 'mb_strtolower(' . $params[ 0 ] . ', \'' . addslashes(Smarty::$_CHARSET) . '\')';
+    }
+    // no MBString fallback
+    return 'strtolower(' . $params[ 0 ] . ')';
+}

+ 20 - 0
third_party/smarty/plugins/modifiercompiler.noprint.php

@@ -0,0 +1,20 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty noprint modifier plugin
+ * Type:     modifier
+ * Name:     noprint
+ * Purpose:  return an empty string
+ *
+ * @author Uwe Tews
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_noprint()
+{
+    return "''";
+}

+ 24 - 0
third_party/smarty/plugins/modifiercompiler.string_format.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty string_format modifier plugin
+ * Type:     modifier
+ * Name:     string_format
+ * Purpose:  format strings via sprintf
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.string.format.php string_format (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_string_format($params)
+{
+    return 'sprintf(' . $params[ 1 ] . ',' . $params[ 0 ] . ')';
+}

+ 30 - 0
third_party/smarty/plugins/modifiercompiler.strip.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty strip modifier plugin
+ * Type:     modifier
+ * Name:     strip
+ * Purpose:  Replace all repeated spaces, newlines, tabs
+ *              with a single space or supplied replacement string.
+ * Example:  {$var|strip} {$var|strip:"&nbsp;"}
+ * Date:     September 25th, 2002
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.strip.php strip (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_strip($params)
+{
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = "' '";
+    }
+    return "preg_replace('!\s+!" . Smarty::$_UTF8_MODIFIER . "', {$params[1]},{$params[0]})";
+}

+ 28 - 0
third_party/smarty/plugins/modifiercompiler.strip_tags.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty strip_tags modifier plugin
+ * Type:     modifier
+ * Name:     strip_tags
+ * Purpose:  strip html tags from text
+ *
+ * @link   https://www.smarty.net/docs/en/language.modifier.strip.tags.tpl strip_tags (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_strip_tags($params)
+{
+    if (!isset($params[ 1 ]) || $params[ 1 ] === true || trim($params[ 1 ], '"') === 'true') {
+        return "preg_replace('!<[^>]*?>!', ' ', {$params[0]})";
+    } else {
+        return 'strip_tags(' . $params[ 0 ] . ')';
+    }
+}

+ 30 - 0
third_party/smarty/plugins/modifiercompiler.to_charset.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty to_charset modifier plugin
+ * Type:     modifier
+ * Name:     to_charset
+ * Purpose:  convert character encoding from internal encoding to $charset
+ *
+ * @author Rodney Rehm
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_to_charset($params)
+{
+    if (!Smarty::$_MBSTRING) {
+        // FIXME: (rodneyrehm) shouldn't this throw an error?
+        return $params[ 0 ];
+    }
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = '"ISO-8859-1"';
+    }
+    return 'mb_convert_encoding(' . $params[ 0 ] . ', ' . $params[ 1 ] . ', "' . addslashes(Smarty::$_CHARSET) . '")';
+}

+ 44 - 0
third_party/smarty/plugins/modifiercompiler.unescape.php

@@ -0,0 +1,44 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty unescape modifier plugin
+ * Type:     modifier
+ * Name:     unescape
+ * Purpose:  unescape html entities
+ *
+ * @author Rodney Rehm
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_unescape($params)
+{
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = 'html';
+    }
+    if (!isset($params[ 2 ])) {
+        $params[ 2 ] = '\'' . addslashes(Smarty::$_CHARSET) . '\'';
+    } else {
+        $params[ 2 ] = "'{$params[ 2 ]}'";
+    }
+    switch (trim($params[ 1 ], '"\'')) {
+        case 'entity':
+        case 'htmlall':
+            if (Smarty::$_MBSTRING) {
+                return 'mb_convert_encoding(' . $params[ 0 ] . ', ' . $params[ 2 ] . ', \'HTML-ENTITIES\')';
+            }
+            return 'html_entity_decode(' . $params[ 0 ] . ', ENT_NOQUOTES, ' . $params[ 2 ] . ')';
+        case 'html':
+            return 'htmlspecialchars_decode(' . $params[ 0 ] . ', ENT_QUOTES)';
+        case 'url':
+            return 'rawurldecode(' . $params[ 0 ] . ')';
+        default:
+            return $params[ 0 ];
+    }
+}

+ 28 - 0
third_party/smarty/plugins/modifiercompiler.upper.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty upper modifier plugin
+ * Type:     modifier
+ * Name:     lower
+ * Purpose:  convert string to uppercase
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.upper.php lower (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array $params parameters
+ *
+ * @return string with compiled code
+ */
+function smarty_modifiercompiler_upper($params)
+{
+    if (Smarty::$_MBSTRING) {
+        return 'mb_strtoupper(' . $params[ 0 ] . ', \'' . addslashes(Smarty::$_CHARSET) . '\')';
+    }
+    // no MBString fallback
+    return 'strtoupper(' . $params[ 0 ] . ')';
+}

+ 39 - 0
third_party/smarty/plugins/modifiercompiler.wordwrap.php

@@ -0,0 +1,39 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsModifierCompiler
+ */
+/**
+ * Smarty wordwrap modifier plugin
+ * Type:     modifier
+ * Name:     wordwrap
+ * Purpose:  wrap a string of text at a given length
+ *
+ * @link   https://www.smarty.net/manual/en/language.modifier.wordwrap.php wordwrap (Smarty online manual)
+ * @author Uwe Tews
+ *
+ * @param array                                 $params parameters
+ * @param \Smarty_Internal_TemplateCompilerBase $compiler
+ *
+ * @return string with compiled code
+ * @throws \SmartyException
+ */
+function smarty_modifiercompiler_wordwrap($params, Smarty_Internal_TemplateCompilerBase $compiler)
+{
+    if (!isset($params[ 1 ])) {
+        $params[ 1 ] = 80;
+    }
+    if (!isset($params[ 2 ])) {
+        $params[ 2 ] = '"\n"';
+    }
+    if (!isset($params[ 3 ])) {
+        $params[ 3 ] = 'false';
+    }
+    $function = 'wordwrap';
+    if (Smarty::$_MBSTRING) {
+        $function = $compiler->getPlugin('mb_wordwrap', 'modifier');
+    }
+    return $function . '(' . $params[ 0 ] . ',' . $params[ 1 ] . ',' . $params[ 2 ] . ',' . $params[ 3 ] . ')';
+}

+ 89 - 0
third_party/smarty/plugins/outputfilter.trimwhitespace.php

@@ -0,0 +1,89 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFilter
+ */
+/**
+ * Smarty trimwhitespace outputfilter plugin
+ * Trim unnecessary whitespace from HTML markup.
+ *
+ * @author Rodney Rehm
+ *
+ * @param string $source input string
+ *
+ * @return string filtered output
+ * @todo   substr_replace() is not overloaded by mbstring.func_overload - so this function might fail!
+ */
+function smarty_outputfilter_trimwhitespace($source)
+{
+    $store = array();
+    $_store = 0;
+    $_offset = 0;
+    // Unify Line-Breaks to \n
+    $source = preg_replace('/\015\012|\015|\012/', "\n", $source);
+    // capture Internet Explorer and KnockoutJS Conditional Comments
+    if (preg_match_all(
+        '#<!--((\[[^\]]+\]>.*?<!\[[^\]]+\])|(\s*/?ko\s+.+))-->#is',
+        $source,
+        $matches,
+        PREG_OFFSET_CAPTURE | PREG_SET_ORDER
+    )
+    ) {
+        foreach ($matches as $match) {
+            $store[] = $match[ 0 ][ 0 ];
+            $_length = strlen($match[ 0 ][ 0 ]);
+            $replace = '@!@SMARTY:' . $_store . ':SMARTY@!@';
+            $source = substr_replace($source, $replace, $match[ 0 ][ 1 ] - $_offset, $_length);
+            $_offset += $_length - strlen($replace);
+            $_store++;
+        }
+    }
+    // Strip all HTML-Comments
+    // yes, even the ones in <script> - see https://stackoverflow.com/a/808850/515124
+    $source = preg_replace('#<!--.*?-->#ms', '', $source);
+    // capture html elements not to be messed with
+    $_offset = 0;
+    if (preg_match_all(
+        '#(<script[^>]*>.*?</script[^>]*>)|(<textarea[^>]*>.*?</textarea[^>]*>)|(<pre[^>]*>.*?</pre[^>]*>)#is',
+        $source,
+        $matches,
+        PREG_OFFSET_CAPTURE | PREG_SET_ORDER
+    )
+    ) {
+        foreach ($matches as $match) {
+            $store[] = $match[ 0 ][ 0 ];
+            $_length = strlen($match[ 0 ][ 0 ]);
+            $replace = '@!@SMARTY:' . $_store . ':SMARTY@!@';
+            $source = substr_replace($source, $replace, $match[ 0 ][ 1 ] - $_offset, $_length);
+            $_offset += $_length - strlen($replace);
+            $_store++;
+        }
+    }
+    $expressions = array(// replace multiple spaces between tags by a single space
+                         // can't remove them entirely, becaue that might break poorly implemented CSS display:inline-block elements
+                         '#(:SMARTY@!@|>)\s+(?=@!@SMARTY:|<)#s'                                    => '\1 \2',
+                         // remove spaces between attributes (but not in attribute values!)
+                         '#(([a-z0-9]\s*=\s*("[^"]*?")|(\'[^\']*?\'))|<[a-z0-9_]+)\s+([a-z/>])#is' => '\1 \5',
+                         // note: for some very weird reason trim() seems to remove spaces inside attributes.
+                         // maybe a \0 byte or something is interfering?
+                         '#^\s+<#Ss'                                                               => '<',
+                         '#>\s+$#Ss'                                                               => '>',
+    );
+    $source = preg_replace(array_keys($expressions), array_values($expressions), $source);
+    // note: for some very weird reason trim() seems to remove spaces inside attributes.
+    // maybe a \0 byte or something is interfering?
+    // $source = trim( $source );
+    $_offset = 0;
+    if (preg_match_all('#@!@SMARTY:([0-9]+):SMARTY@!@#is', $source, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
+        foreach ($matches as $match) {
+            $_length = strlen($match[ 0 ][ 0 ]);
+            $replace = $store[ $match[ 1 ][ 0 ] ];
+            $source = substr_replace($source, $replace, $match[ 0 ][ 1 ] + $_offset, $_length);
+            $_offset += strlen($replace) - $_length;
+            $_store++;
+        }
+    }
+    return $source;
+}

+ 26 - 0
third_party/smarty/plugins/shared.escape_special_chars.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Smarty shared plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsShared
+ */
+/**
+ * escape_special_chars common function
+ * Function: smarty_function_escape_special_chars
+ * Purpose:  used by other smarty functions to escape
+ *           special chars except for already escaped ones
+ *
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param string $string text that should by escaped
+ *
+ * @return string
+ */
+function smarty_function_escape_special_chars($string)
+{
+    if (!is_array($string)) {
+        $string = htmlspecialchars($string, ENT_COMPAT, Smarty::$_CHARSET, false);
+    }
+    return $string;
+}

+ 35 - 0
third_party/smarty/plugins/shared.literal_compiler_param.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsShared
+ */
+/**
+ * evaluate compiler parameter
+ *
+ * @param array   $params  parameter array as given to the compiler function
+ * @param integer $index   array index of the parameter to convert
+ * @param mixed   $default value to be returned if the parameter is not present
+ *
+ * @return mixed evaluated value of parameter or $default
+ * @throws SmartyException if parameter is not a literal (but an expression, variable, …)
+ * @author Rodney Rehm
+ */
+function smarty_literal_compiler_param($params, $index, $default = null)
+{
+    // not set, go default
+    if (!isset($params[ $index ])) {
+        return $default;
+    }
+    // test if param is a literal
+    if (!preg_match('/^([\'"]?)[a-zA-Z0-9-]+(\\1)$/', $params[ $index ])) {
+        throw new SmartyException(
+            '$param[' . $index .
+            '] is not a literal and is thus not evaluatable at compile time'
+        );
+    }
+    $t = null;
+    eval("\$t = " . $params[ $index ] . ";");
+    return $t;
+}

+ 49 - 0
third_party/smarty/plugins/shared.make_timestamp.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * Smarty shared plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsShared
+ */
+/**
+ * Function: smarty_make_timestamp
+ * Purpose:  used by other smarty functions to make a timestamp from a string.
+ *
+ * @author Monte Ohrt <monte at ohrt dot com>
+ *
+ * @param DateTime|int|string $string date object, timestamp or string that can be converted using strtotime()
+ *
+ * @return int
+ */
+function smarty_make_timestamp($string)
+{
+    if (empty($string)) {
+        // use "now":
+        return time();
+    } elseif ($string instanceof DateTime
+              || (interface_exists('DateTimeInterface', false) && $string instanceof DateTimeInterface)
+    ) {
+        return (int)$string->format('U'); // PHP 5.2 BC
+    } elseif (strlen($string) === 14 && ctype_digit($string)) {
+        // it is mysql timestamp format of YYYYMMDDHHMMSS?
+        return mktime(
+            substr($string, 8, 2),
+            substr($string, 10, 2),
+            substr($string, 12, 2),
+            substr($string, 4, 2),
+            substr($string, 6, 2),
+            substr($string, 0, 4)
+        );
+    } elseif (is_numeric($string)) {
+        // it is a numeric string, we handle it as timestamp
+        return (int)$string;
+    } else {
+        // strtotime should handle it
+        $time = strtotime($string);
+        if ($time === -1 || $time === false) {
+            // strtotime() was not able to parse $string, use "now":
+            return time();
+        }
+        return $time;
+    }
+}

+ 53 - 0
third_party/smarty/plugins/shared.mb_str_replace.php

@@ -0,0 +1,53 @@
+<?php
+/**
+ * Smarty shared plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsShared
+ */
+if (!function_exists('smarty_mb_str_replace')) {
+    /**
+     * Multibyte string replace
+     *
+     * @param string|string[] $search  the string to be searched
+     * @param string|string[] $replace the replacement string
+     * @param string          $subject the source string
+     * @param int             &$count  number of matches found
+     *
+     * @return string replaced string
+     * @author Rodney Rehm
+     */
+    function smarty_mb_str_replace($search, $replace, $subject, &$count = 0)
+    {
+        if (!is_array($search) && is_array($replace)) {
+            return false;
+        }
+        if (is_array($subject)) {
+            // call mb_replace for each single string in $subject
+            foreach ($subject as &$string) {
+                $string = smarty_mb_str_replace($search, $replace, $string, $c);
+                $count += $c;
+            }
+        } elseif (is_array($search)) {
+            if (!is_array($replace)) {
+                foreach ($search as &$string) {
+                    $subject = smarty_mb_str_replace($string, $replace, $subject, $c);
+                    $count += $c;
+                }
+            } else {
+                $n = max(count($search), count($replace));
+                while ($n--) {
+                    $subject = smarty_mb_str_replace(current($search), current($replace), $subject, $c);
+                    $count += $c;
+                    next($search);
+                    next($replace);
+                }
+            }
+        } else {
+            $parts = mb_split(preg_quote($search), $subject) ?: array();
+            $count = count($parts) - 1;
+            $subject = implode($replace, $parts);
+        }
+        return $subject;
+    }
+}

+ 51 - 0
third_party/smarty/plugins/shared.mb_unicode.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * Smarty shared plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsShared
+ */
+/**
+ * convert characters to their decimal unicode equivalents
+ *
+ * @link   http://www.ibm.com/developerworks/library/os-php-unicode/index.html#listing3 for inspiration
+ *
+ * @param string $string   characters to calculate unicode of
+ * @param string $encoding encoding of $string, if null mb_internal_encoding() is used
+ *
+ * @return array sequence of unicodes
+ * @author Rodney Rehm
+ */
+function smarty_mb_to_unicode($string, $encoding = null)
+{
+    if ($encoding) {
+        $expanded = mb_convert_encoding($string, 'UTF-32BE', $encoding);
+    } else {
+        $expanded = mb_convert_encoding($string, 'UTF-32BE');
+    }
+    return unpack('N*', $expanded);
+}
+
+/**
+ * convert unicodes to the character of given encoding
+ *
+ * @link   http://www.ibm.com/developerworks/library/os-php-unicode/index.html#listing3 for inspiration
+ *
+ * @param integer|array $unicode  single unicode or list of unicodes to convert
+ * @param string        $encoding encoding of returned string, if null mb_internal_encoding() is used
+ *
+ * @return string unicode as character sequence in given $encoding
+ * @author Rodney Rehm
+ */
+function smarty_mb_from_unicode($unicode, $encoding = null)
+{
+    $t = '';
+    if (!$encoding) {
+        $encoding = mb_internal_encoding();
+    }
+    foreach ((array)$unicode as $utf32be) {
+        $character = pack('N*', $utf32be);
+        $t .= mb_convert_encoding($character, $encoding, 'UTF-32BE');
+    }
+    return $t;
+}

+ 19 - 0
third_party/smarty/plugins/variablefilter.htmlspecialchars.php

@@ -0,0 +1,19 @@
+<?php
+/**
+ * Smarty plugin
+ *
+ * @package    Smarty
+ * @subpackage PluginsFilter
+ */
+/**
+ * Smarty htmlspecialchars variablefilter plugin
+ *
+ * @param string                    $source input string
+ * @param \Smarty_Internal_Template $template
+ *
+ * @return string filtered output
+ */
+function smarty_variablefilter_htmlspecialchars($source, Smarty_Internal_Template $template)
+{
+    return htmlspecialchars($source, ENT_QUOTES, Smarty::$_CHARSET);
+}

+ 219 - 0
third_party/smarty/sysplugins/smarty_cacheresource.php

@@ -0,0 +1,219 @@
+<?php
+/**
+ * Smarty Internal Plugin
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ */
+
+/**
+ * Cache Handler API
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ * @author     Rodney Rehm
+ */
+abstract class Smarty_CacheResource
+{
+    /**
+     * resource types provided by the core
+     *
+     * @var array
+     */
+    protected static $sysplugins = array('file' => 'smarty_internal_cacheresource_file.php',);
+
+    /**
+     * populate Cached Object with meta data from Resource
+     *
+     * @param \Smarty_Template_Cached  $cached    cached object
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return void
+     */
+    abstract public function populate(\Smarty_Template_Cached $cached, Smarty_Internal_Template $_template);
+
+    /**
+     * populate Cached Object with timestamp and exists from Resource
+     *
+     * @param Smarty_Template_Cached $cached
+     *
+     * @return void
+     */
+    abstract public function populateTimestamp(Smarty_Template_Cached $cached);
+
+    /**
+     * Read the cached template and process header
+     *
+     * @param Smarty_Internal_Template $_template template object
+     * @param Smarty_Template_Cached   $cached    cached object
+     * @param boolean                  $update    flag if called because cache update
+     *
+     * @return boolean true or false if the cached content does not exist
+     */
+    abstract public function process(
+        Smarty_Internal_Template $_template,
+        Smarty_Template_Cached $cached = null,
+        $update = false
+    );
+
+    /**
+     * Write the rendered template output to cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     * @param string                   $content   content to cache
+     *
+     * @return boolean success
+     */
+    abstract public function writeCachedContent(Smarty_Internal_Template $_template, $content);
+
+    /**
+     * Read cached template from cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return string  content
+     */
+    abstract public function readCachedContent(Smarty_Internal_Template $_template);
+
+    /**
+     * Return cached content
+     *
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return null|string
+     */
+    public function getCachedContent(Smarty_Internal_Template $_template)
+    {
+        if ($_template->cached->handler->process($_template)) {
+            ob_start();
+            $unifunc = $_template->cached->unifunc;
+            $unifunc($_template);
+            return ob_get_clean();
+        }
+        return null;
+    }
+
+    /**
+     * Empty cache
+     *
+     * @param Smarty  $smarty   Smarty object
+     * @param integer $exp_time expiration time (number of seconds, not timestamp)
+     *
+     * @return integer number of cache files deleted
+     */
+    abstract public function clearAll(Smarty $smarty, $exp_time = null);
+
+    /**
+     * Empty cache for a specific template
+     *
+     * @param Smarty  $smarty        Smarty object
+     * @param string  $resource_name template name
+     * @param string  $cache_id      cache id
+     * @param string  $compile_id    compile id
+     * @param integer $exp_time      expiration time (number of seconds, not timestamp)
+     *
+     * @return integer number of cache files deleted
+     */
+    abstract public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time);
+
+    /**
+     * @param Smarty                 $smarty
+     * @param Smarty_Template_Cached $cached
+     *
+     * @return bool|null
+     */
+    public function locked(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        // theoretically locking_timeout should be checked against time_limit (max_execution_time)
+        $start = microtime(true);
+        $hadLock = null;
+        while ($this->hasLock($smarty, $cached)) {
+            $hadLock = true;
+            if (microtime(true) - $start > $smarty->locking_timeout) {
+                // abort waiting for lock release
+                return false;
+            }
+            sleep(1);
+        }
+        return $hadLock;
+    }
+
+    /**
+     * Check is cache is locked for this template
+     *
+     * @param Smarty                 $smarty
+     * @param Smarty_Template_Cached $cached
+     *
+     * @return bool
+     */
+    public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        // check if lock exists
+        return false;
+    }
+
+    /**
+     * Lock cache for this template
+     *
+     * @param Smarty                 $smarty
+     * @param Smarty_Template_Cached $cached
+     *
+     * @return bool
+     */
+    public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        // create lock
+        return true;
+    }
+
+    /**
+     * Unlock cache for this template
+     *
+     * @param Smarty                 $smarty
+     * @param Smarty_Template_Cached $cached
+     *
+     * @return bool
+     */
+    public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        // release lock
+        return true;
+    }
+
+    /**
+     * Load Cache Resource Handler
+     *
+     * @param Smarty $smarty Smarty object
+     * @param string $type   name of the cache resource
+     *
+     * @throws SmartyException
+     * @return Smarty_CacheResource Cache Resource Handler
+     */
+    public static function load(Smarty $smarty, $type = null)
+    {
+        if (!isset($type)) {
+            $type = $smarty->caching_type;
+        }
+        // try smarty's cache
+        if (isset($smarty->_cache[ 'cacheresource_handlers' ][ $type ])) {
+            return $smarty->_cache[ 'cacheresource_handlers' ][ $type ];
+        }
+        // try registered resource
+        if (isset($smarty->registered_cache_resources[ $type ])) {
+            // do not cache these instances as they may vary from instance to instance
+            return $smarty->_cache[ 'cacheresource_handlers' ][ $type ] = $smarty->registered_cache_resources[ $type ];
+        }
+        // try sysplugins dir
+        if (isset(self::$sysplugins[ $type ])) {
+            $cache_resource_class = 'Smarty_Internal_CacheResource_' . ucfirst($type);
+            return $smarty->_cache[ 'cacheresource_handlers' ][ $type ] = new $cache_resource_class();
+        }
+        // try plugins dir
+        $cache_resource_class = 'Smarty_CacheResource_' . ucfirst($type);
+        if ($smarty->loadPlugin($cache_resource_class)) {
+            return $smarty->_cache[ 'cacheresource_handlers' ][ $type ] = new $cache_resource_class();
+        }
+        // give up
+        throw new SmartyException("Unable to load cache resource '{$type}'");
+    }
+}

+ 297 - 0
third_party/smarty/sysplugins/smarty_cacheresource_custom.php

@@ -0,0 +1,297 @@
+<?php
+/**
+ * Smarty Internal Plugin
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ */
+
+/**
+ * Cache Handler API
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ * @author     Rodney Rehm
+ */
+abstract class Smarty_CacheResource_Custom extends Smarty_CacheResource
+{
+    /**
+     * fetch cached content and its modification time from data source
+     *
+     * @param string  $id         unique cache content identifier
+     * @param string  $name       template name
+     * @param string  $cache_id   cache id
+     * @param string  $compile_id compile id
+     * @param string  $content    cached content
+     * @param integer $mtime      cache modification timestamp (epoch)
+     *
+     * @return void
+     */
+    abstract protected function fetch($id, $name, $cache_id, $compile_id, &$content, &$mtime);
+
+    /**
+     * Fetch cached content's modification timestamp from data source
+     * {@internal implementing this method is optional.
+     *  Only implement it if modification times can be accessed faster than loading the complete cached content.}}
+     *
+     * @param string $id         unique cache content identifier
+     * @param string $name       template name
+     * @param string $cache_id   cache id
+     * @param string $compile_id compile id
+     *
+     * @return integer|boolean timestamp (epoch) the template was modified, or false if not found
+     */
+    protected function fetchTimestamp($id, $name, $cache_id, $compile_id)
+    {
+        return false;
+    }
+
+    /**
+     * Save content to cache
+     *
+     * @param string       $id         unique cache content identifier
+     * @param string       $name       template name
+     * @param string       $cache_id   cache id
+     * @param string       $compile_id compile id
+     * @param integer|null $exp_time   seconds till expiration or null
+     * @param string       $content    content to cache
+     *
+     * @return boolean      success
+     */
+    abstract protected function save($id, $name, $cache_id, $compile_id, $exp_time, $content);
+
+    /**
+     * Delete content from cache
+     *
+     * @param string|null  $name       template name
+     * @param string|null  $cache_id   cache id
+     * @param string|null  $compile_id compile id
+     * @param integer|null $exp_time   seconds till expiration time in seconds or null
+     *
+     * @return integer      number of deleted caches
+     */
+    abstract protected function delete($name, $cache_id, $compile_id, $exp_time);
+
+    /**
+     * populate Cached Object with meta data from Resource
+     *
+     * @param Smarty_Template_Cached   $cached    cached object
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return void
+     */
+    public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template)
+    {
+        $_cache_id = isset($cached->cache_id) ? preg_replace('![^\w\|]+!', '_', $cached->cache_id) : null;
+        $_compile_id = isset($cached->compile_id) ? preg_replace('![^\w]+!', '_', $cached->compile_id) : null;
+        $path = $cached->source->uid . $_cache_id . $_compile_id;
+        $cached->filepath = sha1($path);
+        if ($_template->smarty->cache_locking) {
+            $cached->lock_id = sha1('lock.' . $path);
+        }
+        $this->populateTimestamp($cached);
+    }
+
+    /**
+     * populate Cached Object with timestamp and exists from Resource
+     *
+     * @param Smarty_Template_Cached $cached
+     *
+     * @return void
+     */
+    public function populateTimestamp(Smarty_Template_Cached $cached)
+    {
+        $mtime =
+            $this->fetchTimestamp($cached->filepath, $cached->source->name, $cached->cache_id, $cached->compile_id);
+        if ($mtime !== null) {
+            $cached->timestamp = $mtime;
+            $cached->exists = !!$cached->timestamp;
+            return;
+        }
+        $timestamp = null;
+        $this->fetch(
+            $cached->filepath,
+            $cached->source->name,
+            $cached->cache_id,
+            $cached->compile_id,
+            $cached->content,
+            $timestamp
+        );
+        $cached->timestamp = isset($timestamp) ? $timestamp : false;
+        $cached->exists = !!$cached->timestamp;
+    }
+
+    /**
+     * Read the cached template and process the header
+     *
+     * @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
+     * @param Smarty_Template_Cached    $cached      cached object
+     * @param boolean                   $update      flag if called because cache update
+     *
+     * @return boolean                 true or false if the cached content does not exist
+     */
+    public function process(
+        Smarty_Internal_Template $_smarty_tpl,
+        Smarty_Template_Cached $cached = null,
+        $update = false
+    ) {
+        if (!$cached) {
+            $cached = $_smarty_tpl->cached;
+        }
+        $content = $cached->content ? $cached->content : null;
+        $timestamp = $cached->timestamp ? $cached->timestamp : null;
+        if ($content === null || !$timestamp) {
+            $this->fetch(
+                $_smarty_tpl->cached->filepath,
+                $_smarty_tpl->source->name,
+                $_smarty_tpl->cache_id,
+                $_smarty_tpl->compile_id,
+                $content,
+                $timestamp
+            );
+        }
+        if (isset($content)) {
+            eval('?>' . $content);
+            $cached->content = null;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Write the rendered template output to cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     * @param string                   $content   content to cache
+     *
+     * @return boolean                  success
+     */
+    public function writeCachedContent(Smarty_Internal_Template $_template, $content)
+    {
+        return $this->save(
+            $_template->cached->filepath,
+            $_template->source->name,
+            $_template->cache_id,
+            $_template->compile_id,
+            $_template->cache_lifetime,
+            $content
+        );
+    }
+
+    /**
+     * Read cached template from cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return string|boolean  content
+     */
+    public function readCachedContent(Smarty_Internal_Template $_template)
+    {
+        $content = $_template->cached->content ? $_template->cached->content : null;
+        $timestamp = null;
+        if ($content === null) {
+            $timestamp = null;
+            $this->fetch(
+                $_template->cached->filepath,
+                $_template->source->name,
+                $_template->cache_id,
+                $_template->compile_id,
+                $content,
+                $timestamp
+            );
+        }
+        if (isset($content)) {
+            return $content;
+        }
+        return false;
+    }
+
+    /**
+     * Empty cache
+     *
+     * @param Smarty  $smarty   Smarty object
+     * @param integer $exp_time expiration time (number of seconds, not timestamp)
+     *
+     * @return integer number of cache files deleted
+     */
+    public function clearAll(Smarty $smarty, $exp_time = null)
+    {
+        return $this->delete(null, null, null, $exp_time);
+    }
+
+    /**
+     * Empty cache for a specific template
+     *
+     * @param Smarty  $smarty        Smarty object
+     * @param string  $resource_name template name
+     * @param string  $cache_id      cache id
+     * @param string  $compile_id    compile id
+     * @param integer $exp_time      expiration time (number of seconds, not timestamp)
+     *
+     * @return int number of cache files deleted
+     * @throws \SmartyException
+     */
+    public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time)
+    {
+        $cache_name = null;
+        if (isset($resource_name)) {
+            $source = Smarty_Template_Source::load(null, $smarty, $resource_name);
+            if ($source->exists) {
+                $cache_name = $source->name;
+            } else {
+                return 0;
+            }
+        }
+        return $this->delete($cache_name, $cache_id, $compile_id, $exp_time);
+    }
+
+    /**
+     * Check is cache is locked for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return boolean               true or false if cache is locked
+     */
+    public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $id = $cached->lock_id;
+        $name = $cached->source->name . '.lock';
+        $mtime = $this->fetchTimestamp($id, $name, $cached->cache_id, $cached->compile_id);
+        if ($mtime === null) {
+            $this->fetch($id, $name, $cached->cache_id, $cached->compile_id, $content, $mtime);
+        }
+        return $mtime && ($t = time()) - $mtime < $smarty->locking_timeout;
+    }
+
+    /**
+     * Lock cache for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return bool|void
+     */
+    public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $cached->is_locked = true;
+        $id = $cached->lock_id;
+        $name = $cached->source->name . '.lock';
+        $this->save($id, $name, $cached->cache_id, $cached->compile_id, $smarty->locking_timeout, '');
+    }
+
+    /**
+     * Unlock cache for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return bool|void
+     */
+    public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $cached->is_locked = false;
+        $name = $cached->source->name . '.lock';
+        $this->delete($name, $cached->cache_id, $cached->compile_id, null);
+    }
+}

+ 538 - 0
third_party/smarty/sysplugins/smarty_cacheresource_keyvaluestore.php

@@ -0,0 +1,538 @@
+<?php
+/**
+ * Smarty Internal Plugin
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ */
+
+/**
+ * Smarty Cache Handler Base for Key/Value Storage Implementations
+ * This class implements the functionality required to use simple key/value stores
+ * for hierarchical cache groups. key/value stores like memcache or APC do not support
+ * wildcards in keys, therefore a cache group cannot be cleared like "a|*" - which
+ * is no problem to filesystem and RDBMS implementations.
+ * This implementation is based on the concept of invalidation. While one specific cache
+ * can be identified and cleared, any range of caches cannot be identified. For this reason
+ * each level of the cache group hierarchy can have its own value in the store. These values
+ * are nothing but microtimes, telling us when a particular cache group was cleared for the
+ * last time. These keys are evaluated for every cache read to determine if the cache has
+ * been invalidated since it was created and should hence be treated as inexistent.
+ * Although deep hierarchies are possible, they are not recommended. Try to keep your
+ * cache groups as shallow as possible. Anything up 3-5 parents should be ok. So
+ * »a|b|c« is a good depth where »a|b|c|d|e|f|g|h|i|j|k« isn't. Try to join correlating
+ * cache groups: if your cache groups look somewhat like »a|b|$page|$items|$whatever«
+ * consider using »a|b|c|$page-$items-$whatever« instead.
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ * @author     Rodney Rehm
+ */
+abstract class Smarty_CacheResource_KeyValueStore extends Smarty_CacheResource
+{
+    /**
+     * cache for contents
+     *
+     * @var array
+     */
+    protected $contents = array();
+
+    /**
+     * cache for timestamps
+     *
+     * @var array
+     */
+    protected $timestamps = array();
+
+    /**
+     * populate Cached Object with meta data from Resource
+     *
+     * @param Smarty_Template_Cached   $cached    cached object
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return void
+     */
+    public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template)
+    {
+        $cached->filepath = $_template->source->uid . '#' . $this->sanitize($cached->source->resource) . '#' .
+                            $this->sanitize($cached->cache_id) . '#' . $this->sanitize($cached->compile_id);
+        $this->populateTimestamp($cached);
+    }
+
+    /**
+     * populate Cached Object with timestamp and exists from Resource
+     *
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return void
+     */
+    public function populateTimestamp(Smarty_Template_Cached $cached)
+    {
+        if (!$this->fetch(
+            $cached->filepath,
+            $cached->source->name,
+            $cached->cache_id,
+            $cached->compile_id,
+            $content,
+            $timestamp,
+            $cached->source->uid
+        )
+        ) {
+            return;
+        }
+        $cached->content = $content;
+        $cached->timestamp = (int)$timestamp;
+        $cached->exists = !!$cached->timestamp;
+    }
+
+    /**
+     * Read the cached template and process the header
+     *
+     * @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
+     * @param Smarty_Template_Cached    $cached      cached object
+     * @param boolean                   $update      flag if called because cache update
+     *
+     * @return boolean                 true or false if the cached content does not exist
+     */
+    public function process(
+        Smarty_Internal_Template $_smarty_tpl,
+        Smarty_Template_Cached $cached = null,
+        $update = false
+    ) {
+        if (!$cached) {
+            $cached = $_smarty_tpl->cached;
+        }
+        $content = $cached->content ? $cached->content : null;
+        $timestamp = $cached->timestamp ? $cached->timestamp : null;
+        if ($content === null || !$timestamp) {
+            if (!$this->fetch(
+                $_smarty_tpl->cached->filepath,
+                $_smarty_tpl->source->name,
+                $_smarty_tpl->cache_id,
+                $_smarty_tpl->compile_id,
+                $content,
+                $timestamp,
+                $_smarty_tpl->source->uid
+            )
+            ) {
+                return false;
+            }
+        }
+        if (isset($content)) {
+            eval('?>' . $content);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Write the rendered template output to cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     * @param string                   $content   content to cache
+     *
+     * @return boolean                  success
+     */
+    public function writeCachedContent(Smarty_Internal_Template $_template, $content)
+    {
+        $this->addMetaTimestamp($content);
+        return $this->write(array($_template->cached->filepath => $content), $_template->cache_lifetime);
+    }
+
+    /**
+     * Read cached template from cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return string|false  content
+     */
+    public function readCachedContent(Smarty_Internal_Template $_template)
+    {
+        $content = $_template->cached->content ? $_template->cached->content : null;
+        $timestamp = null;
+        if ($content === null) {
+            if (!$this->fetch(
+                $_template->cached->filepath,
+                $_template->source->name,
+                $_template->cache_id,
+                $_template->compile_id,
+                $content,
+                $timestamp,
+                $_template->source->uid
+            )
+            ) {
+                return false;
+            }
+        }
+        if (isset($content)) {
+            return $content;
+        }
+        return false;
+    }
+
+    /**
+     * Empty cache
+     * {@internal the $exp_time argument is ignored altogether }}
+     *
+     * @param Smarty  $smarty   Smarty object
+     * @param integer $exp_time expiration time [being ignored]
+     *
+     * @return integer number of cache files deleted [always -1]
+     * @uses   purge() to clear the whole store
+     * @uses   invalidate() to mark everything outdated if purge() is inapplicable
+     */
+    public function clearAll(Smarty $smarty, $exp_time = null)
+    {
+        if (!$this->purge()) {
+            $this->invalidate(null);
+        }
+        return -1;
+    }
+
+    /**
+     * Empty cache for a specific template
+     * {@internal the $exp_time argument is ignored altogether}}
+     *
+     * @param Smarty  $smarty        Smarty object
+     * @param string  $resource_name template name
+     * @param string  $cache_id      cache id
+     * @param string  $compile_id    compile id
+     * @param integer $exp_time      expiration time [being ignored]
+     *
+     * @return int number of cache files deleted [always -1]
+     * @throws \SmartyException
+     * @uses   buildCachedFilepath() to generate the CacheID
+     * @uses   invalidate() to mark CacheIDs parent chain as outdated
+     * @uses   delete() to remove CacheID from cache
+     */
+    public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time)
+    {
+        $uid = $this->getTemplateUid($smarty, $resource_name);
+        $cid = $uid . '#' . $this->sanitize($resource_name) . '#' . $this->sanitize($cache_id) . '#' .
+               $this->sanitize($compile_id);
+        $this->delete(array($cid));
+        $this->invalidate($cid, $resource_name, $cache_id, $compile_id, $uid);
+        return -1;
+    }
+
+    /**
+     * Get template's unique ID
+     *
+     * @param Smarty $smarty        Smarty object
+     * @param string $resource_name template name
+     *
+     * @return string filepath of cache file
+     * @throws \SmartyException
+     */
+    protected function getTemplateUid(Smarty $smarty, $resource_name)
+    {
+        if (isset($resource_name)) {
+            $source = Smarty_Template_Source::load(null, $smarty, $resource_name);
+            if ($source->exists) {
+                return $source->uid;
+            }
+        }
+        return '';
+    }
+
+    /**
+     * Sanitize CacheID components
+     *
+     * @param string $string CacheID component to sanitize
+     *
+     * @return string sanitized CacheID component
+     */
+    protected function sanitize($string)
+    {
+        $string = trim($string, '|');
+        if (!$string) {
+            return '';
+        }
+        return preg_replace('#[^\w\|]+#S', '_', $string);
+    }
+
+    /**
+     * Fetch and prepare a cache object.
+     *
+     * @param string  $cid           CacheID to fetch
+     * @param string  $resource_name template name
+     * @param string  $cache_id      cache id
+     * @param string  $compile_id    compile id
+     * @param string  $content       cached content
+     * @param integer &$timestamp    cached timestamp (epoch)
+     * @param string  $resource_uid  resource's uid
+     *
+     * @return boolean success
+     */
+    protected function fetch(
+        $cid,
+        $resource_name = null,
+        $cache_id = null,
+        $compile_id = null,
+        &$content = null,
+        &$timestamp = null,
+        $resource_uid = null
+    ) {
+        $t = $this->read(array($cid));
+        $content = !empty($t[ $cid ]) ? $t[ $cid ] : null;
+        $timestamp = null;
+        if ($content && ($timestamp = $this->getMetaTimestamp($content))) {
+            $invalidated =
+                $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid);
+            if ($invalidated > $timestamp) {
+                $timestamp = null;
+                $content = null;
+            }
+        }
+        return !!$content;
+    }
+
+    /**
+     * Add current microtime to the beginning of $cache_content
+     * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}}
+     *
+     * @param string &$content the content to be cached
+     */
+    protected function addMetaTimestamp(&$content)
+    {
+        $mt = explode(' ', microtime());
+        $ts = pack('NN', $mt[ 1 ], (int)($mt[ 0 ] * 100000000));
+        $content = $ts . $content;
+    }
+
+    /**
+     * Extract the timestamp the $content was cached
+     *
+     * @param string &$content the cached content
+     *
+     * @return float  the microtime the content was cached
+     */
+    protected function getMetaTimestamp(&$content)
+    {
+        extract(unpack('N1s/N1m/a*content', $content));
+        /**
+         * @var  int $s
+         * @var  int $m
+         */
+        return $s + ($m / 100000000);
+    }
+
+    /**
+     * Invalidate CacheID
+     *
+     * @param string $cid           CacheID
+     * @param string $resource_name template name
+     * @param string $cache_id      cache id
+     * @param string $compile_id    compile id
+     * @param string $resource_uid  source's uid
+     *
+     * @return void
+     */
+    protected function invalidate(
+        $cid = null,
+        $resource_name = null,
+        $cache_id = null,
+        $compile_id = null,
+        $resource_uid = null
+    ) {
+        $now = microtime(true);
+        $key = null;
+        // invalidate everything
+        if (!$resource_name && !$cache_id && !$compile_id) {
+            $key = 'IVK#ALL';
+        } // invalidate all caches by template
+        else {
+            if ($resource_name && !$cache_id && !$compile_id) {
+                $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name);
+            } // invalidate all caches by cache group
+            else {
+                if (!$resource_name && $cache_id && !$compile_id) {
+                    $key = 'IVK#CACHE#' . $this->sanitize($cache_id);
+                } // invalidate all caches by compile id
+                else {
+                    if (!$resource_name && !$cache_id && $compile_id) {
+                        $key = 'IVK#COMPILE#' . $this->sanitize($compile_id);
+                    } // invalidate by combination
+                    else {
+                        $key = 'IVK#CID#' . $cid;
+                    }
+                }
+            }
+        }
+        $this->write(array($key => $now));
+    }
+
+    /**
+     * Determine the latest timestamp known to the invalidation chain
+     *
+     * @param string $cid           CacheID to determine latest invalidation timestamp of
+     * @param string $resource_name template name
+     * @param string $cache_id      cache id
+     * @param string $compile_id    compile id
+     * @param string $resource_uid  source's filepath
+     *
+     * @return float  the microtime the CacheID was invalidated
+     */
+    protected function getLatestInvalidationTimestamp(
+        $cid,
+        $resource_name = null,
+        $cache_id = null,
+        $compile_id = null,
+        $resource_uid = null
+    ) {
+        // abort if there is no CacheID
+        if (false && !$cid) {
+            return 0;
+        }
+        // abort if there are no InvalidationKeys to check
+        if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) {
+            return 0;
+        }
+        // there are no InValidationKeys
+        if (!($values = $this->read($_cid))) {
+            return 0;
+        }
+        // make sure we're dealing with floats
+        $values = array_map('floatval', $values);
+        return max($values);
+    }
+
+    /**
+     * Translate a CacheID into the list of applicable InvalidationKeys.
+     * Splits 'some|chain|into|an|array' into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... )
+     *
+     * @param string $cid           CacheID to translate
+     * @param string $resource_name template name
+     * @param string $cache_id      cache id
+     * @param string $compile_id    compile id
+     * @param string $resource_uid  source's filepath
+     *
+     * @return array  list of InvalidationKeys
+     * @uses   $invalidationKeyPrefix to prepend to each InvalidationKey
+     */
+    protected function listInvalidationKeys(
+        $cid,
+        $resource_name = null,
+        $cache_id = null,
+        $compile_id = null,
+        $resource_uid = null
+    ) {
+        $t = array('IVK#ALL');
+        $_name = $_compile = '#';
+        if ($resource_name) {
+            $_name .= $resource_uid . '#' . $this->sanitize($resource_name);
+            $t[] = 'IVK#TEMPLATE' . $_name;
+        }
+        if ($compile_id) {
+            $_compile .= $this->sanitize($compile_id);
+            $t[] = 'IVK#COMPILE' . $_compile;
+        }
+        $_name .= '#';
+        $cid = trim($cache_id, '|');
+        if (!$cid) {
+            return $t;
+        }
+        $i = 0;
+        while (true) {
+            // determine next delimiter position
+            $i = strpos($cid, '|', $i);
+            // add complete CacheID if there are no more delimiters
+            if ($i === false) {
+                $t[] = 'IVK#CACHE#' . $cid;
+                $t[] = 'IVK#CID' . $_name . $cid . $_compile;
+                $t[] = 'IVK#CID' . $_name . $_compile;
+                break;
+            }
+            $part = substr($cid, 0, $i);
+            // add slice to list
+            $t[] = 'IVK#CACHE#' . $part;
+            $t[] = 'IVK#CID' . $_name . $part . $_compile;
+            // skip past delimiter position
+            $i++;
+        }
+        return $t;
+    }
+
+    /**
+     * Check is cache is locked for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return boolean               true or false if cache is locked
+     */
+    public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $key = 'LOCK#' . $cached->filepath;
+        $data = $this->read(array($key));
+        return $data && time() - $data[ $key ] < $smarty->locking_timeout;
+    }
+
+    /**
+     * Lock cache for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return bool|void
+     */
+    public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $cached->is_locked = true;
+        $key = 'LOCK#' . $cached->filepath;
+        $this->write(array($key => time()), $smarty->locking_timeout);
+    }
+
+    /**
+     * Unlock cache for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return bool|void
+     */
+    public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $cached->is_locked = false;
+        $key = 'LOCK#' . $cached->filepath;
+        $this->delete(array($key));
+    }
+
+    /**
+     * Read values for a set of keys from cache
+     *
+     * @param array $keys list of keys to fetch
+     *
+     * @return array list of values with the given keys used as indexes
+     */
+    abstract protected function read(array $keys);
+
+    /**
+     * Save values for a set of keys to cache
+     *
+     * @param array $keys   list of values to save
+     * @param int   $expire expiration time
+     *
+     * @return boolean true on success, false on failure
+     */
+    abstract protected function write(array $keys, $expire = null);
+
+    /**
+     * Remove values from cache
+     *
+     * @param array $keys list of keys to delete
+     *
+     * @return boolean true on success, false on failure
+     */
+    abstract protected function delete(array $keys);
+
+    /**
+     * Remove *all* values from cache
+     *
+     * @return boolean true on success, false on failure
+     */
+    protected function purge()
+    {
+        return false;
+    }
+}

+ 68 - 0
third_party/smarty/sysplugins/smarty_data.php

@@ -0,0 +1,68 @@
+<?php
+/**
+ * Smarty Plugin Data
+ * This file contains the data object
+ *
+ * @package    Smarty
+ * @subpackage Template
+ * @author     Uwe Tews
+ */
+
+/**
+ * class for the Smarty data object
+ * The Smarty data object will hold Smarty variables in the current scope
+ *
+ * @package    Smarty
+ * @subpackage Template
+ */
+class Smarty_Data extends Smarty_Internal_Data
+{
+    /**
+     * Counter
+     *
+     * @var int
+     */
+    public static $count = 0;
+
+    /**
+     * Data block name
+     *
+     * @var string
+     */
+    public $dataObjectName = '';
+
+    /**
+     * Smarty object
+     *
+     * @var Smarty
+     */
+    public $smarty = null;
+
+    /**
+     * create Smarty data object
+     *
+     * @param Smarty|array                    $_parent parent template
+     * @param Smarty|Smarty_Internal_Template $smarty  global smarty instance
+     * @param string                          $name    optional data block name
+     *
+     * @throws SmartyException
+     */
+    public function __construct($_parent = null, $smarty = null, $name = null)
+    {
+        parent::__construct();
+        self::$count++;
+        $this->dataObjectName = 'Data_object ' . (isset($name) ? "'{$name}'" : self::$count);
+        $this->smarty = $smarty;
+        if (is_object($_parent)) {
+            // when object set up back pointer
+            $this->parent = $_parent;
+        } elseif (is_array($_parent)) {
+            // set up variable values
+            foreach ($_parent as $_key => $_val) {
+                $this->tpl_vars[ $_key ] = new Smarty_Variable($_val);
+            }
+        } elseif ($_parent !== null) {
+            throw new SmartyException('Wrong type for template variables');
+        }
+    }
+}

+ 90 - 0
third_party/smarty/sysplugins/smarty_internal_block.php

@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Smarty {block} tag class
+ *
+ * @package    Smarty
+ * @subpackage PluginsInternal
+ * @author     Uwe Tews
+ */
+class Smarty_Internal_Block
+{
+    /**
+     * Block name
+     *
+     * @var string
+     */
+    public $name = '';
+
+    /**
+     * Hide attribute
+     *
+     * @var bool
+     */
+    public $hide = false;
+
+    /**
+     * Append attribute
+     *
+     * @var bool
+     */
+    public $append = false;
+
+    /**
+     * prepend attribute
+     *
+     * @var bool
+     */
+    public $prepend = false;
+
+    /**
+     * Block calls $smarty.block.child
+     *
+     * @var bool
+     */
+    public $callsChild = false;
+
+    /**
+     * Inheritance child block
+     *
+     * @var Smarty_Internal_Block|null
+     */
+    public $child = null;
+
+    /**
+     * Inheritance calling parent block
+     *
+     * @var Smarty_Internal_Block|null
+     */
+    public $parent = null;
+
+    /**
+     * Inheritance Template index
+     *
+     * @var int
+     */
+    public $tplIndex = 0;
+
+    /**
+     * Smarty_Internal_Block constructor.
+     * - if outer level {block} of child template ($state === 1) save it as child root block
+     * - otherwise process inheritance and render
+     *
+     * @param string   $name     block name
+     * @param int|null $tplIndex index of outer level {block} if nested
+     */
+    public function __construct($name, $tplIndex)
+    {
+        $this->name = $name;
+        $this->tplIndex = $tplIndex;
+    }
+
+    /**
+     * Compiled block code overloaded by {block} class
+     *
+     * @param \Smarty_Internal_Template $tpl
+     */
+    public function callBlock(Smarty_Internal_Template $tpl)
+    {
+    }
+}

+ 235 - 0
third_party/smarty/sysplugins/smarty_internal_cacheresource_file.php

@@ -0,0 +1,235 @@
+<?php
+/**
+ * Smarty Internal Plugin CacheResource File
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ * @author     Uwe Tews
+ * @author     Rodney Rehm
+ */
+
+/**
+ * This class does contain all necessary methods for the HTML cache on file system
+ * Implements the file system as resource for the HTML cache Version ussing nocache inserts.
+ *
+ * @package    Smarty
+ * @subpackage Cacher
+ */
+class Smarty_Internal_CacheResource_File extends Smarty_CacheResource
+{
+    /**
+     * populate Cached Object with meta data from Resource
+     *
+     * @param Smarty_Template_Cached   $cached    cached object
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return void
+     */
+    public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template)
+    {
+        $source = &$_template->source;
+        $smarty = &$_template->smarty;
+        $_compile_dir_sep = $smarty->use_sub_dirs ? DIRECTORY_SEPARATOR : '^';
+        $_filepath = sha1($source->uid . $smarty->_joined_template_dir);
+        $cached->filepath = $smarty->getCacheDir();
+        if (isset($_template->cache_id)) {
+            $cached->filepath .= preg_replace(
+                                     array(
+                                         '![^\w|]+!',
+                                         '![|]+!'
+                                     ),
+                                     array(
+                                         '_',
+                                         $_compile_dir_sep
+                                     ),
+                                     $_template->cache_id
+                                 ) . $_compile_dir_sep;
+        }
+        if (isset($_template->compile_id)) {
+            $cached->filepath .= preg_replace('![^\w]+!', '_', $_template->compile_id) . $_compile_dir_sep;
+        }
+        // if use_sub_dirs, break file into directories
+        if ($smarty->use_sub_dirs) {
+            $cached->filepath .= $_filepath[ 0 ] . $_filepath[ 1 ] . DIRECTORY_SEPARATOR . $_filepath[ 2 ] .
+                                 $_filepath[ 3 ] .
+                                 DIRECTORY_SEPARATOR .
+                                 $_filepath[ 4 ] . $_filepath[ 5 ] . DIRECTORY_SEPARATOR;
+        }
+        $cached->filepath .= $_filepath;
+        $basename = $source->handler->getBasename($source);
+        if (!empty($basename)) {
+            $cached->filepath .= '.' . $basename;
+        }
+        if ($smarty->cache_locking) {
+            $cached->lock_id = $cached->filepath . '.lock';
+        }
+        $cached->filepath .= '.php';
+        $cached->timestamp = $cached->exists = is_file($cached->filepath);
+        if ($cached->exists) {
+            $cached->timestamp = filemtime($cached->filepath);
+        }
+    }
+
+    /**
+     * populate Cached Object with timestamp and exists from Resource
+     *
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return void
+     */
+    public function populateTimestamp(Smarty_Template_Cached $cached)
+    {
+        $cached->timestamp = $cached->exists = is_file($cached->filepath);
+        if ($cached->exists) {
+            $cached->timestamp = filemtime($cached->filepath);
+        }
+    }
+
+    /**
+     * Read the cached template and process its header
+     *
+     * @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template
+     * @param Smarty_Template_Cached    $cached      cached object
+     * @param bool                      $update      flag if called because cache update
+     *
+     * @return boolean true or false if the cached content does not exist
+     */
+    public function process(
+        Smarty_Internal_Template $_smarty_tpl,
+        Smarty_Template_Cached $cached = null,
+        $update = false
+    ) {
+        $_smarty_tpl->cached->valid = false;
+        if ($update && defined('HHVM_VERSION')) {
+            eval('?>' . file_get_contents($_smarty_tpl->cached->filepath));
+            return true;
+        } else {
+            return @include $_smarty_tpl->cached->filepath;
+        }
+    }
+
+    /**
+     * Write the rendered template output to cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     * @param string                   $content   content to cache
+     *
+     * @return bool success
+     * @throws \SmartyException
+     */
+    public function writeCachedContent(Smarty_Internal_Template $_template, $content)
+    {
+        if ($_template->smarty->ext->_writeFile->writeFile(
+                $_template->cached->filepath,
+                $content,
+                $_template->smarty
+            ) === true
+        ) {
+            if (function_exists('opcache_invalidate')
+                && (!function_exists('ini_get') || strlen(ini_get('opcache.restrict_api'))) < 1
+            ) {
+                opcache_invalidate($_template->cached->filepath, true);
+            } elseif (function_exists('apc_compile_file')) {
+                apc_compile_file($_template->cached->filepath);
+            }
+            $cached = $_template->cached;
+            $cached->timestamp = $cached->exists = is_file($cached->filepath);
+            if ($cached->exists) {
+                $cached->timestamp = filemtime($cached->filepath);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Read cached template from cache
+     *
+     * @param Smarty_Internal_Template $_template template object
+     *
+     * @return string  content
+     */
+    public function readCachedContent(Smarty_Internal_Template $_template)
+    {
+        if (is_file($_template->cached->filepath)) {
+            return file_get_contents($_template->cached->filepath);
+        }
+        return false;
+    }
+
+    /**
+     * Empty cache
+     *
+     * @param Smarty  $smarty
+     * @param integer $exp_time expiration time (number of seconds, not timestamp)
+     *
+     * @return integer number of cache files deleted
+     */
+    public function clearAll(Smarty $smarty, $exp_time = null)
+    {
+        return $smarty->ext->_cacheResourceFile->clear($smarty, null, null, null, $exp_time);
+    }
+
+    /**
+     * Empty cache for a specific template
+     *
+     * @param Smarty  $smarty
+     * @param string  $resource_name template name
+     * @param string  $cache_id      cache id
+     * @param string  $compile_id    compile id
+     * @param integer $exp_time      expiration time (number of seconds, not timestamp)
+     *
+     * @return integer number of cache files deleted
+     */
+    public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time)
+    {
+        return $smarty->ext->_cacheResourceFile->clear($smarty, $resource_name, $cache_id, $compile_id, $exp_time);
+    }
+
+    /**
+     * Check is cache is locked for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return boolean true or false if cache is locked
+     */
+    public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        clearstatcache(true, $cached->lock_id ?? '');
+        if (null !== $cached->lock_id && is_file($cached->lock_id)) {
+            $t = filemtime($cached->lock_id);
+            return $t && (time() - $t < $smarty->locking_timeout);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Lock cache for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return bool|void
+     */
+    public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $cached->is_locked = true;
+        touch($cached->lock_id);
+    }
+
+    /**
+     * Unlock cache for this template
+     *
+     * @param Smarty                 $smarty Smarty object
+     * @param Smarty_Template_Cached $cached cached object
+     *
+     * @return bool|void
+     */
+    public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
+    {
+        $cached->is_locked = false;
+        @unlink($cached->lock_id);
+    }
+}

+ 52 - 0
third_party/smarty/sysplugins/smarty_internal_compile_append.php

@@ -0,0 +1,52 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Append
+ * Compiles the {append} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Append Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Append extends Smarty_Internal_Compile_Assign
+{
+    /**
+     * Compiles code for the {append} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        // the following must be assigned at runtime because it will be overwritten in parent class
+        $this->required_attributes = array('var', 'value');
+        $this->shorttag_order = array('var', 'value');
+        $this->optional_attributes = array('scope', 'index');
+        $this->mapCache = array();
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        // map to compile assign attributes
+        if (isset($_attr[ 'index' ])) {
+            $_params[ 'smarty_internal_index' ] = '[' . $_attr[ 'index' ] . ']';
+            unset($_attr[ 'index' ]);
+        } else {
+            $_params[ 'smarty_internal_index' ] = '[]';
+        }
+        $_new_attr = array();
+        foreach ($_attr as $key => $value) {
+            $_new_attr[] = array($key => $value);
+        }
+        // call compile assign
+        return parent::compile($_new_attr, $compiler, $_params);
+    }
+}

+ 96 - 0
third_party/smarty/sysplugins/smarty_internal_compile_assign.php

@@ -0,0 +1,96 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Assign
+ * Compiles the {assign} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Assign Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Assign extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $option_flags = array('nocache', 'noscope');
+
+    /**
+     * Valid scope names
+     *
+     * @var array
+     */
+    public $valid_scopes = array(
+        'local'    => Smarty::SCOPE_LOCAL, 'parent' => Smarty::SCOPE_PARENT,
+        'root'     => Smarty::SCOPE_ROOT, 'global' => Smarty::SCOPE_GLOBAL,
+        'tpl_root' => Smarty::SCOPE_TPL_ROOT, 'smarty' => Smarty::SCOPE_SMARTY
+    );
+
+    /**
+     * Compiles code for the {assign} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        // the following must be assigned at runtime because it will be overwritten in Smarty_Internal_Compile_Append
+        $this->required_attributes = array('var', 'value');
+        $this->shorttag_order = array('var', 'value');
+        $this->optional_attributes = array('scope');
+        $this->mapCache = array();
+        $_nocache = false;
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        // nocache ?
+        if ($_var = $compiler->getId($_attr[ 'var' ])) {
+            $_var = "'{$_var}'";
+        } else {
+            $_var = $_attr[ 'var' ];
+        }
+        if ($compiler->tag_nocache || $compiler->nocache) {
+            $_nocache = true;
+            // create nocache var to make it know for further compiling
+            $compiler->setNocacheInVariable($_attr[ 'var' ]);
+        }
+        // scope setup
+        if ($_attr[ 'noscope' ]) {
+            $_scope = -1;
+        } else {
+            $_scope = $compiler->convertScope($_attr, $this->valid_scopes);
+        }
+        // optional parameter
+        $_params = '';
+        if ($_nocache || $_scope) {
+            $_params .= ' ,' . var_export($_nocache, true);
+        }
+        if ($_scope) {
+            $_params .= ' ,' . $_scope;
+        }
+        if (isset($parameter[ 'smarty_internal_index' ])) {
+            $output =
+                "<?php \$_tmp_array = isset(\$_smarty_tpl->tpl_vars[{$_var}]) ? \$_smarty_tpl->tpl_vars[{$_var}]->value : array();\n";
+            $output .= "if (!(is_array(\$_tmp_array) || \$_tmp_array instanceof ArrayAccess)) {\n";
+            $output .= "settype(\$_tmp_array, 'array');\n";
+            $output .= "}\n";
+            $output .= "\$_tmp_array{$parameter['smarty_internal_index']} = {$_attr['value']};\n";
+            $output .= "\$_smarty_tpl->_assignInScope({$_var}, \$_tmp_array{$_params});?>";
+        } else {
+            $output = "<?php \$_smarty_tpl->_assignInScope({$_var}, {$_attr['value']}{$_params});?>";
+        }
+        return $output;
+    }
+}

+ 189 - 0
third_party/smarty/sysplugins/smarty_internal_compile_block.php

@@ -0,0 +1,189 @@
+<?php
+/**
+ * This file is part of Smarty.
+ *
+ * (c) 2015 Uwe Tews
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Smarty Internal Plugin Compile Block Class
+ *
+ * @author Uwe Tews <uwe.tews@googlemail.com>
+ */
+class Smarty_Internal_Compile_Block extends Smarty_Internal_Compile_Shared_Inheritance
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $option_flags = array('hide', 'nocache');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('assign');
+
+    /**
+     * Compiles code for the {block} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        if (!isset($compiler->_cache[ 'blockNesting' ])) {
+            $compiler->_cache[ 'blockNesting' ] = 0;
+        }
+        if ($compiler->_cache[ 'blockNesting' ] === 0) {
+            // make sure that inheritance gets initialized in template code
+            $this->registerInit($compiler);
+            $this->option_flags = array('hide', 'nocache', 'append', 'prepend');
+        } else {
+            $this->option_flags = array('hide', 'nocache');
+        }
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        ++$compiler->_cache[ 'blockNesting' ];
+        $_className = 'Block_' . preg_replace('![^\w]+!', '_', uniqid(mt_rand(), true));
+        $compiler->_cache[ 'blockName' ][ $compiler->_cache[ 'blockNesting' ] ] = $_attr[ 'name' ];
+        $compiler->_cache[ 'blockClass' ][ $compiler->_cache[ 'blockNesting' ] ] = $_className;
+        $compiler->_cache[ 'blockParams' ][ $compiler->_cache[ 'blockNesting' ] ] = array();
+        $compiler->_cache[ 'blockParams' ][ 1 ][ 'subBlocks' ][ trim($_attr[ 'name' ], '"\'') ][] = $_className;
+        $this->openTag(
+            $compiler,
+            'block',
+            array(
+                $_attr, $compiler->nocache, $compiler->parser->current_buffer,
+                $compiler->template->compiled->has_nocache_code,
+                $compiler->template->caching
+            )
+        );
+        $compiler->saveRequiredPlugins(true);
+        $compiler->nocache = $compiler->nocache | $compiler->tag_nocache;
+        $compiler->parser->current_buffer = new Smarty_Internal_ParseTree_Template();
+        $compiler->template->compiled->has_nocache_code = false;
+        $compiler->suppressNocacheProcessing = true;
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile BlockClose Class
+ */
+class Smarty_Internal_Compile_Blockclose extends Smarty_Internal_Compile_Shared_Inheritance
+{
+    /**
+     * Compiles code for the {/block} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return bool true
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        list($_attr, $_nocache, $_buffer, $_has_nocache_code, $_caching) = $this->closeTag($compiler, array('block'));
+        // init block parameter
+        $_block = $compiler->_cache[ 'blockParams' ][ $compiler->_cache[ 'blockNesting' ] ];
+        unset($compiler->_cache[ 'blockParams' ][ $compiler->_cache[ 'blockNesting' ] ]);
+        $_name = $_attr[ 'name' ];
+        $_assign = isset($_attr[ 'assign' ]) ? $_attr[ 'assign' ] : null;
+        unset($_attr[ 'assign' ], $_attr[ 'name' ]);
+        foreach ($_attr as $name => $stat) {
+            if ((is_bool($stat) && $stat !== false) || (!is_bool($stat) && $stat !== 'false')) {
+                $_block[ $name ] = 'true';
+            }
+        }
+        $_className = $compiler->_cache[ 'blockClass' ][ $compiler->_cache[ 'blockNesting' ] ];
+        // get compiled block code
+        $_functionCode = $compiler->parser->current_buffer;
+        // setup buffer for template function code
+        $compiler->parser->current_buffer = new Smarty_Internal_ParseTree_Template();
+        $output = "<?php\n";
+        $output .= "/* {block {$_name}} */\n";
+        $output .= "class {$_className} extends Smarty_Internal_Block\n";
+        $output .= "{\n";
+        foreach ($_block as $property => $value) {
+            $output .= "public \${$property} = " . var_export($value, true) . ";\n";
+        }
+        $output .= "public function callBlock(Smarty_Internal_Template \$_smarty_tpl) {\n";
+        $output .= $compiler->compileRequiredPlugins();
+        $compiler->restoreRequiredPlugins();
+        if ($compiler->template->compiled->has_nocache_code) {
+            $output .= "\$_smarty_tpl->cached->hashes['{$compiler->template->compiled->nocache_hash}'] = true;\n";
+        }
+        if (isset($_assign)) {
+            $output .= "ob_start();\n";
+        }
+        $output .= "?>\n";
+        $compiler->parser->current_buffer->append_subtree(
+            $compiler->parser,
+            new Smarty_Internal_ParseTree_Tag(
+                $compiler->parser,
+                $output
+            )
+        );
+        $compiler->parser->current_buffer->append_subtree($compiler->parser, $_functionCode);
+        $output = "<?php\n";
+        if (isset($_assign)) {
+            $output .= "\$_smarty_tpl->assign({$_assign}, ob_get_clean());\n";
+        }
+        $output .= "}\n";
+        $output .= "}\n";
+        $output .= "/* {/block {$_name}} */\n\n";
+        $output .= "?>\n";
+        $compiler->parser->current_buffer->append_subtree(
+            $compiler->parser,
+            new Smarty_Internal_ParseTree_Tag(
+                $compiler->parser,
+                $output
+            )
+        );
+        $compiler->blockOrFunctionCode .= $compiler->parser->current_buffer->to_smarty_php($compiler->parser);
+        $compiler->parser->current_buffer = new Smarty_Internal_ParseTree_Template();
+        // restore old status
+        $compiler->template->compiled->has_nocache_code = $_has_nocache_code;
+        $compiler->tag_nocache = $compiler->nocache;
+        $compiler->nocache = $_nocache;
+        $compiler->parser->current_buffer = $_buffer;
+        $output = "<?php \n";
+        if ($compiler->_cache[ 'blockNesting' ] === 1) {
+            $output .= "\$_smarty_tpl->inheritance->instanceBlock(\$_smarty_tpl, '$_className', $_name);\n";
+        } else {
+            $output .= "\$_smarty_tpl->inheritance->instanceBlock(\$_smarty_tpl, '$_className', $_name, \$this->tplIndex);\n";
+        }
+        $output .= "?>\n";
+        --$compiler->_cache[ 'blockNesting' ];
+        if ($compiler->_cache[ 'blockNesting' ] === 0) {
+            unset($compiler->_cache[ 'blockNesting' ]);
+        }
+        $compiler->has_code = true;
+        $compiler->suppressNocacheProcessing = true;
+        return $output;
+    }
+}

+ 24 - 0
third_party/smarty/sysplugins/smarty_internal_compile_block_child.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * This file is part of Smarty.
+ *
+ * (c) 2015 Uwe Tews
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Smarty Internal Plugin Compile Block Child Class
+ *
+ * @author Uwe Tews <uwe.tews@googlemail.com>
+ */
+class Smarty_Internal_Compile_Block_Child extends Smarty_Internal_Compile_Child
+{
+    /**
+     * Tag name
+     *
+     * @var string
+     */
+    public $tag = 'block_child';
+}

+ 31 - 0
third_party/smarty/sysplugins/smarty_internal_compile_block_parent.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * This file is part of Smarty.
+ *
+ * (c) 2015 Uwe Tews
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Smarty Internal Plugin Compile Block Parent Class
+ *
+ * @author Uwe Tews <uwe.tews@googlemail.com>
+ */
+class Smarty_Internal_Compile_Block_Parent extends Smarty_Internal_Compile_Child
+{
+    /**
+     * Tag name
+     *
+     * @var string
+     */
+    public $tag = 'block_parent';
+
+    /**
+     * Block type
+     *
+     * @var string
+     */
+    public $blockType = 'Parent';
+}

+ 117 - 0
third_party/smarty/sysplugins/smarty_internal_compile_break.php

@@ -0,0 +1,117 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Break
+ * Compiles the {break} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Break Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Break extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('levels');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('levels');
+
+    /**
+     * Tag name may be overloaded by Smarty_Internal_Compile_Continue
+     *
+     * @var string
+     */
+    public $tag = 'break';
+
+    /**
+     * Compiles code for the {break} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        list($levels, $foreachLevels) = $this->checkLevels($args, $compiler);
+        $output = "<?php ";
+        if ($foreachLevels > 0 && $this->tag === 'continue') {
+            $foreachLevels--;
+        }
+        if ($foreachLevels > 0) {
+            /* @var Smarty_Internal_Compile_Foreach $foreachCompiler */
+            $foreachCompiler = $compiler->getTagCompiler('foreach');
+            $output .= $foreachCompiler->compileRestore($foreachLevels);
+        }
+        $output .= "{$this->tag} {$levels};?>";
+        return $output;
+    }
+
+    /**
+     * check attributes and return array of break and foreach levels
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return array
+     * @throws \SmartyCompilerException
+     */
+    public function checkLevels($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        static $_is_loopy = array('for' => true, 'foreach' => true, 'while' => true, 'section' => true);
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        if ($_attr[ 'nocache' ] === true) {
+            $compiler->trigger_template_error('nocache option not allowed', null, true);
+        }
+        if (isset($_attr[ 'levels' ])) {
+            if (!is_numeric($_attr[ 'levels' ])) {
+                $compiler->trigger_template_error('level attribute must be a numeric constant', null, true);
+            }
+            $levels = $_attr[ 'levels' ];
+        } else {
+            $levels = 1;
+        }
+        $level_count = $levels;
+        $stack_count = count($compiler->_tag_stack) - 1;
+        $foreachLevels = 0;
+        $lastTag = '';
+        while ($level_count > 0 && $stack_count >= 0) {
+            if (isset($_is_loopy[ $compiler->_tag_stack[ $stack_count ][ 0 ] ])) {
+                $lastTag = $compiler->_tag_stack[ $stack_count ][ 0 ];
+                if ($level_count === 0) {
+                    break;
+                }
+                $level_count--;
+                if ($compiler->_tag_stack[ $stack_count ][ 0 ] === 'foreach') {
+                    $foreachLevels++;
+                }
+            }
+            $stack_count--;
+        }
+        if ($level_count !== 0) {
+            $compiler->trigger_template_error("cannot {$this->tag} {$levels} level(s)", null, true);
+        }
+        if ($lastTag === 'foreach' && $this->tag === 'break' && $foreachLevels > 0) {
+            $foreachLevels--;
+        }
+        return array($levels, $foreachLevels);
+    }
+}

+ 89 - 0
third_party/smarty/sysplugins/smarty_internal_compile_call.php

@@ -0,0 +1,89 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Function_Call
+ * Compiles the calls of user defined tags defined by {function}
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Function_Call Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Call extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('_any');
+
+    /**
+     * Compiles the calls of user defined tags defined by {function}
+     *
+     * @param array  $args     array with attributes from parser
+     * @param object $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        // save possible attributes
+        if (isset($_attr[ 'assign' ])) {
+            // output will be stored in a smarty variable instead of being displayed
+            $_assign = $_attr[ 'assign' ];
+        }
+        //$_name = trim($_attr['name'], "''");
+        $_name = $_attr[ 'name' ];
+        unset($_attr[ 'name' ], $_attr[ 'assign' ], $_attr[ 'nocache' ]);
+        // set flag (compiled code of {function} must be included in cache file
+        if (!$compiler->template->caching || $compiler->nocache || $compiler->tag_nocache) {
+            $_nocache = 'true';
+        } else {
+            $_nocache = 'false';
+        }
+        $_paramsArray = array();
+        foreach ($_attr as $_key => $_value) {
+            if (is_int($_key)) {
+                $_paramsArray[] = "$_key=>$_value";
+            } else {
+                $_paramsArray[] = "'$_key'=>$_value";
+            }
+        }
+        $_params = 'array(' . implode(',', $_paramsArray) . ')';
+        //$compiler->suppressNocacheProcessing = true;
+        // was there an assign attribute
+        if (isset($_assign)) {
+            $_output =
+                "<?php ob_start();\n\$_smarty_tpl->smarty->ext->_tplFunction->callTemplateFunction(\$_smarty_tpl, {$_name}, {$_params}, {$_nocache});\n\$_smarty_tpl->assign({$_assign}, ob_get_clean());?>\n";
+        } else {
+            $_output =
+                "<?php \$_smarty_tpl->smarty->ext->_tplFunction->callTemplateFunction(\$_smarty_tpl, {$_name}, {$_params}, {$_nocache});?>\n";
+        }
+        return $_output;
+    }
+}

+ 105 - 0
third_party/smarty/sysplugins/smarty_internal_compile_capture.php

@@ -0,0 +1,105 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Capture
+ * Compiles the {capture} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Capture Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Capture extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('name', 'assign', 'append');
+
+    /**
+     * Compiles code for the {$smarty.capture.xxx}
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     */
+    public static function compileSpecialVariable(
+        $args,
+        Smarty_Internal_TemplateCompilerBase $compiler,
+        $parameter = null
+    ) {
+        return '$_smarty_tpl->smarty->ext->_capture->getBuffer($_smarty_tpl' .
+               (isset($parameter[ 1 ]) ? ", {$parameter[ 1 ]})" : ')');
+    }
+
+    /**
+     * Compiles code for the {capture} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     * @param null                                  $parameter
+     *
+     * @return string compiled code
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter = null)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args, $parameter, 'capture');
+        $buffer = isset($_attr[ 'name' ]) ? $_attr[ 'name' ] : "'default'";
+        $assign = isset($_attr[ 'assign' ]) ? $_attr[ 'assign' ] : 'null';
+        $append = isset($_attr[ 'append' ]) ? $_attr[ 'append' ] : 'null';
+        $compiler->_cache[ 'capture_stack' ][] = array($compiler->nocache);
+        // maybe nocache because of nocache variables
+        $compiler->nocache = $compiler->nocache | $compiler->tag_nocache;
+        $_output = "<?php \$_smarty_tpl->smarty->ext->_capture->open(\$_smarty_tpl, $buffer, $assign, $append);?>";
+        return $_output;
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Captureclose Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_CaptureClose extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {/capture} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     * @param null                                  $parameter
+     *
+     * @return string compiled code
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args, $parameter, '/capture');
+        // must endblock be nocache?
+        if ($compiler->nocache) {
+            $compiler->tag_nocache = true;
+        }
+        list($compiler->nocache) = array_pop($compiler->_cache[ 'capture_stack' ]);
+        return "<?php \$_smarty_tpl->smarty->ext->_capture->close(\$_smarty_tpl);?>";
+    }
+}

+ 79 - 0
third_party/smarty/sysplugins/smarty_internal_compile_child.php

@@ -0,0 +1,79 @@
+<?php
+/**
+ * This file is part of Smarty.
+ *
+ * (c) 2015 Uwe Tews
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Smarty Internal Plugin Compile Child Class
+ *
+ * @author Uwe Tews <uwe.tews@googlemail.com>
+ */
+class Smarty_Internal_Compile_Child extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('assign');
+
+    /**
+     * Tag name
+     *
+     * @var string
+     */
+    public $tag = 'child';
+
+    /**
+     * Block type
+     *
+     * @var string
+     */
+    public $blockType = 'Child';
+
+    /**
+     * Compiles code for the {child} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        $tag = isset($parameter[ 0 ]) ? "'{$parameter[0]}'" : "'{{$this->tag}}'";
+        if (!isset($compiler->_cache[ 'blockNesting' ])) {
+            $compiler->trigger_template_error(
+                "{$tag} used outside {block} tags ",
+                $compiler->parser->lex->taglineno
+            );
+        }
+        $compiler->has_code = true;
+        $compiler->suppressNocacheProcessing = true;
+        if ($this->blockType === 'Child') {
+            $compiler->_cache[ 'blockParams' ][ $compiler->_cache[ 'blockNesting' ] ][ 'callsChild' ] = 'true';
+        }
+        $_assign = isset($_attr[ 'assign' ]) ? $_attr[ 'assign' ] : null;
+        $output = "<?php \n";
+        if (isset($_assign)) {
+            $output .= "ob_start();\n";
+        }
+        $output .= '$_smarty_tpl->inheritance->call' . $this->blockType . '($_smarty_tpl, $this' .
+                   ($this->blockType === 'Child' ? '' : ", {$tag}") . ");\n";
+        if (isset($_assign)) {
+            $output .= "\$_smarty_tpl->assign({$_assign}, ob_get_clean());\n";
+        }
+        $output .= "?>\n";
+        return $output;
+    }
+}

+ 96 - 0
third_party/smarty/sysplugins/smarty_internal_compile_config_load.php

@@ -0,0 +1,96 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Config Load
+ * Compiles the {config load} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Config Load Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Config_Load extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('file');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('file', 'section');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('section', 'scope');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $option_flags = array('nocache', 'noscope');
+
+    /**
+     * Valid scope names
+     *
+     * @var array
+     */
+    public $valid_scopes = array(
+        'local'  => Smarty::SCOPE_LOCAL, 'parent' => Smarty::SCOPE_PARENT,
+        'root'   => Smarty::SCOPE_ROOT, 'tpl_root' => Smarty::SCOPE_TPL_ROOT,
+        'smarty' => Smarty::SCOPE_SMARTY, 'global' => Smarty::SCOPE_SMARTY
+    );
+
+    /**
+     * Compiles code for the {config_load} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        if ($_attr[ 'nocache' ] === true) {
+            $compiler->trigger_template_error('nocache option not allowed', null, true);
+        }
+        // save possible attributes
+        $conf_file = $_attr[ 'file' ];
+        if (isset($_attr[ 'section' ])) {
+            $section = $_attr[ 'section' ];
+        } else {
+            $section = 'null';
+        }
+        // scope setup
+        if ($_attr[ 'noscope' ]) {
+            $_scope = -1;
+        } else {
+            $_scope = $compiler->convertScope($_attr, $this->valid_scopes);
+        }
+        // create config object
+        $_output =
+            "<?php\n\$_smarty_tpl->smarty->ext->configLoad->_loadConfigFile(\$_smarty_tpl, {$conf_file}, {$section}, {$_scope});\n?>\n";
+        return $_output;
+    }
+}

+ 25 - 0
third_party/smarty/sysplugins/smarty_internal_compile_continue.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Continue
+ * Compiles the {continue} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Continue Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Continue extends Smarty_Internal_Compile_Break
+{
+    /**
+     * Tag name
+     *
+     * @var string
+     */
+    public $tag = 'continue';
+}

+ 40 - 0
third_party/smarty/sysplugins/smarty_internal_compile_debug.php

@@ -0,0 +1,40 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Debug
+ * Compiles the {debug} tag.
+ * It opens a window the the Smarty Debugging Console.
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Debug Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Debug extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {debug} tag
+     *
+     * @param array  $args     array with attributes from parser
+     * @param object $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        // compile always as nocache
+        $compiler->tag_nocache = true;
+        // display debug template
+        $_output =
+            "<?php \$_smarty_debug = new Smarty_Internal_Debug;\n \$_smarty_debug->display_debug(\$_smarty_tpl);\n";
+        $_output .= "unset(\$_smarty_debug);\n?>";
+        return $_output;
+    }
+}

+ 70 - 0
third_party/smarty/sysplugins/smarty_internal_compile_eval.php

@@ -0,0 +1,70 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Eval
+ * Compiles the {eval} tag.
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Eval Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Eval extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('var');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('assign');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('var', 'assign');
+
+    /**
+     * Compiles code for the {eval} tag
+     *
+     * @param array  $args     array with attributes from parser
+     * @param object $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        if (isset($_attr[ 'assign' ])) {
+            // output will be stored in a smarty variable instead of being displayed
+            $_assign = $_attr[ 'assign' ];
+        }
+        // create template object
+        $_output =
+            "\$_template = new {$compiler->smarty->template_class}('eval:'.{$_attr[ 'var' ]}, \$_smarty_tpl->smarty, \$_smarty_tpl);";
+        //was there an assign attribute?
+        if (isset($_assign)) {
+            $_output .= "\$_smarty_tpl->assign($_assign,\$_template->fetch());";
+        } else {
+            $_output .= 'echo $_template->fetch();';
+        }
+        return "<?php $_output ?>";
+    }
+}

+ 158 - 0
third_party/smarty/sysplugins/smarty_internal_compile_extends.php

@@ -0,0 +1,158 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile extend
+ * Compiles the {extends} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile extend Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Extends extends Smarty_Internal_Compile_Shared_Inheritance
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('file');
+
+    /**
+     * Array of names of optional attribute required by tag
+     * use array('_any') if there is no restriction of attributes names
+     *
+     * @var array
+     */
+    public $optional_attributes = array('extends_resource');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('file');
+
+    /**
+     * Compiles code for the {extends} tag extends: resource
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        if ($_attr[ 'nocache' ] === true) {
+            $compiler->trigger_template_error('nocache option not allowed', $compiler->parser->lex->line - 1);
+        }
+        if (strpos($_attr[ 'file' ], '$_tmp') !== false) {
+            $compiler->trigger_template_error('illegal value for file attribute', $compiler->parser->lex->line - 1);
+        }
+        // add code to initialize inheritance
+        $this->registerInit($compiler, true);
+        $file = trim($_attr[ 'file' ], '\'"');
+        if (strlen($file) > 8 && substr($file, 0, 8) === 'extends:') {
+            // generate code for each template
+            $files = array_reverse(explode('|', substr($file, 8)));
+            $i = 0;
+            foreach ($files as $file) {
+                if ($file[ 0 ] === '"') {
+                    $file = trim($file, '".');
+                } else {
+                    $file = "'{$file}'";
+                }
+                $i++;
+                if ($i === count($files) && isset($_attr[ 'extends_resource' ])) {
+                    $this->compileEndChild($compiler);
+                }
+                $this->compileInclude($compiler, $file);
+            }
+            if (!isset($_attr[ 'extends_resource' ])) {
+                $this->compileEndChild($compiler);
+            }
+        } else {
+            $this->compileEndChild($compiler, $_attr[ 'file' ]);
+        }
+        $compiler->has_code = false;
+        return '';
+    }
+
+    /**
+     * Add code for inheritance endChild() method to end of template
+     *
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     * @param null|string                           $template optional inheritance parent template
+     *
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    private function compileEndChild(Smarty_Internal_TemplateCompilerBase $compiler, $template = null)
+    {
+        $inlineUids = '';
+        if (isset($template) && $compiler->smarty->merge_compiled_includes) {
+            $code = $compiler->compileTag('include', array($template, array('scope' => 'parent')));
+            if (preg_match('/([,][\s]*[\'][a-z0-9]+[\'][,][\s]*[\']content.*[\'])[)]/', $code, $match)) {
+                $inlineUids = $match[ 1 ];
+            }
+        }
+        $compiler->parser->template_postfix[] = new Smarty_Internal_ParseTree_Tag(
+            $compiler->parser,
+            '<?php $_smarty_tpl->inheritance->endChild($_smarty_tpl' .
+            (isset($template) ?
+                ", {$template}{$inlineUids}" :
+                '') . ");\n?>"
+        );
+    }
+
+    /**
+     * Add code for including subtemplate to end of template
+     *
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     * @param string                                $template subtemplate name
+     *
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    private function compileInclude(Smarty_Internal_TemplateCompilerBase $compiler, $template)
+    {
+        $compiler->parser->template_postfix[] = new Smarty_Internal_ParseTree_Tag(
+            $compiler->parser,
+            $compiler->compileTag(
+                'include',
+                array(
+                    $template,
+                    array('scope' => 'parent')
+                )
+            )
+        );
+    }
+
+    /**
+     * Create source code for {extends} from source components array
+     *
+     * @param \Smarty_Internal_Template $template
+     *
+     * @return string
+     */
+    public static function extendsSourceArrayCode(Smarty_Internal_Template $template)
+    {
+        $resources = array();
+        foreach ($template->source->components as $source) {
+            $resources[] = $source->resource;
+        }
+        return $template->smarty->left_delimiter . 'extends file=\'extends:' . join('|', $resources) .
+               '\' extends_resource=true' . $template->smarty->right_delimiter;
+    }
+}

+ 164 - 0
third_party/smarty/sysplugins/smarty_internal_compile_for.php

@@ -0,0 +1,164 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile For
+ * Compiles the {for} {forelse} {/for} tags
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile For Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_For extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {for} tag
+     * Smarty supports two different syntax's:
+     * - {for $var in $array}
+     * For looping over arrays or iterators
+     * - {for $x=0; $x<$y; $x++}
+     * For general loops
+     * The parser is generating different sets of attribute by which this compiler can
+     * determine which syntax is used.
+     *
+     * @param array  $args      array with attributes from parser
+     * @param object $compiler  compiler object
+     * @param array  $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     */
+    public function compile($args, $compiler, $parameter)
+    {
+        $compiler->loopNesting++;
+        if ($parameter === 0) {
+            $this->required_attributes = array('start', 'to');
+            $this->optional_attributes = array('max', 'step');
+        } else {
+            $this->required_attributes = array('start', 'ifexp', 'var', 'step');
+            $this->optional_attributes = array();
+        }
+        $this->mapCache = array();
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        $output = "<?php\n";
+        if ($parameter === 1) {
+            foreach ($_attr[ 'start' ] as $_statement) {
+                if (is_array($_statement[ 'var' ])) {
+                    $var = $_statement[ 'var' ][ 'var' ];
+                    $index = $_statement[ 'var' ][ 'smarty_internal_index' ];
+                } else {
+                    $var = $_statement[ 'var' ];
+                    $index = '';
+                }
+                $output .= "\$_smarty_tpl->tpl_vars[$var] = new Smarty_Variable(null, \$_smarty_tpl->isRenderingCache);\n";
+                $output .= "\$_smarty_tpl->tpl_vars[$var]->value{$index} = {$_statement['value']};\n";
+            }
+            if (is_array($_attr[ 'var' ])) {
+                $var = $_attr[ 'var' ][ 'var' ];
+                $index = $_attr[ 'var' ][ 'smarty_internal_index' ];
+            } else {
+                $var = $_attr[ 'var' ];
+                $index = '';
+            }
+            $output .= "if ($_attr[ifexp]) {\nfor (\$_foo=true;$_attr[ifexp]; \$_smarty_tpl->tpl_vars[$var]->value{$index}$_attr[step]) {\n";
+        } else {
+            $_statement = $_attr[ 'start' ];
+            if (is_array($_statement[ 'var' ])) {
+                $var = $_statement[ 'var' ][ 'var' ];
+                $index = $_statement[ 'var' ][ 'smarty_internal_index' ];
+            } else {
+                $var = $_statement[ 'var' ];
+                $index = '';
+            }
+            $output .= "\$_smarty_tpl->tpl_vars[$var] = new Smarty_Variable(null, \$_smarty_tpl->isRenderingCache);";
+            if (isset($_attr[ 'step' ])) {
+                $output .= "\$_smarty_tpl->tpl_vars[$var]->step = $_attr[step];";
+            } else {
+                $output .= "\$_smarty_tpl->tpl_vars[$var]->step = 1;";
+            }
+            if (isset($_attr[ 'max' ])) {
+                $output .= "\$_smarty_tpl->tpl_vars[$var]->total = (int) min(ceil((\$_smarty_tpl->tpl_vars[$var]->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs(\$_smarty_tpl->tpl_vars[$var]->step)),$_attr[max]);\n";
+            } else {
+                $output .= "\$_smarty_tpl->tpl_vars[$var]->total = (int) ceil((\$_smarty_tpl->tpl_vars[$var]->step > 0 ? $_attr[to]+1 - ($_statement[value]) : $_statement[value]-($_attr[to])+1)/abs(\$_smarty_tpl->tpl_vars[$var]->step));\n";
+            }
+            $output .= "if (\$_smarty_tpl->tpl_vars[$var]->total > 0) {\n";
+            $output .= "for (\$_smarty_tpl->tpl_vars[$var]->value{$index} = $_statement[value], \$_smarty_tpl->tpl_vars[$var]->iteration = 1;\$_smarty_tpl->tpl_vars[$var]->iteration <= \$_smarty_tpl->tpl_vars[$var]->total;\$_smarty_tpl->tpl_vars[$var]->value{$index} += \$_smarty_tpl->tpl_vars[$var]->step, \$_smarty_tpl->tpl_vars[$var]->iteration++) {\n";
+            $output .= "\$_smarty_tpl->tpl_vars[$var]->first = \$_smarty_tpl->tpl_vars[$var]->iteration === 1;";
+            $output .= "\$_smarty_tpl->tpl_vars[$var]->last = \$_smarty_tpl->tpl_vars[$var]->iteration === \$_smarty_tpl->tpl_vars[$var]->total;";
+        }
+        $output .= '?>';
+        $this->openTag($compiler, 'for', array('for', $compiler->nocache));
+        // maybe nocache because of nocache variables
+        $compiler->nocache = $compiler->nocache | $compiler->tag_nocache;
+        // return compiled code
+        return $output;
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Forelse Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Forelse extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {forelse} tag
+     *
+     * @param array  $args      array with attributes from parser
+     * @param object $compiler  compiler object
+     * @param array  $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     */
+    public function compile($args, $compiler, $parameter)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        list($openTag, $nocache) = $this->closeTag($compiler, array('for'));
+        $this->openTag($compiler, 'forelse', array('forelse', $nocache));
+        return "<?php }} else { ?>";
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Forclose Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Forclose extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {/for} tag
+     *
+     * @param array  $args      array with attributes from parser
+     * @param object $compiler  compiler object
+     * @param array  $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     */
+    public function compile($args, $compiler, $parameter)
+    {
+        $compiler->loopNesting--;
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        // must endblock be nocache?
+        if ($compiler->nocache) {
+            $compiler->tag_nocache = true;
+        }
+        list($openTag, $compiler->nocache) = $this->closeTag($compiler, array('for', 'forelse'));
+        $output = "<?php }\n";
+        if ($openTag !== 'forelse') {
+            $output .= "}\n";
+        }
+        $output .= "?>";
+        return $output;
+    }
+}

+ 343 - 0
third_party/smarty/sysplugins/smarty_internal_compile_foreach.php

@@ -0,0 +1,343 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Foreach
+ * Compiles the {foreach} {foreachelse} {/foreach} tags
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Foreach Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Foreach extends Smarty_Internal_Compile_Private_ForeachSection
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('from', 'item');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('name', 'key', 'properties');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('from', 'item', 'key', 'name');
+
+    /**
+     * counter
+     *
+     * @var int
+     */
+    public $counter = 0;
+
+    /**
+     * Name of this tag
+     *
+     * @var string
+     */
+    public $tagName = 'foreach';
+
+    /**
+     * Valid properties of $smarty.foreach.name.xxx variable
+     *
+     * @var array
+     */
+    public $nameProperties = array('first', 'last', 'index', 'iteration', 'show', 'total');
+
+    /**
+     * Valid properties of $item@xxx variable
+     *
+     * @var array
+     */
+    public $itemProperties = array('first', 'last', 'index', 'iteration', 'show', 'total', 'key');
+
+    /**
+     * Flag if tag had name attribute
+     *
+     * @var bool
+     */
+    public $isNamed = false;
+
+    /**
+     * Compiles code for the {foreach} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $compiler->loopNesting++;
+        // init
+        $this->isNamed = false;
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        $from = $_attr[ 'from' ];
+        $item = $compiler->getId($_attr[ 'item' ]);
+        if ($item === false) {
+            $item = $compiler->getVariableName($_attr[ 'item' ]);
+        }
+        $key = $name = null;
+        $attributes = array('item' => $item);
+        if (isset($_attr[ 'key' ])) {
+            $key = $compiler->getId($_attr[ 'key' ]);
+            if ($key === false) {
+                $key = $compiler->getVariableName($_attr[ 'key' ]);
+            }
+            $attributes[ 'key' ] = $key;
+        }
+        if (isset($_attr[ 'name' ])) {
+            $this->isNamed = true;
+            $name = $attributes[ 'name' ] = $compiler->getId($_attr[ 'name' ]);
+        }
+        foreach ($attributes as $a => $v) {
+            if ($v === false) {
+                $compiler->trigger_template_error("'{$a}' attribute/variable has illegal value", null, true);
+            }
+        }
+        $fromName = $compiler->getVariableName($_attr[ 'from' ]);
+        if ($fromName) {
+            foreach (array('item', 'key') as $a) {
+                if (isset($attributes[ $a ]) && $attributes[ $a ] === $fromName) {
+                    $compiler->trigger_template_error(
+                        "'{$a}' and 'from' may not have same variable name '{$fromName}'",
+                        null,
+                        true
+                    );
+                }
+            }
+        }
+        $itemVar = "\$_smarty_tpl->tpl_vars['{$item}']";
+        $local = '$__foreach_' . $attributes[ 'item' ] . '_' . $this->counter++ . '_';
+        // search for used tag attributes
+        $itemAttr = array();
+        $namedAttr = array();
+        $this->scanForProperties($attributes, $compiler);
+        if (!empty($this->matchResults[ 'item' ])) {
+            $itemAttr = $this->matchResults[ 'item' ];
+        }
+        if (!empty($this->matchResults[ 'named' ])) {
+            $namedAttr = $this->matchResults[ 'named' ];
+        }
+        if (isset($_attr[ 'properties' ]) && preg_match_all('/[\'](.*?)[\']/', $_attr[ 'properties' ], $match)) {
+            foreach ($match[ 1 ] as $prop) {
+                if (in_array($prop, $this->itemProperties)) {
+                    $itemAttr[ $prop ] = true;
+                } else {
+                    $compiler->trigger_template_error("Invalid property '{$prop}'", null, true);
+                }
+            }
+            if ($this->isNamed) {
+                foreach ($match[ 1 ] as $prop) {
+                    if (in_array($prop, $this->nameProperties)) {
+                        $nameAttr[ $prop ] = true;
+                    } else {
+                        $compiler->trigger_template_error("Invalid property '{$prop}'", null, true);
+                    }
+                }
+            }
+        }
+        if (isset($itemAttr[ 'first' ])) {
+            $itemAttr[ 'index' ] = true;
+        }
+        if (isset($namedAttr[ 'first' ])) {
+            $namedAttr[ 'index' ] = true;
+        }
+        if (isset($namedAttr[ 'last' ])) {
+            $namedAttr[ 'iteration' ] = true;
+            $namedAttr[ 'total' ] = true;
+        }
+        if (isset($itemAttr[ 'last' ])) {
+            $itemAttr[ 'iteration' ] = true;
+            $itemAttr[ 'total' ] = true;
+        }
+        if (isset($namedAttr[ 'show' ])) {
+            $namedAttr[ 'total' ] = true;
+        }
+        if (isset($itemAttr[ 'show' ])) {
+            $itemAttr[ 'total' ] = true;
+        }
+        $keyTerm = '';
+        if (isset($attributes[ 'key' ])) {
+            $keyTerm = "\$_smarty_tpl->tpl_vars['{$key}']->value => ";
+        }
+        if (isset($itemAttr[ 'key' ])) {
+            $keyTerm = "{$itemVar}->key => ";
+        }
+        if ($this->isNamed) {
+            $foreachVar = "\$_smarty_tpl->tpl_vars['__smarty_foreach_{$attributes['name']}']";
+        }
+        $needTotal = isset($itemAttr[ 'total' ]);
+        // Register tag
+        $this->openTag(
+            $compiler,
+            'foreach',
+            array('foreach', $compiler->nocache, $local, $itemVar, empty($itemAttr) ? 1 : 2)
+        );
+        // maybe nocache because of nocache variables
+        $compiler->nocache = $compiler->nocache | $compiler->tag_nocache;
+        // generate output code
+        $output = "<?php\n";
+        $output .= "\$_from = \$_smarty_tpl->smarty->ext->_foreach->init(\$_smarty_tpl, $from, " .
+                   var_export($item, true);
+        if ($name || $needTotal || $key) {
+            $output .= ', ' . var_export($needTotal, true);
+        }
+        if ($name || $key) {
+            $output .= ', ' . var_export($key, true);
+        }
+        if ($name) {
+            $output .= ', ' . var_export($name, true) . ', ' . var_export($namedAttr, true);
+        }
+        $output .= ");\n";
+        if (isset($itemAttr[ 'show' ])) {
+            $output .= "{$itemVar}->show = ({$itemVar}->total > 0);\n";
+        }
+        if (isset($itemAttr[ 'iteration' ])) {
+            $output .= "{$itemVar}->iteration = 0;\n";
+        }
+        if (isset($itemAttr[ 'index' ])) {
+            $output .= "{$itemVar}->index = -1;\n";
+        }
+        $output .= "{$itemVar}->do_else = true;\n";
+        $output .= "if (\$_from !== null) foreach (\$_from as {$keyTerm}{$itemVar}->value) {\n";
+        $output .= "{$itemVar}->do_else = false;\n";
+        if (isset($attributes[ 'key' ]) && isset($itemAttr[ 'key' ])) {
+            $output .= "\$_smarty_tpl->tpl_vars['{$key}']->value = {$itemVar}->key;\n";
+        }
+        if (isset($itemAttr[ 'iteration' ])) {
+            $output .= "{$itemVar}->iteration++;\n";
+        }
+        if (isset($itemAttr[ 'index' ])) {
+            $output .= "{$itemVar}->index++;\n";
+        }
+        if (isset($itemAttr[ 'first' ])) {
+            $output .= "{$itemVar}->first = !{$itemVar}->index;\n";
+        }
+        if (isset($itemAttr[ 'last' ])) {
+            $output .= "{$itemVar}->last = {$itemVar}->iteration === {$itemVar}->total;\n";
+        }
+        if (isset($foreachVar)) {
+            if (isset($namedAttr[ 'iteration' ])) {
+                $output .= "{$foreachVar}->value['iteration']++;\n";
+            }
+            if (isset($namedAttr[ 'index' ])) {
+                $output .= "{$foreachVar}->value['index']++;\n";
+            }
+            if (isset($namedAttr[ 'first' ])) {
+                $output .= "{$foreachVar}->value['first'] = !{$foreachVar}->value['index'];\n";
+            }
+            if (isset($namedAttr[ 'last' ])) {
+                $output .= "{$foreachVar}->value['last'] = {$foreachVar}->value['iteration'] === {$foreachVar}->value['total'];\n";
+            }
+        }
+        if (!empty($itemAttr)) {
+            $output .= "{$local}saved = {$itemVar};\n";
+        }
+        $output .= '?>';
+        return $output;
+    }
+
+    /**
+     * Compiles code for to restore saved template variables
+     *
+     * @param int $levels number of levels to restore
+     *
+     * @return string compiled code
+     */
+    public function compileRestore($levels)
+    {
+        return "\$_smarty_tpl->smarty->ext->_foreach->restore(\$_smarty_tpl, {$levels});";
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Foreachelse Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Foreachelse extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {foreachelse} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        list($openTag, $nocache, $local, $itemVar, $restore) = $this->closeTag($compiler, array('foreach'));
+        $this->openTag($compiler, 'foreachelse', array('foreachelse', $nocache, $local, $itemVar, 0));
+        $output = "<?php\n";
+        if ($restore === 2) {
+            $output .= "{$itemVar} = {$local}saved;\n";
+        }
+        $output .= "}\nif ({$itemVar}->do_else) {\n?>";
+        return $output;
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Foreachclose Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Foreachclose extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {/foreach} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $compiler->loopNesting--;
+        // must endblock be nocache?
+        if ($compiler->nocache) {
+            $compiler->tag_nocache = true;
+        }
+        list(
+            $openTag, $compiler->nocache, $local, $itemVar, $restore
+            ) = $this->closeTag($compiler, array('foreach', 'foreachelse'));
+        $output = "<?php\n";
+        if ($restore === 2) {
+            $output .= "{$itemVar} = {$local}saved;\n";
+        }
+        $output .= "}\n";
+        /* @var Smarty_Internal_Compile_Foreach $foreachCompiler */
+        $foreachCompiler = $compiler->getTagCompiler('foreach');
+        $output .= $foreachCompiler->compileRestore(1);
+        $output .= "?>";
+        return $output;
+    }
+}

+ 236 - 0
third_party/smarty/sysplugins/smarty_internal_compile_function.php

@@ -0,0 +1,236 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Function
+ * Compiles the {function} {/function} tags
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Function Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Function extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('_any');
+
+    /**
+     * Compiles code for the {function} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return bool true
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        if ($_attr[ 'nocache' ] === true) {
+            $compiler->trigger_template_error('nocache option not allowed', null, true);
+        }
+        unset($_attr[ 'nocache' ]);
+        $_name = trim($_attr[ 'name' ], '\'"');
+
+        if (!preg_match('/^[a-zA-Z0-9_\x80-\xff]+$/', $_name)) {
+	        $compiler->trigger_template_error("Function name contains invalid characters: {$_name}", null, true);
+        }
+
+        $compiler->parent_compiler->tpl_function[ $_name ] = array();
+        $save = array(
+            $_attr, $compiler->parser->current_buffer, $compiler->template->compiled->has_nocache_code,
+            $compiler->template->caching
+        );
+        $this->openTag($compiler, 'function', $save);
+        // Init temporary context
+        $compiler->parser->current_buffer = new Smarty_Internal_ParseTree_Template();
+        $compiler->template->compiled->has_nocache_code = false;
+        $compiler->saveRequiredPlugins(true);
+        return true;
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Functionclose Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Functionclose extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiler object
+     *
+     * @var object
+     */
+    private $compiler = null;
+
+    /**
+     * Compiles code for the {/function} tag
+     *
+     * @param array                                        $args     array with attributes from parser
+     * @param object|\Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return bool true
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $this->compiler = $compiler;
+        $saved_data = $this->closeTag($compiler, array('function'));
+        $_attr = $saved_data[ 0 ];
+        $_name = trim($_attr[ 'name' ], '\'"');
+        $compiler->parent_compiler->tpl_function[ $_name ][ 'compiled_filepath' ] =
+            $compiler->parent_compiler->template->compiled->filepath;
+        $compiler->parent_compiler->tpl_function[ $_name ][ 'uid' ] = $compiler->template->source->uid;
+        $_parameter = $_attr;
+        unset($_parameter[ 'name' ]);
+        // default parameter
+        $_paramsArray = array();
+        foreach ($_parameter as $_key => $_value) {
+            if (is_int($_key)) {
+                $_paramsArray[] = "$_key=>$_value";
+            } else {
+                $_paramsArray[] = "'$_key'=>$_value";
+            }
+        }
+        if (!empty($_paramsArray)) {
+            $_params = 'array(' . implode(',', $_paramsArray) . ')';
+            $_paramsCode = "\$params = array_merge($_params, \$params);\n";
+        } else {
+            $_paramsCode = '';
+        }
+        $_functionCode = $compiler->parser->current_buffer;
+        // setup buffer for template function code
+        $compiler->parser->current_buffer = new Smarty_Internal_ParseTree_Template();
+        $_funcName = "smarty_template_function_{$_name}_{$compiler->template->compiled->nocache_hash}";
+        $_funcNameCaching = $_funcName . '_nocache';
+        if ($compiler->template->compiled->has_nocache_code) {
+            $compiler->parent_compiler->tpl_function[ $_name ][ 'call_name_caching' ] = $_funcNameCaching;
+            $output = "<?php\n";
+            $output .= "/* {$_funcNameCaching} */\n";
+            $output .= "if (!function_exists('{$_funcNameCaching}')) {\n";
+            $output .= "function {$_funcNameCaching} (Smarty_Internal_Template \$_smarty_tpl,\$params) {\n";
+            $output .= "ob_start();\n";
+            $output .= $compiler->compileRequiredPlugins();
+            $output .= "\$_smarty_tpl->compiled->has_nocache_code = true;\n";
+            $output .= $_paramsCode;
+            $output .= "foreach (\$params as \$key => \$value) {\n\$_smarty_tpl->tpl_vars[\$key] = new Smarty_Variable(\$value, \$_smarty_tpl->isRenderingCache);\n}\n";
+            $output .= "\$params = var_export(\$params, true);\n";
+            $output .= "echo \"/*%%SmartyNocache:{$compiler->template->compiled->nocache_hash}%%*/<?php ";
+            $output .= "\\\$_smarty_tpl->smarty->ext->_tplFunction->saveTemplateVariables(\\\$_smarty_tpl, '{$_name}');\nforeach (\$params as \\\$key => \\\$value) {\n\\\$_smarty_tpl->tpl_vars[\\\$key] = new Smarty_Variable(\\\$value, \\\$_smarty_tpl->isRenderingCache);\n}\n?>";
+            $output .= "/*/%%SmartyNocache:{$compiler->template->compiled->nocache_hash}%%*/\";?>";
+            $compiler->parser->current_buffer->append_subtree(
+                $compiler->parser,
+                new Smarty_Internal_ParseTree_Tag(
+                    $compiler->parser,
+                    $output
+                )
+            );
+            $compiler->parser->current_buffer->append_subtree($compiler->parser, $_functionCode);
+            $output = "<?php echo \"/*%%SmartyNocache:{$compiler->template->compiled->nocache_hash}%%*/<?php ";
+            $output .= "\\\$_smarty_tpl->smarty->ext->_tplFunction->restoreTemplateVariables(\\\$_smarty_tpl, '{$_name}');?>\n";
+            $output .= "/*/%%SmartyNocache:{$compiler->template->compiled->nocache_hash}%%*/\";\n?>";
+            $output .= "<?php echo str_replace('{$compiler->template->compiled->nocache_hash}', \$_smarty_tpl->compiled->nocache_hash ?? '', ob_get_clean());\n";
+            $output .= "}\n}\n";
+            $output .= "/*/ {$_funcName}_nocache */\n\n";
+            $output .= "?>\n";
+            $compiler->parser->current_buffer->append_subtree(
+                $compiler->parser,
+                new Smarty_Internal_ParseTree_Tag(
+                    $compiler->parser,
+                    $output
+                )
+            );
+            $_functionCode = new Smarty_Internal_ParseTree_Tag(
+                $compiler->parser,
+                preg_replace_callback(
+                    "/((<\?php )?echo '\/\*%%SmartyNocache:{$compiler->template->compiled->nocache_hash}%%\*\/([\S\s]*?)\/\*\/%%SmartyNocache:{$compiler->template->compiled->nocache_hash}%%\*\/';(\?>\n)?)/",
+                    array($this, 'removeNocache'),
+                    $_functionCode->to_smarty_php($compiler->parser)
+                )
+            );
+        }
+        $compiler->parent_compiler->tpl_function[ $_name ][ 'call_name' ] = $_funcName;
+        $output = "<?php\n";
+        $output .= "/* {$_funcName} */\n";
+        $output .= "if (!function_exists('{$_funcName}')) {\n";
+        $output .= "function {$_funcName}(Smarty_Internal_Template \$_smarty_tpl,\$params) {\n";
+        $output .= $_paramsCode;
+        $output .= "foreach (\$params as \$key => \$value) {\n\$_smarty_tpl->tpl_vars[\$key] = new Smarty_Variable(\$value, \$_smarty_tpl->isRenderingCache);\n}\n";
+        $output .= $compiler->compileCheckPlugins(array_merge($compiler->required_plugins[ 'compiled' ],
+            $compiler->required_plugins[ 'nocache' ]));
+        $output .= "?>\n";
+        $compiler->parser->current_buffer->append_subtree(
+            $compiler->parser,
+            new Smarty_Internal_ParseTree_Tag(
+                $compiler->parser,
+                $output
+            )
+        );
+        $compiler->parser->current_buffer->append_subtree($compiler->parser, $_functionCode);
+        $output = "<?php\n}}\n";
+        $output .= "/*/ {$_funcName} */\n\n";
+        $output .= "?>\n";
+        $compiler->parser->current_buffer->append_subtree(
+            $compiler->parser,
+            new Smarty_Internal_ParseTree_Tag(
+                $compiler->parser,
+                $output
+            )
+        );
+        $compiler->parent_compiler->blockOrFunctionCode .= $compiler->parser->current_buffer->to_smarty_php($compiler->parser);
+        // restore old buffer
+        $compiler->parser->current_buffer = $saved_data[ 1 ];
+        // restore old status
+        $compiler->restoreRequiredPlugins();
+        $compiler->template->compiled->has_nocache_code = $saved_data[ 2 ];
+        $compiler->template->caching = $saved_data[ 3 ];
+        return true;
+    }
+
+    /**
+     * Remove nocache code
+     *
+     * @param $match
+     *
+     * @return string
+     */
+    public function removeNocache($match)
+    {
+        $code =
+            preg_replace(
+                "/((<\?php )?echo '\/\*%%SmartyNocache:{$this->compiler->template->compiled->nocache_hash}%%\*\/)|(\/\*\/%%SmartyNocache:{$this->compiler->template->compiled->nocache_hash}%%\*\/';(\?>\n)?)/",
+                '',
+                $match[ 0 ]
+            );
+        $code = str_replace(array('\\\'', '\\\\\''), array('\'', '\\\''), $code);
+        return $code;
+    }
+}

+ 207 - 0
third_party/smarty/sysplugins/smarty_internal_compile_if.php

@@ -0,0 +1,207 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile If
+ * Compiles the {if} {else} {elseif} {/if} tags
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile If Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_If extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {if} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        $this->openTag($compiler, 'if', array(1, $compiler->nocache));
+        // must whole block be nocache ?
+        $compiler->nocache = $compiler->nocache | $compiler->tag_nocache;
+        if (!isset($parameter[ 'if condition' ])) {
+            $compiler->trigger_template_error('missing if condition', null, true);
+        }
+        if (is_array($parameter[ 'if condition' ])) {
+            if (is_array($parameter[ 'if condition' ][ 'var' ])) {
+                $var = $parameter[ 'if condition' ][ 'var' ][ 'var' ];
+            } else {
+                $var = $parameter[ 'if condition' ][ 'var' ];
+            }
+            if ($compiler->nocache) {
+                // create nocache var to make it know for further compiling
+                $compiler->setNocacheInVariable($var);
+            }
+            $prefixVar = $compiler->getNewPrefixVariable();
+            $_output = "<?php {$prefixVar} = {$parameter[ 'if condition' ][ 'value' ]};?>\n";
+            $assignAttr = array();
+            $assignAttr[][ 'value' ] = $prefixVar;
+            $assignCompiler = new Smarty_Internal_Compile_Assign();
+            if (is_array($parameter[ 'if condition' ][ 'var' ])) {
+                $assignAttr[][ 'var' ] = $parameter[ 'if condition' ][ 'var' ][ 'var' ];
+                $_output .= $assignCompiler->compile(
+                    $assignAttr,
+                    $compiler,
+                    array('smarty_internal_index' => $parameter[ 'if condition' ][ 'var' ][ 'smarty_internal_index' ])
+                );
+            } else {
+                $assignAttr[][ 'var' ] = $parameter[ 'if condition' ][ 'var' ];
+                $_output .= $assignCompiler->compile($assignAttr, $compiler, array());
+            }
+            $_output .= "<?php if ({$prefixVar}) {?>";
+            return $_output;
+        } else {
+            return "<?php if ({$parameter['if condition']}) {?>";
+        }
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Else Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Else extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {else} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        list($nesting, $compiler->tag_nocache) = $this->closeTag($compiler, array('if', 'elseif'));
+        $this->openTag($compiler, 'else', array($nesting, $compiler->tag_nocache));
+        return '<?php } else { ?>';
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile ElseIf Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Elseif extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {elseif} tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        list($nesting, $compiler->tag_nocache) = $this->closeTag($compiler, array('if', 'elseif'));
+        if (!isset($parameter[ 'if condition' ])) {
+            $compiler->trigger_template_error('missing elseif condition', null, true);
+        }
+        $assignCode = '';
+        $var = '';
+        if (is_array($parameter[ 'if condition' ])) {
+            $condition_by_assign = true;
+            if (is_array($parameter[ 'if condition' ][ 'var' ])) {
+                $var = $parameter[ 'if condition' ][ 'var' ][ 'var' ];
+            } else {
+                $var = $parameter[ 'if condition' ][ 'var' ];
+            }
+            if ($compiler->nocache) {
+                // create nocache var to make it know for further compiling
+                $compiler->setNocacheInVariable($var);
+            }
+            $prefixVar = $compiler->getNewPrefixVariable();
+            $assignCode = "<?php {$prefixVar} = {$parameter[ 'if condition' ][ 'value' ]};?>\n";
+            $assignCompiler = new Smarty_Internal_Compile_Assign();
+            $assignAttr = array();
+            $assignAttr[][ 'value' ] = $prefixVar;
+            if (is_array($parameter[ 'if condition' ][ 'var' ])) {
+                $assignAttr[][ 'var' ] = $parameter[ 'if condition' ][ 'var' ][ 'var' ];
+                $assignCode .= $assignCompiler->compile(
+                    $assignAttr,
+                    $compiler,
+                    array('smarty_internal_index' => $parameter[ 'if condition' ][ 'var' ][ 'smarty_internal_index' ])
+                );
+            } else {
+                $assignAttr[][ 'var' ] = $parameter[ 'if condition' ][ 'var' ];
+                $assignCode .= $assignCompiler->compile($assignAttr, $compiler, array());
+            }
+        } else {
+            $condition_by_assign = false;
+        }
+        $prefixCode = $compiler->getPrefixCode();
+        if (empty($prefixCode)) {
+            if ($condition_by_assign) {
+                $this->openTag($compiler, 'elseif', array($nesting + 1, $compiler->tag_nocache));
+                $_output = $compiler->appendCode("<?php } else {\n?>", $assignCode);
+                return $compiler->appendCode($_output, "<?php if ({$prefixVar}) {?>");
+            } else {
+                $this->openTag($compiler, 'elseif', array($nesting, $compiler->tag_nocache));
+                return "<?php } elseif ({$parameter['if condition']}) {?>";
+            }
+        } else {
+            $_output = $compiler->appendCode("<?php } else {\n?>", $prefixCode);
+            $this->openTag($compiler, 'elseif', array($nesting + 1, $compiler->tag_nocache));
+            if ($condition_by_assign) {
+                $_output = $compiler->appendCode($_output, $assignCode);
+                return $compiler->appendCode($_output, "<?php if ({$prefixVar}) {?>");
+            } else {
+                return $compiler->appendCode($_output, "<?php if ({$parameter['if condition']}) {?>");
+            }
+        }
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Ifclose Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Ifclose extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {/if} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // must endblock be nocache?
+        if ($compiler->nocache) {
+            $compiler->tag_nocache = true;
+        }
+        list($nesting, $compiler->nocache) = $this->closeTag($compiler, array('if', 'else', 'elseif'));
+        $tmp = '';
+        for ($i = 0; $i < $nesting; $i++) {
+            $tmp .= '}';
+        }
+        return "<?php {$tmp}?>";
+    }
+}

+ 347 - 0
third_party/smarty/sysplugins/smarty_internal_compile_include.php

@@ -0,0 +1,347 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Include
+ * Compiles the {include} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Include Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Include extends Smarty_Internal_CompileBase
+{
+    /**
+     * caching mode to create nocache code but no cache file
+     */
+    const CACHING_NOCACHE_CODE = 9999;
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('file');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('file');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $option_flags = array('nocache', 'inline', 'caching');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('_any');
+
+    /**
+     * Valid scope names
+     *
+     * @var array
+     */
+    public $valid_scopes = array(
+        'parent' => Smarty::SCOPE_PARENT, 'root' => Smarty::SCOPE_ROOT,
+        'global' => Smarty::SCOPE_GLOBAL, 'tpl_root' => Smarty::SCOPE_TPL_ROOT,
+        'smarty' => Smarty::SCOPE_SMARTY
+    );
+
+    /**
+     * Compiles code for the {include} tag
+     *
+     * @param array                                  $args     array with attributes from parser
+     * @param Smarty_Internal_SmartyTemplateCompiler $compiler compiler object
+     *
+     * @return string
+     * @throws \Exception
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    public function compile($args, Smarty_Internal_SmartyTemplateCompiler $compiler)
+    {
+        $uid = $t_hash = null;
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        $fullResourceName = $source_resource = $_attr[ 'file' ];
+        $variable_template = false;
+        $cache_tpl = false;
+        // parse resource_name
+        if (preg_match('/^([\'"])(([A-Za-z0-9_\-]{2,})[:])?(([^$()]+)|(.+))\1$/', $source_resource, $match)) {
+            $type = !empty($match[ 3 ]) ? $match[ 3 ] : $compiler->template->smarty->default_resource_type;
+            $name = !empty($match[ 5 ]) ? $match[ 5 ] : $match[ 6 ];
+            $handler = Smarty_Resource::load($compiler->smarty, $type);
+            if ($handler->recompiled || $handler->uncompiled) {
+                $variable_template = true;
+            }
+            if (!$variable_template) {
+                if ($type !== 'string') {
+                    $fullResourceName = "{$type}:{$name}";
+                    $compiled = $compiler->parent_compiler->template->compiled;
+                    if (isset($compiled->includes[ $fullResourceName ])) {
+                        $compiled->includes[ $fullResourceName ]++;
+                        $cache_tpl = true;
+                    } else {
+                        if ("{$compiler->template->source->type}:{$compiler->template->source->name}" ==
+                            $fullResourceName
+                        ) {
+                            // recursive call of current template
+                            $compiled->includes[ $fullResourceName ] = 2;
+                            $cache_tpl = true;
+                        } else {
+                            $compiled->includes[ $fullResourceName ] = 1;
+                        }
+                    }
+                    $fullResourceName = $match[ 1 ] . $fullResourceName . $match[ 1 ];
+                }
+            }
+            if (empty($match[ 5 ])) {
+                $variable_template = true;
+            }
+        } else {
+            $variable_template = true;
+        }
+        // scope setup
+        $_scope = $compiler->convertScope($_attr, $this->valid_scopes);
+        // set flag to cache subtemplate object when called within loop or template name is variable.
+        if ($cache_tpl || $variable_template || $compiler->loopNesting > 0) {
+            $_cache_tpl = 'true';
+        } else {
+            $_cache_tpl = 'false';
+        }
+        // assume caching is off
+        $_caching = Smarty::CACHING_OFF;
+        $call_nocache = $compiler->tag_nocache || $compiler->nocache;
+        // caching was on and {include} is not in nocache mode
+        if ($compiler->template->caching && !$compiler->nocache && !$compiler->tag_nocache) {
+            $_caching = self::CACHING_NOCACHE_CODE;
+        }
+        // flag if included template code should be merged into caller
+        $merge_compiled_includes = ($compiler->smarty->merge_compiled_includes || $_attr[ 'inline' ] === true) &&
+                                   !$compiler->template->source->handler->recompiled;
+        if ($merge_compiled_includes) {
+            // variable template name ?
+            if ($variable_template) {
+                $merge_compiled_includes = false;
+            }
+            // variable compile_id?
+            if (isset($_attr[ 'compile_id' ]) && $compiler->isVariable($_attr[ 'compile_id' ])) {
+                $merge_compiled_includes = false;
+            }
+        }
+        /*
+        * if the {include} tag provides individual parameter for caching or compile_id
+        * the subtemplate must not be included into the common cache file and is treated like
+        * a call in nocache mode.
+        *
+        */
+        if ($_attr[ 'nocache' ] !== true && $_attr[ 'caching' ]) {
+            $_caching = $_new_caching = (int)$_attr[ 'caching' ];
+            $call_nocache = true;
+        } else {
+            $_new_caching = Smarty::CACHING_LIFETIME_CURRENT;
+        }
+        if (isset($_attr[ 'cache_lifetime' ])) {
+            $_cache_lifetime = $_attr[ 'cache_lifetime' ];
+            $call_nocache = true;
+            $_caching = $_new_caching;
+        } else {
+            $_cache_lifetime = '$_smarty_tpl->cache_lifetime';
+        }
+        if (isset($_attr[ 'cache_id' ])) {
+            $_cache_id = $_attr[ 'cache_id' ];
+            $call_nocache = true;
+            $_caching = $_new_caching;
+        } else {
+            $_cache_id = '$_smarty_tpl->cache_id';
+        }
+        if (isset($_attr[ 'compile_id' ])) {
+            $_compile_id = $_attr[ 'compile_id' ];
+        } else {
+            $_compile_id = '$_smarty_tpl->compile_id';
+        }
+        // if subtemplate will be called in nocache mode do not merge
+        if ($compiler->template->caching && $call_nocache) {
+            $merge_compiled_includes = false;
+        }
+        // assign attribute
+        if (isset($_attr[ 'assign' ])) {
+            // output will be stored in a smarty variable instead of being displayed
+            if ($_assign = $compiler->getId($_attr[ 'assign' ])) {
+                $_assign = "'{$_assign}'";
+                if ($compiler->tag_nocache || $compiler->nocache || $call_nocache) {
+                    // create nocache var to make it know for further compiling
+                    $compiler->setNocacheInVariable($_attr[ 'assign' ]);
+                }
+            } else {
+                $_assign = $_attr[ 'assign' ];
+            }
+        }
+        $has_compiled_template = false;
+        if ($merge_compiled_includes) {
+            $c_id = isset($_attr[ 'compile_id' ]) ? $_attr[ 'compile_id' ] : $compiler->template->compile_id;
+            // we must observe different compile_id and caching
+            $t_hash = sha1($c_id . ($_caching ? '--caching' : '--nocaching'));
+            $compiler->smarty->allow_ambiguous_resources = true;
+            /* @var Smarty_Internal_Template $tpl */
+            $tpl = new $compiler->smarty->template_class(
+                trim($fullResourceName, '"\''),
+                $compiler->smarty,
+                $compiler->template,
+                $compiler->template->cache_id,
+                $c_id,
+                $_caching
+            );
+            $uid = $tpl->source->type . $tpl->source->uid;
+            if (!isset($compiler->parent_compiler->mergedSubTemplatesData[ $uid ][ $t_hash ])) {
+                $has_compiled_template = $this->compileInlineTemplate($compiler, $tpl, $t_hash);
+            } else {
+                $has_compiled_template = true;
+            }
+            unset($tpl);
+        }
+        // delete {include} standard attributes
+        unset($_attr[ 'file' ], $_attr[ 'assign' ], $_attr[ 'cache_id' ], $_attr[ 'compile_id' ], $_attr[ 'cache_lifetime' ], $_attr[ 'nocache' ], $_attr[ 'caching' ], $_attr[ 'scope' ], $_attr[ 'inline' ]);
+        // remaining attributes must be assigned as smarty variable
+        $_vars = 'array()';
+        if (!empty($_attr)) {
+            $_pairs = array();
+            // create variables
+            foreach ($_attr as $key => $value) {
+                $_pairs[] = "'$key'=>$value";
+            }
+            $_vars = 'array(' . join(',', $_pairs) . ')';
+        }
+        $update_compile_id = $compiler->template->caching && !$compiler->tag_nocache && !$compiler->nocache &&
+                             $_compile_id !== '$_smarty_tpl->compile_id';
+        if ($has_compiled_template && !$call_nocache) {
+            $_output = "<?php\n";
+            if ($update_compile_id) {
+                $_output .= $compiler->makeNocacheCode("\$_compile_id_save[] = \$_smarty_tpl->compile_id;\n\$_smarty_tpl->compile_id = {$_compile_id};\n");
+            }
+            if (!empty($_attr) && $_caching === 9999 && $compiler->template->caching) {
+                $_vars_nc = "foreach ($_vars as \$ik => \$iv) {\n";
+                $_vars_nc .= "\$_smarty_tpl->tpl_vars[\$ik] =  new Smarty_Variable(\$iv);\n";
+                $_vars_nc .= "}\n";
+                $_output .= substr($compiler->processNocacheCode('<?php ' . $_vars_nc . "?>\n", true), 6, -3);
+            }
+            if (isset($_assign)) {
+                $_output .= "ob_start();\n";
+            }
+            $_output .= "\$_smarty_tpl->_subTemplateRender({$fullResourceName}, {$_cache_id}, {$_compile_id}, {$_caching}, {$_cache_lifetime}, {$_vars}, {$_scope}, {$_cache_tpl}, '{$compiler->parent_compiler->mergedSubTemplatesData[$uid][$t_hash]['uid']}', '{$compiler->parent_compiler->mergedSubTemplatesData[$uid][$t_hash]['func']}');\n";
+            if (isset($_assign)) {
+                $_output .= "\$_smarty_tpl->assign({$_assign}, ob_get_clean());\n";
+            }
+            if ($update_compile_id) {
+                $_output .= $compiler->makeNocacheCode("\$_smarty_tpl->compile_id = array_pop(\$_compile_id_save);\n");
+            }
+            $_output .= "?>";
+            return $_output;
+        }
+        if ($call_nocache) {
+            $compiler->tag_nocache = true;
+        }
+        $_output = "<?php ";
+        if ($update_compile_id) {
+            $_output .= "\$_compile_id_save[] = \$_smarty_tpl->compile_id;\n\$_smarty_tpl->compile_id = {$_compile_id};\n";
+        }
+        // was there an assign attribute
+        if (isset($_assign)) {
+            $_output .= "ob_start();\n";
+        }
+        $_output .= "\$_smarty_tpl->_subTemplateRender({$fullResourceName}, $_cache_id, $_compile_id, $_caching, $_cache_lifetime, $_vars, $_scope, {$_cache_tpl});\n";
+        if (isset($_assign)) {
+            $_output .= "\$_smarty_tpl->assign({$_assign}, ob_get_clean());\n";
+        }
+        if ($update_compile_id) {
+            $_output .= "\$_smarty_tpl->compile_id = array_pop(\$_compile_id_save);\n";
+        }
+        $_output .= "?>";
+        return $_output;
+    }
+
+    /**
+     * Compile inline sub template
+     *
+     * @param \Smarty_Internal_SmartyTemplateCompiler $compiler
+     * @param \Smarty_Internal_Template               $tpl
+     * @param string                                  $t_hash
+     *
+     * @return bool
+     * @throws \Exception
+     * @throws \SmartyException
+     */
+    public function compileInlineTemplate(
+        Smarty_Internal_SmartyTemplateCompiler $compiler,
+        Smarty_Internal_Template $tpl,
+        $t_hash
+    ) {
+        $uid = $tpl->source->type . $tpl->source->uid;
+        if (!($tpl->source->handler->uncompiled) && $tpl->source->exists) {
+            $compiler->parent_compiler->mergedSubTemplatesData[ $uid ][ $t_hash ][ 'uid' ] = $tpl->source->uid;
+            if (isset($compiler->template->inheritance)) {
+                $tpl->inheritance = clone $compiler->template->inheritance;
+            }
+            $tpl->compiled = new Smarty_Template_Compiled();
+            $tpl->compiled->nocache_hash = $compiler->parent_compiler->template->compiled->nocache_hash;
+            $tpl->loadCompiler();
+            // save unique function name
+            $compiler->parent_compiler->mergedSubTemplatesData[ $uid ][ $t_hash ][ 'func' ] =
+            $tpl->compiled->unifunc = 'content_' . str_replace(array('.', ','), '_', uniqid('', true));
+            // make sure whole chain gets compiled
+            $tpl->mustCompile = true;
+            $compiler->parent_compiler->mergedSubTemplatesData[ $uid ][ $t_hash ][ 'nocache_hash' ] =
+                $tpl->compiled->nocache_hash;
+            if ($tpl->source->type === 'file') {
+                $sourceInfo = $tpl->source->filepath;
+            } else {
+                $basename = $tpl->source->handler->getBasename($tpl->source);
+                $sourceInfo = $tpl->source->type . ':' .
+                              ($basename ? $basename : $tpl->source->name);
+            }
+            // get compiled code
+            $compiled_code = "<?php\n\n";
+            $compiled_code .= "/* Start inline template \"{$sourceInfo}\" =============================*/\n";
+            $compiled_code .= "function {$tpl->compiled->unifunc} (Smarty_Internal_Template \$_smarty_tpl) {\n";
+            $compiled_code .= "?>\n" . $tpl->compiler->compileTemplateSource($tpl, null, $compiler->parent_compiler);
+            $compiled_code .= "<?php\n";
+            $compiled_code .= "}\n?>\n";
+            $compiled_code .= $tpl->compiler->postFilter($tpl->compiler->blockOrFunctionCode);
+            $compiled_code .= "<?php\n\n";
+            $compiled_code .= "/* End inline template \"{$sourceInfo}\" =============================*/\n";
+            $compiled_code .= '?>';
+            unset($tpl->compiler);
+            if ($tpl->compiled->has_nocache_code) {
+                // replace nocache_hash
+                $compiled_code =
+                    str_replace(
+                        "{$tpl->compiled->nocache_hash}",
+                        $compiler->template->compiled->nocache_hash,
+                        $compiled_code
+                    );
+                $compiler->template->compiled->has_nocache_code = true;
+            }
+            $compiler->parent_compiler->mergedSubTemplatesCode[ $tpl->compiled->unifunc ] = $compiled_code;
+            return true;
+        } else {
+            return false;
+        }
+    }
+}

+ 157 - 0
third_party/smarty/sysplugins/smarty_internal_compile_insert.php

@@ -0,0 +1,157 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Insert
+ * Compiles the {insert} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Insert Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Insert extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $required_attributes = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $shorttag_order = array('name');
+
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('_any');
+
+    /**
+     * Compiles code for the {insert} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        $nocacheParam = $compiler->template->caching && ($compiler->tag_nocache || $compiler->nocache);
+        if (!$nocacheParam) {
+            // do not compile as nocache code
+            $compiler->suppressNocacheProcessing = true;
+        }
+        $compiler->tag_nocache = true;
+        $_smarty_tpl = $compiler->template;
+        $_name = null;
+        $_script = null;
+        $_output = '<?php ';
+        // save possible attributes
+        eval('$_name = @' . $_attr[ 'name' ] . ';');
+        if (isset($_attr[ 'assign' ])) {
+            // output will be stored in a smarty variable instead of being displayed
+            $_assign = $_attr[ 'assign' ];
+            // create variable to make sure that the compiler knows about its nocache status
+            $var = trim($_attr[ 'assign' ], '\'');
+            if (isset($compiler->template->tpl_vars[ $var ])) {
+                $compiler->template->tpl_vars[ $var ]->nocache = true;
+            } else {
+                $compiler->template->tpl_vars[ $var ] = new Smarty_Variable(null, true);
+            }
+        }
+        if (isset($_attr[ 'script' ])) {
+            // script which must be included
+            $_function = "smarty_insert_{$_name}";
+            $_smarty_tpl = $compiler->template;
+            $_filepath = false;
+            eval('$_script = @' . $_attr[ 'script' ] . ';');
+            if (!isset($compiler->smarty->security_policy) && file_exists($_script)) {
+                $_filepath = $_script;
+            } else {
+                if (isset($compiler->smarty->security_policy)) {
+                    $_dir = $compiler->smarty->security_policy->trusted_dir;
+                } else {
+                    $_dir = null;
+                }
+                if (!empty($_dir)) {
+                    foreach ((array)$_dir as $_script_dir) {
+                        $_script_dir = rtrim($_script_dir, '/\\') . DIRECTORY_SEPARATOR;
+                        if (file_exists($_script_dir . $_script)) {
+                            $_filepath = $_script_dir . $_script;
+                            break;
+                        }
+                    }
+                }
+            }
+            if ($_filepath === false) {
+                $compiler->trigger_template_error("{insert} missing script file '{$_script}'", null, true);
+            }
+            // code for script file loading
+            $_output .= "require_once '{$_filepath}' ;";
+            include_once $_filepath;
+            if (!is_callable($_function)) {
+                $compiler->trigger_template_error(
+                    " {insert} function '{$_function}' is not callable in script file '{$_script}'",
+                    null,
+                    true
+                );
+            }
+        } else {
+            $_filepath = 'null';
+            $_function = "insert_{$_name}";
+            // function in PHP script ?
+            if (!is_callable($_function)) {
+                // try plugin
+                if (!$_function = $compiler->getPlugin($_name, 'insert')) {
+                    $compiler->trigger_template_error(
+                        "{insert} no function or plugin found for '{$_name}'",
+                        null,
+                        true
+                    );
+                }
+            }
+        }
+        // delete {insert} standard attributes
+        unset($_attr[ 'name' ], $_attr[ 'assign' ], $_attr[ 'script' ], $_attr[ 'nocache' ]);
+        // convert attributes into parameter array string
+        $_paramsArray = array();
+        foreach ($_attr as $_key => $_value) {
+            $_paramsArray[] = "'$_key' => $_value";
+        }
+        $_params = 'array(' . implode(", ", $_paramsArray) . ')';
+        // call insert
+        if (isset($_assign)) {
+            if ($_smarty_tpl->caching && !$nocacheParam) {
+                $_output .= "echo Smarty_Internal_Nocache_Insert::compile ('{$_function}',{$_params}, \$_smarty_tpl, '{$_filepath}',{$_assign});?>";
+            } else {
+                $_output .= "\$_smarty_tpl->assign({$_assign} , {$_function} ({$_params},\$_smarty_tpl), true);?>";
+            }
+        } else {
+            if ($_smarty_tpl->caching && !$nocacheParam) {
+                $_output .= "echo Smarty_Internal_Nocache_Insert::compile ('{$_function}',{$_params}, \$_smarty_tpl, '{$_filepath}');?>";
+            } else {
+                $_output .= "echo {$_function}({$_params},\$_smarty_tpl);?>";
+            }
+        }
+        $compiler->template->compiled->has_nocache_code = true;
+        return $_output;
+    }
+}

+ 37 - 0
third_party/smarty/sysplugins/smarty_internal_compile_ldelim.php

@@ -0,0 +1,37 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Ldelim
+ * Compiles the {ldelim} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Ldelim Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Ldelim extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {ldelim} tag
+     * This tag does output the left delimiter
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $_attr = $this->getAttributes($compiler, $args);
+        if ($_attr[ 'nocache' ] === true) {
+            $compiler->trigger_template_error('nocache option not allowed', null, true);
+        }
+        return $compiler->smarty->left_delimiter;
+    }
+}

+ 62 - 0
third_party/smarty/sysplugins/smarty_internal_compile_make_nocache.php

@@ -0,0 +1,62 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Make_Nocache
+ * Compiles the {make_nocache} tag
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Make_Nocache Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Make_Nocache extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $option_flags = array();
+
+    /**
+     * Array of names of required attribute required by tag
+     *
+     * @var array
+     */
+    public $required_attributes = array('var');
+
+    /**
+     * Shorttag attribute order defined by its names
+     *
+     * @var array
+     */
+    public $shorttag_order = array('var');
+
+    /**
+     * Compiles code for the {make_nocache} tag
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return string compiled code
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // check and get attributes
+        $_attr = $this->getAttributes($compiler, $args);
+        if ($compiler->template->caching) {
+            $output = "<?php \$_smarty_tpl->smarty->ext->_make_nocache->save(\$_smarty_tpl, {$_attr[ 'var' ]});\n?>\n";
+            $compiler->template->compiled->has_nocache_code = true;
+            $compiler->suppressNocacheProcessing = true;
+            return $output;
+        } else {
+            return true;
+        }
+    }
+}

+ 73 - 0
third_party/smarty/sysplugins/smarty_internal_compile_nocache.php

@@ -0,0 +1,73 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Nocache
+ * Compiles the {nocache} {/nocache} tags.
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Nocache Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Nocache extends Smarty_Internal_CompileBase
+{
+    /**
+     * Array of names of valid option flags
+     *
+     * @var array
+     */
+    public $option_flags = array();
+
+    /**
+     * Compiles code for the {nocache} tag
+     * This tag does not generate compiled output. It only sets a compiler flag.
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return bool
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $_attr = $this->getAttributes($compiler, $args);
+        $this->openTag($compiler, 'nocache', array($compiler->nocache));
+        // enter nocache mode
+        $compiler->nocache = true;
+        // this tag does not return compiled code
+        $compiler->has_code = false;
+        return true;
+    }
+}
+
+/**
+ * Smarty Internal Plugin Compile Nocacheclose Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Nocacheclose extends Smarty_Internal_CompileBase
+{
+    /**
+     * Compiles code for the {/nocache} tag
+     * This tag does not generate compiled output. It only sets a compiler flag.
+     *
+     * @param array                                 $args     array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler compiler object
+     *
+     * @return bool
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $_attr = $this->getAttributes($compiler, $args);
+        // leave nocache mode
+        list($compiler->nocache) = $this->closeTag($compiler, array('nocache'));
+        // this tag does not return compiled code
+        $compiler->has_code = false;
+        return true;
+    }
+}

+ 31 - 0
third_party/smarty/sysplugins/smarty_internal_compile_parent.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * This file is part of Smarty.
+ *
+ * (c) 2015 Uwe Tews
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Smarty Internal Plugin Compile Parent Class
+ *
+ * @author Uwe Tews <uwe.tews@googlemail.com>
+ */
+class Smarty_Internal_Compile_Parent extends Smarty_Internal_Compile_Child
+{
+    /**
+     * Tag name
+     *
+     * @var string
+     */
+    public $tag = 'parent';
+
+    /**
+     * Block type
+     *
+     * @var string
+     */
+    public $blockType = 'Parent';
+}

+ 124 - 0
third_party/smarty/sysplugins/smarty_internal_compile_private_block_plugin.php

@@ -0,0 +1,124 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile Block Plugin
+ * Compiles code for the execution of block plugin
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile Block Plugin Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Private_Block_Plugin extends Smarty_Internal_CompileBase
+{
+    /**
+     * Attribute definition: Overwrites base class.
+     *
+     * @var array
+     * @see Smarty_Internal_CompileBase
+     */
+    public $optional_attributes = array('_any');
+
+    /**
+     * nesting level
+     *
+     * @var int
+     */
+    public $nesting = 0;
+
+    /**
+     * Compiles code for the execution of block plugin
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     * @param string                                $tag       name of block plugin
+     * @param string                                $function  PHP function name
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     * @throws \SmartyException
+     */
+    public function compile($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter, $tag, $function = null)
+    {
+        if (!isset($tag[ 5 ]) || substr($tag, -5) !== 'close') {
+            // opening tag of block plugin
+            // check and get attributes
+            $_attr = $this->getAttributes($compiler, $args);
+            $this->nesting++;
+            unset($_attr[ 'nocache' ]);
+            list($callback, $_paramsArray, $callable) = $this->setup($compiler, $_attr, $tag, $function);
+            $_params = 'array(' . implode(',', $_paramsArray) . ')';
+            // compile code
+            $output = "<?php ";
+            if (is_array($callback)) {
+                $output .= "\$_block_plugin{$this->nesting} = isset({$callback[0]}) ? {$callback[0]} : null;\n";
+                $callback = "\$_block_plugin{$this->nesting}{$callback[1]}";
+            }
+            if (isset($callable)) {
+                $output .= "if (!is_callable({$callable})) {\nthrow new SmartyException('block tag \'{$tag}\' not callable or registered');\n}\n";
+            }
+            $output .= "\$_smarty_tpl->smarty->_cache['_tag_stack'][] = array('{$tag}', {$_params});\n";
+            $output .= "\$_block_repeat=true;\necho {$callback}({$_params}, null, \$_smarty_tpl, \$_block_repeat);\nwhile (\$_block_repeat) {\nob_start();?>";
+            $this->openTag($compiler, $tag, array($_params, $compiler->nocache, $callback));
+            // maybe nocache because of nocache variables or nocache plugin
+            $compiler->nocache = $compiler->nocache | $compiler->tag_nocache;
+        } else {
+            // must endblock be nocache?
+            if ($compiler->nocache) {
+                $compiler->tag_nocache = true;
+            }
+            // closing tag of block plugin, restore nocache
+            list($_params, $compiler->nocache, $callback) = $this->closeTag($compiler, substr($tag, 0, -5));
+            // compile code
+            if (!isset($parameter[ 'modifier_list' ])) {
+                $mod_pre = $mod_post = $mod_content = '';
+                $mod_content2 = 'ob_get_clean()';
+            } else {
+                $mod_content2 = "\$_block_content{$this->nesting}";
+                $mod_content = "\$_block_content{$this->nesting} = ob_get_clean();\n";
+                $mod_pre = "ob_start();\n";
+                $mod_post = 'echo ' . $compiler->compileTag(
+                        'private_modifier',
+                        array(),
+                        array(
+                            'modifierlist' => $parameter[ 'modifier_list' ],
+                            'value'        => 'ob_get_clean()'
+                        )
+                    ) . ";\n";
+            }
+            $output =
+                "<?php {$mod_content}\$_block_repeat=false;\n{$mod_pre}echo {$callback}({$_params}, {$mod_content2}, \$_smarty_tpl, \$_block_repeat);\n{$mod_post}}\n";
+            $output .= 'array_pop($_smarty_tpl->smarty->_cache[\'_tag_stack\']);?>';
+        }
+        return $output;
+    }
+
+    /**
+     * Setup callback and parameter array
+     *
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     * @param array                                 $_attr attributes
+     * @param string                                $tag
+     * @param string                                $function
+     *
+     * @return array
+     */
+    public function setup(Smarty_Internal_TemplateCompilerBase $compiler, $_attr, $tag, $function)
+    {
+        $_paramsArray = array();
+        foreach ($_attr as $_key => $_value) {
+            if (is_int($_key)) {
+                $_paramsArray[] = "$_key=>$_value";
+            } else {
+                $_paramsArray[] = "'$_key'=>$_value";
+            }
+        }
+        return array($function, $_paramsArray, null);
+    }
+}

+ 228 - 0
third_party/smarty/sysplugins/smarty_internal_compile_private_foreachsection.php

@@ -0,0 +1,228 @@
+<?php
+/**
+ * Smarty Internal Plugin Compile ForeachSection
+ * Shared methods for {foreach} {section} tags
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ * @author     Uwe Tews
+ */
+
+/**
+ * Smarty Internal Plugin Compile ForeachSection Class
+ *
+ * @package    Smarty
+ * @subpackage Compiler
+ */
+class Smarty_Internal_Compile_Private_ForeachSection extends Smarty_Internal_CompileBase
+{
+    /**
+     * Name of this tag
+     *
+     * @var string
+     */
+    public $tagName = '';
+
+    /**
+     * Valid properties of $smarty.xxx variable
+     *
+     * @var array
+     */
+    public $nameProperties = array();
+
+    /**
+     * {section} tag has no item properties
+     *
+     * @var array
+     */
+    public $itemProperties = null;
+
+    /**
+     * {section} tag has always name attribute
+     *
+     * @var bool
+     */
+    public $isNamed = true;
+
+    /**
+     * @var array
+     */
+    public $matchResults = array();
+
+    /**
+     * Preg search pattern
+     *
+     * @var string
+     */
+    private $propertyPreg = '';
+
+    /**
+     * Offsets in preg match result
+     *
+     * @var array
+     */
+    private $resultOffsets = array();
+
+    /**
+     * Start offset
+     *
+     * @var int
+     */
+    private $startOffset = 0;
+
+    /**
+     * Scan sources for used tag attributes
+     *
+     * @param array                                 $attributes
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     *
+     * @throws \SmartyException
+     */
+    public function scanForProperties($attributes, Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $this->propertyPreg = '~(';
+        $this->startOffset = 1;
+        $this->resultOffsets = array();
+        $this->matchResults = array('named' => array(), 'item' => array());
+        if (isset($attributes[ 'name' ])) {
+            $this->buildPropertyPreg(true, $attributes);
+        }
+        if (isset($this->itemProperties)) {
+            if ($this->isNamed) {
+                $this->propertyPreg .= '|';
+            }
+            $this->buildPropertyPreg(false, $attributes);
+        }
+        $this->propertyPreg .= ')\W~i';
+        // Template source
+        $this->matchTemplateSource($compiler);
+        // Parent template source
+        $this->matchParentTemplateSource($compiler);
+        // {block} source
+        $this->matchBlockSource($compiler);
+    }
+
+    /**
+     * Build property preg string
+     *
+     * @param bool  $named
+     * @param array $attributes
+     */
+    public function buildPropertyPreg($named, $attributes)
+    {
+        if ($named) {
+            $this->resultOffsets[ 'named' ] = $this->startOffset = $this->startOffset + 3;
+            $this->propertyPreg .= "(([\$]smarty[.]{$this->tagName}[.]" .
+                                   ($this->tagName === 'section' ? "|[\[]\s*" : '') .
+                                   "){$attributes['name']}[.](";
+            $properties = $this->nameProperties;
+        } else {
+            $this->resultOffsets[ 'item' ] = $this->startOffset = $this->startOffset + 2;
+            $this->propertyPreg .= "([\$]{$attributes['item']}[@](";
+            $properties = $this->itemProperties;
+        }
+        $propName = reset($properties);
+        while ($propName) {
+            $this->propertyPreg .= "{$propName}";
+            $propName = next($properties);
+            if ($propName) {
+                $this->propertyPreg .= '|';
+            }
+        }
+        $this->propertyPreg .= '))';
+    }
+
+    /**
+     * Find matches in source string
+     *
+     * @param string $source
+     */
+    public function matchProperty($source)
+    {
+        preg_match_all($this->propertyPreg, $source, $match);
+        foreach ($this->resultOffsets as $key => $offset) {
+            foreach ($match[ $offset ] as $m) {
+                if (!empty($m)) {
+                    $this->matchResults[ $key ][ strtolower($m) ] = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * Find matches in template source
+     *
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     */
+    public function matchTemplateSource(Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        $this->matchProperty($compiler->parser->lex->data);
+    }
+
+    /**
+     * Find matches in all parent template source
+     *
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     *
+     * @throws \SmartyException
+     */
+    public function matchParentTemplateSource(Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+        // search parent compiler template source
+        $nextCompiler = $compiler;
+        while ($nextCompiler !== $nextCompiler->parent_compiler) {
+            $nextCompiler = $nextCompiler->parent_compiler;
+            if ($compiler !== $nextCompiler) {
+                // get template source
+                $_content = $nextCompiler->template->source->getContent();
+                if ($_content !== '') {
+                    // run pre filter if required
+                    if ((isset($nextCompiler->smarty->autoload_filters[ 'pre' ]) ||
+                         isset($nextCompiler->smarty->registered_filters[ 'pre' ]))
+                    ) {
+                        $_content = $nextCompiler->smarty->ext->_filterHandler->runFilter(
+                            'pre',
+                            $_content,
+                            $nextCompiler->template
+                        );
+                    }
+                    $this->matchProperty($_content);
+                }
+            }
+        }
+    }
+
+    /**
+     * Find matches in {block} tag source
+     *
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler
+     */
+    public function matchBlockSource(Smarty_Internal_TemplateCompilerBase $compiler)
+    {
+    }
+
+    /**
+     * Compiles code for the {$smarty.foreach.xxx} or {$smarty.section.xxx}tag
+     *
+     * @param array                                 $args      array with attributes from parser
+     * @param \Smarty_Internal_TemplateCompilerBase $compiler  compiler object
+     * @param array                                 $parameter array with compilation parameter
+     *
+     * @return string compiled code
+     * @throws \SmartyCompilerException
+     */
+    public function compileSpecialVariable($args, Smarty_Internal_TemplateCompilerBase $compiler, $parameter)
+    {
+        $tag = strtolower(trim($parameter[ 0 ], '"\''));
+        $name = isset($parameter[ 1 ]) ? $compiler->getId($parameter[ 1 ]) : false;
+        if (!$name) {
+            $compiler->trigger_template_error("missing or illegal \$smarty.{$tag} name attribute", null, true);
+        }
+        $property = isset($parameter[ 2 ]) ? strtolower($compiler->getId($parameter[ 2 ])) : false;
+        if (!$property || !in_array($property, $this->nameProperties)) {
+            $compiler->trigger_template_error("missing or illegal \$smarty.{$tag} property attribute", null, true);
+        }
+        $tagVar = "'__smarty_{$tag}_{$name}'";
+        return "(isset(\$_smarty_tpl->tpl_vars[{$tagVar}]->value['{$property}']) ? \$_smarty_tpl->tpl_vars[{$tagVar}]->value['{$property}'] : null)";
+    }
+}

Some files were not shown because too many files changed in this diff