Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core\output;
  18  
  19  /**
  20   * Unit tests for the Mustache source loader class.
  21   *
  22   * Unit tests for lib/classes/output/mustache_template_source_loader.php
  23   *
  24   * @package   core
  25   * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
  26   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  class mustache_template_source_loader_test extends \advanced_testcase {
  29      /**
  30       * Ensure that stripping comments from templates does not mutilate the template body.
  31       */
  32      public function test_strip_template_comments() {
  33  
  34          $templatebody = <<<'TBD'
  35          <h1>{{# str }} pluginname, mod_lemmings {{/ str }}</h1>
  36          <div>{{test}}</div>
  37          <div>{{{unescapedtest}}}</div>
  38          {{#lemmings}}
  39              <div>
  40                  <h2>{{name}}</h2>
  41                  {{> mod_lemmings/lemmingprofile }}
  42                  {{# pix }} t/edit, core, Edit Lemming {{/ pix }}
  43              </div>
  44          {{/lemmings}}
  45          {{^lemmings}}Sorry, no lemmings today{{/lemmings}}
  46          <div id="{{ uniqid }}-tab-container">
  47              {{# tabheader }}
  48                  <ul role="tablist" class="nav nav-tabs">
  49                      {{# iconlist }}
  50                          {{# icons }}
  51                              {{> core/pix_icon }}
  52                          {{/ icons }}
  53                      {{/ iconlist }}
  54                  </ul>
  55              {{/ tabheader }}
  56              {{# tabbody }}
  57                  <div class="tab-content">
  58                      {{# tabcontent }}
  59                          {{# tabs }}
  60                              {{> core/notification_info}}
  61                          {{/ tabs }}
  62                      {{/ tabcontent }}
  63                  </div>
  64              {{/ tabbody }}
  65          </div>
  66          {{#js}}
  67              require(['jquery','core/tabs'], function($, tabs) {
  68  
  69                  var container = $("#{{ uniqid }}-tab-container");
  70                  tabs.create(container);
  71              });
  72          {{/js}}
  73  TBD;
  74          $templatewithcomment = <<<TBC
  75          {{!
  76              This file is part of Moodle - http://moodle.org/
  77  
  78              Moodle is free software: you can redistribute it and/or modify
  79              it under the terms of the GNU General Public License as published by
  80              the Free Software Foundation, either version 3 of the License, or
  81              (at your option) any later version.
  82  
  83              Moodle is distributed in the hope that it will be useful,
  84              but WITHOUT ANY WARRANTY; without even the implied warranty of
  85              MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  86              GNU General Public License for more details.
  87  
  88              You should have received a copy of the GNU General Public License
  89              along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  90          }}
  91          {{!
  92              @template mod_lemmings/lemmings
  93  
  94              Lemmings template.
  95  
  96              The purpose of this template is to render a lot of lemmings.
  97  
  98              Classes required for JS:
  99              * none
 100  
 101              Data attributes required for JS:
 102              * none
 103  
 104              Context variables required for this template:
 105              * attributes Array of name / value pairs.
 106  
 107              Example context (json):
 108              {
 109                  "lemmings": [
 110                      { "name": "Lemmy Winks", "age" : 1, "size" : "big" },
 111                      { "name": "Rocky", "age" : 2, "size" : "small" }
 112                  ]
 113              }
 114  
 115          }}
 116          $templatebody
 117          {{!
 118              Here's some more comment text
 119              Note, there is no need to test bracketed variables inside comments as gherkin does not support that!
 120              See this issue: https://github.com/mustache/spec/issues/8
 121          }}
 122  TBC;
 123  
 124          $loader = new mustache_template_source_loader();
 125          $actual = \phpunit_util::call_internal_method(
 126              $loader,
 127              'strip_template_comments',
 128              [$templatewithcomment],
 129              \core\output\mustache_template_source_loader::class
 130          );
 131          $this->assertEquals(trim($templatebody), trim($actual));
 132      }
 133  
 134      /**
 135       * Data provider for the test_load function.
 136       */
 137      public function load_test_cases() {
 138          $cache = [
 139              'core' => [
 140                  'test' => '{{! a comment }}The rest of the template'
 141              ]
 142          ];
 143          $loader = $this->build_loader_from_static_cache($cache);
 144  
 145          return [
 146              'with comments' => [
 147                  'loader' => $loader,
 148                  'component' => 'core',
 149                  'name' => 'test',
 150                  'includecomments' => true,
 151                  'expected' => '{{! a comment }}The rest of the template'
 152              ],
 153              'without comments' => [
 154                  'loader' => $loader,
 155                  'component' => 'core',
 156                  'name' => 'test',
 157                  'includecomments' => false,
 158                  'expected' => 'The rest of the template'
 159              ],
 160          ];
 161      }
 162  
 163      /**
 164       * Test the load function.
 165       *
 166       * @dataProvider load_test_cases
 167       * @param mustache_template_source_loader $loader The loader
 168       * @param string $component The moodle component
 169       * @param string $name The template name
 170       * @param bool $includecomments Whether to strip comments
 171       * @param string $expected The expected output
 172       */
 173      public function test_load($loader, $component, $name, $includecomments, $expected) {
 174          $this->assertEquals($expected, $loader->load($component, $name, 'boost', $includecomments));
 175      }
 176  
 177      /**
 178       * Data provider for the load_with_dependencies function.
 179       */
 180      public function load_with_dependencies_test_cases() {
 181          // Create a bunch of templates that include one another in various ways. There is
 182          // multiple instances of recursive inclusions to test that the code doensn't get
 183          // stuck in an infinite loop.
 184          $foo = '{{! a comment }}{{> core/bar }}{{< test/bop }}{{/ test/bop}}{{#str}} help, core {{/str}}';
 185          $foo2 = '{{! a comment }}hello';
 186          $bar = '{{! a comment }}{{> core/baz }}';
 187          $baz = '{{! a comment }}{{#str}} hide, core {{/str}}';
 188          $bop = '{{! a comment }}{{< test/bim }}{{/ test/bim }}{{> core/foo }}';
 189          $bim = '{{! a comment }}{{< core/foo }}{{/ core/foo}}{{> test/foo }}';
 190          $foonocomment = '{{> core/bar }}{{< test/bop }}{{/ test/bop}}{{#str}} help, core {{/str}}';
 191          $foo2nocomment = 'hello';
 192          $barnocomment = '{{> core/baz }}';
 193          $baznocomment = '{{#str}} hide, core {{/str}}';
 194          $bopnocomment = '{{< test/bim }}{{/ test/bim }}{{> core/foo }}';
 195          $bimnocomment = '{{< core/foo }}{{/ core/foo}}{{> test/foo }}';
 196          $cache = [
 197              'core' => [
 198                  'foo' => $foo,
 199                  'bar' => $bar,
 200                  'baz' => $baz,
 201              ],
 202              'test' => [
 203                  'foo' => $foo2,
 204                  'bop' => $bop,
 205                  'bim' => $bim
 206              ]
 207          ];
 208          $loader = $this->build_loader_from_static_cache($cache);
 209  
 210          return [
 211              'no template includes w comments' => [
 212                  'loader' => $loader,
 213                  'component' => 'test',
 214                  'name' => 'foo',
 215                  'includecomments' => true,
 216                  'expected' => [
 217                      'templates' => [
 218                          'test' => [
 219                              'foo' => $foo2
 220                          ]
 221                      ],
 222                      'strings' => []
 223                  ]
 224              ],
 225              'no template includes w/o comments' => [
 226                  'loader' => $loader,
 227                  'component' => 'test',
 228                  'name' => 'foo',
 229                  'includecomments' => false,
 230                  'expected' => [
 231                      'templates' => [
 232                          'test' => [
 233                              'foo' => $foo2nocomment
 234                          ]
 235                      ],
 236                      'strings' => []
 237                  ]
 238              ],
 239              'no template includes with string w comments' => [
 240                  'loader' => $loader,
 241                  'component' => 'core',
 242                  'name' => 'baz',
 243                  'includecomments' => true,
 244                  'expected' => [
 245                      'templates' => [
 246                          'core' => [
 247                              'baz' => $baz
 248                          ]
 249                      ],
 250                      'strings' => [
 251                          'core' => [
 252                              'hide' => 'Hide'
 253                          ]
 254                      ]
 255                  ]
 256              ],
 257              'no template includes with string w/o comments' => [
 258                  'loader' => $loader,
 259                  'component' => 'core',
 260                  'name' => 'baz',
 261                  'includecomments' => false,
 262                  'expected' => [
 263                      'templates' => [
 264                          'core' => [
 265                              'baz' => $baznocomment
 266                          ]
 267                      ],
 268                      'strings' => [
 269                          'core' => [
 270                              'hide' => 'Hide'
 271                          ]
 272                      ]
 273                  ]
 274              ],
 275              'full with comments' => [
 276                  'loader' => $loader,
 277                  'component' => 'core',
 278                  'name' => 'foo',
 279                  'includecomments' => true,
 280                  'expected' => [
 281                      'templates' => [
 282                          'core' => [
 283                              'foo' => $foo,
 284                              'bar' => $bar,
 285                              'baz' => $baz
 286                          ],
 287                          'test' => [
 288                              'foo' => $foo2,
 289                              'bop' => $bop,
 290                              'bim' => $bim
 291                          ]
 292                      ],
 293                      'strings' => [
 294                          'core' => [
 295                              'help' => 'Help',
 296                              'hide' => 'Hide'
 297                          ]
 298                      ]
 299                  ]
 300              ],
 301              'full without comments' => [
 302                  'loader' => $loader,
 303                  'component' => 'core',
 304                  'name' => 'foo',
 305                  'includecomments' => false,
 306                  'expected' => [
 307                      'templates' => [
 308                          'core' => [
 309                              'foo' => $foonocomment,
 310                              'bar' => $barnocomment,
 311                              'baz' => $baznocomment
 312                          ],
 313                          'test' => [
 314                              'foo' => $foo2nocomment,
 315                              'bop' => $bopnocomment,
 316                              'bim' => $bimnocomment
 317                          ]
 318                      ],
 319                      'strings' => [
 320                          'core' => [
 321                              'help' => 'Help',
 322                              'hide' => 'Hide'
 323                          ]
 324                      ]
 325                  ]
 326              ]
 327          ];
 328      }
 329  
 330      /**
 331       * Test the load_with_dependencies function.
 332       *
 333       * @dataProvider load_with_dependencies_test_cases
 334       * @param mustache_template_source_loader $loader The loader
 335       * @param string $component The moodle component
 336       * @param string $name The template name
 337       * @param bool $includecomments Whether to strip comments
 338       * @param string $expected The expected output
 339       */
 340      public function test_load_with_dependencies($loader, $component, $name, $includecomments, $expected) {
 341          $actual = $loader->load_with_dependencies($component, $name, 'boost', $includecomments);
 342          $this->assertEquals($expected, $actual);
 343      }
 344      /**
 345       * Data provider for the test_load function.
 346       */
 347      public function scan_template_source_for_dependencies_test_cases() {
 348          $foo = '{{! a comment }}{{> core/bar }}{{< test/bop }}{{/ test/bop}}{{#str}} help, core {{/str}}';
 349          $bar = '{{! a comment }}{{> core/baz }}';
 350          $baz = '{{! a comment }}{{#str}} hide, core {{/str}}';
 351          $bop = '{{! a comment }}hello';
 352          $multiline1 = <<<TEMPLATE
 353  {{! a comment }}{{#str}} authorreplyingprivatelytoauthor,
 354  mod_forum {{/str}}
 355  TEMPLATE;
 356          $multiline2 = <<<TEMPLATE
 357  {{! a comment }}{{#str}}
 358  authorreplyingprivatelytoauthor,
 359  mod_forum {{/str}}
 360  TEMPLATE;
 361          $multiline3 = <<<TEMPLATE
 362  {{! a comment }}{{#str}}
 363  authorreplyingprivatelytoauthor,
 364  mod_forum
 365  {{/str}}
 366  TEMPLATE;
 367          $multiline4 = <<<TEMPLATE
 368  {{! a comment }}{{#str}}
 369  authorreplyingprivatelytoauthor, mod_forum
 370  {{/str}}
 371  TEMPLATE;
 372          $multiline5 = <<<TEMPLATE
 373  {{! a comment }}{{#str}}
 374  hide
 375  {{/str}}
 376  TEMPLATE;
 377  
 378          $cache = [
 379              'core' => [
 380                  'foo' => $foo,
 381                  'bar' => $bar,
 382                  'baz' => $baz,
 383                  'bop' => $bop,
 384                  'multiline1' => $multiline1,
 385                  'multiline2' => $multiline2,
 386                  'multiline3' => $multiline3,
 387                  'multiline4' => $multiline4,
 388                  'multiline5' => $multiline5,
 389              ]
 390          ];
 391          $loader = $this->build_loader_from_static_cache($cache);
 392  
 393          return [
 394              'single template include' => [
 395                  'loader' => $loader,
 396                  'source' => $bar,
 397                  'expected' => [
 398                      'templates' => [
 399                          'core' => ['baz']
 400                      ],
 401                      'strings' => []
 402                  ]
 403              ],
 404              'single string include' => [
 405                  'loader' => $loader,
 406                  'source' => $baz,
 407                  'expected' => [
 408                      'templates' => [],
 409                      'strings' => [
 410                          'core' => ['hide']
 411                      ]
 412                  ]
 413              ],
 414              'no include' => [
 415                  'loader' => $loader,
 416                  'source' => $bop,
 417                  'expected' => [
 418                      'templates' => [],
 419                      'strings' => []
 420                  ]
 421              ],
 422              'all include' => [
 423                  'loader' => $loader,
 424                  'source' => $foo,
 425                  'expected' => [
 426                      'templates' => [
 427                          'core' => ['bar'],
 428                          'test' => ['bop']
 429                      ],
 430                      'strings' => [
 431                          'core' => ['help']
 432                      ]
 433                  ]
 434              ],
 435              'string: component on new line' => [
 436                  'loader' => $loader,
 437                  'source' => $multiline1,
 438                  'expected' => [
 439                      'templates' => [],
 440                      'strings' => [
 441                          'mod_forum' => ['authorreplyingprivatelytoauthor']
 442                      ]
 443                  ]
 444              ],
 445              'string: identifier on own line' => [
 446                  'loader' => $loader,
 447                  'source' => $multiline2,
 448                  'expected' => [
 449                      'templates' => [],
 450                      'strings' => [
 451                          'mod_forum' => ['authorreplyingprivatelytoauthor']
 452                      ]
 453                  ]
 454              ],
 455              'string: all parts on new lines' => [
 456                  'loader' => $loader,
 457                  'source' => $multiline3,
 458                  'expected' => [
 459                      'templates' => [],
 460                      'strings' => [
 461                          'mod_forum' => ['authorreplyingprivatelytoauthor']
 462                      ]
 463                  ]
 464              ],
 465              'string: id and component on own line' => [
 466                  'loader' => $loader,
 467                  'source' => $multiline4,
 468                  'expected' => [
 469                      'templates' => [],
 470                      'strings' => [
 471                          'mod_forum' => ['authorreplyingprivatelytoauthor']
 472                      ]
 473                  ]
 474              ],
 475              'string: no component' => [
 476                  'loader' => $loader,
 477                  'source' => $multiline5,
 478                  'expected' => [
 479                      'templates' => [],
 480                      'strings' => [
 481                          'core' => ['hide']
 482                      ]
 483                  ]
 484              ],
 485          ];
 486      }
 487  
 488      /**
 489       * Test the scan_template_source_for_dependencies function.
 490       *
 491       * @dataProvider scan_template_source_for_dependencies_test_cases()
 492       * @param mustache_template_source_loader $loader The loader
 493       * @param string $source The template to test
 494       * @param string $expected The expected output
 495       */
 496      public function test_scan_template_source_for_dependencies($loader, $source, $expected) {
 497          $actual = \phpunit_util::call_internal_method(
 498              $loader,
 499              'scan_template_source_for_dependencies',
 500              [$source],
 501              \core\output\mustache_template_source_loader::class
 502          );
 503          $this->assertEquals($expected, $actual);
 504      }
 505  
 506      /**
 507       * Create an instance of mustache_template_source_loader which loads its templates
 508       * from the given cache rather than disk.
 509       *
 510       * @param array $cache A cache of templates
 511       * @return mustache_template_source_loader
 512       */
 513      private function build_loader_from_static_cache(array $cache) : mustache_template_source_loader {
 514          return new mustache_template_source_loader(function($component, $name, $themename) use ($cache) {
 515              return $cache[$component][$name];
 516          });
 517      }
 518  }