Exemplo n.º 1
0
 /**
  * Return an instance of the mustache class.
  *
  * @since 2.9
  * @return Mustache_Engine
  */
 protected function get_mustache()
 {
     global $CFG;
     if ($this->mustache === null) {
         $themename = $this->page->theme->name;
         $themerev = theme_get_revision();
         $cachedir = make_localcache_directory("mustache/{$themerev}/{$themename}");
         $loader = new \core\output\mustache_filesystem_loader();
         $stringhelper = new \core\output\mustache_string_helper();
         $quotehelper = new \core\output\mustache_quote_helper();
         $jshelper = new \core\output\mustache_javascript_helper($this->page->requires);
         $pixhelper = new \core\output\mustache_pix_helper($this);
         // We only expose the variables that are exposed to JS templates.
         $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
         $helpers = array('config' => $safeconfig, 'str' => array($stringhelper, 'str'), 'quote' => array($quotehelper, 'quote'), 'js' => array($jshelper, 'help'), 'pix' => array($pixhelper, 'pix'));
         $this->mustache = new Mustache_Engine(array('cache' => $cachedir, 'escape' => 's', 'loader' => $loader, 'helpers' => $helpers, 'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
     }
     return $this->mustache;
 }
Exemplo n.º 2
0
 /**
  * Return an instance of the mustache class.
  *
  * @since 2.9
  * @return Mustache_Engine
  */
 protected function get_mustache()
 {
     global $CFG;
     if ($this->mustache === null) {
         require_once $CFG->dirroot . '/lib/mustache/src/Mustache/Autoloader.php';
         Mustache_Autoloader::register();
         $themename = $this->page->theme->name;
         $themerev = theme_get_revision();
         $cachedir = make_localcache_directory("mustache/{$themerev}/{$themename}");
         $loader = new \core\output\mustache_filesystem_loader();
         $stringhelper = new \core\output\mustache_string_helper();
         $jshelper = new \core\output\mustache_javascript_helper($this->page->requires);
         $pixhelper = new \core\output\mustache_pix_helper($this);
         // We only expose the variables that are exposed to JS templates.
         $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
         $helpers = array('config' => $safeconfig, 'str' => array($stringhelper, 'str'), 'js' => array($jshelper, 'help'), 'pix' => array($pixhelper, 'pix'));
         $this->mustache = new Mustache_Engine(array('cache' => $cachedir, 'escape' => 's', 'loader' => $loader, 'helpers' => $helpers));
     }
     return $this->mustache;
 }
Exemplo n.º 3
0
 public function test_localcachedir()
 {
     global $CFG;
     $this->resetAfterTest(true);
     // Test default location - can not be modified in phpunit tests because we override everything in config.php.
     $this->assertSame("{$CFG->dataroot}/localcache", $CFG->localcachedir);
     $this->setCurrentTimeStart();
     $timestampfile = "{$CFG->localcachedir}/.lastpurged";
     // Delete existing localcache directory, as this is testing first call
     // to make_localcache_directory.
     remove_dir($CFG->localcachedir, true);
     $dir = make_localcache_directory('', false);
     $this->assertSame($CFG->localcachedir, $dir);
     $this->assertFileNotExists("{$CFG->localcachedir}/.htaccess");
     $this->assertFileExists($timestampfile);
     $this->assertTimeCurrent(filemtime($timestampfile));
     $dir = make_localcache_directory('test/test', false);
     $this->assertSame("{$CFG->localcachedir}/test/test", $dir);
     // Test custom location.
     $CFG->localcachedir = "{$CFG->dataroot}/testlocalcache";
     $this->setCurrentTimeStart();
     $timestampfile = "{$CFG->localcachedir}/.lastpurged";
     $this->assertFileNotExists($timestampfile);
     $dir = make_localcache_directory('', false);
     $this->assertSame($CFG->localcachedir, $dir);
     $this->assertFileExists("{$CFG->localcachedir}/.htaccess");
     $this->assertFileExists($timestampfile);
     $this->assertTimeCurrent(filemtime($timestampfile));
     $dir = make_localcache_directory('test', false);
     $this->assertSame("{$CFG->localcachedir}/test", $dir);
     $prevtime = filemtime($timestampfile);
     $dir = make_localcache_directory('pokus', false);
     $this->assertSame("{$CFG->localcachedir}/pokus", $dir);
     $this->assertSame($prevtime, filemtime($timestampfile));
     // Test purging.
     $testfile = "{$CFG->localcachedir}/test/test.txt";
     $this->assertTrue(touch($testfile));
     $now = $this->setCurrentTimeStart();
     set_config('localcachedirpurged', $now - 2);
     purge_all_caches();
     $this->assertFileNotExists($testfile);
     $this->assertFileNotExists(dirname($testfile));
     $this->assertFileExists($timestampfile);
     $this->assertTimeCurrent(filemtime($timestampfile));
     $this->assertTimeCurrent($CFG->localcachedirpurged);
     // Simulates purge_all_caches() on another server node.
     make_localcache_directory('test', false);
     $this->assertTrue(touch($testfile));
     set_config('localcachedirpurged', $now - 1);
     $this->assertTrue(touch($timestampfile, $now - 2));
     clearstatcache();
     $this->assertSame($now - 2, filemtime($timestampfile));
     $this->setCurrentTimeStart();
     $dir = make_localcache_directory('', false);
     $this->assertSame("{$CFG->localcachedir}", $dir);
     $this->assertFileNotExists($testfile);
     $this->assertFileNotExists(dirname($testfile));
     $this->assertFileExists($timestampfile);
     $this->assertTimeCurrent(filemtime($timestampfile));
 }
Exemplo n.º 4
0
/**
 * Invalidates browser caches and cached data in temp.
 *
 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
 * {@link phpunit_util::reset_dataroot()}
 *
 * @return void
 */
function purge_all_caches()
{
    global $CFG, $DB;
    reset_text_filters_cache();
    js_reset_all_caches();
    theme_reset_all_caches();
    get_string_manager()->reset_caches();
    core_text::reset_caches();
    if (class_exists('core_plugin_manager')) {
        core_plugin_manager::reset_caches();
    }
    // Bump up cacherev field for all courses.
    try {
        increment_revision_number('course', 'cacherev', '');
    } catch (moodle_exception $e) {
        // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
    }
    $DB->reset_caches();
    cache_helper::purge_all();
    // Purge all other caches: rss, simplepie, etc.
    clearstatcache();
    remove_dir($CFG->cachedir . '', true);
    // Make sure cache dir is writable, throws exception if not.
    make_cache_directory('');
    // This is the only place where we purge local caches, we are only adding files there.
    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
    remove_dir($CFG->localcachedir, true);
    set_config('localcachedirpurged', time());
    make_localcache_directory('', true);
    \core\task\manager::clear_static_caches();
}
Exemplo n.º 5
0
/**
 * Install core moodle tables and initialize
 * @param float $version target version
 * @param bool $verbose
 * @return void, may throw exception
 */
function install_core($version, $verbose) {
    global $CFG, $DB;

    // We can not call purge_all_caches() yet, make sure the temp and cache dirs exist and are empty.
    remove_dir($CFG->cachedir.'', true);
    make_cache_directory('', true);

    remove_dir($CFG->localcachedir.'', true);
    make_localcache_directory('', true);

    remove_dir($CFG->tempdir.'', true);
    make_temp_directory('', true);

    remove_dir($CFG->dataroot.'/muc', true);
    make_writable_directory($CFG->dataroot.'/muc', true);

    try {
        core_php_time_limit::raise(600);
        print_upgrade_part_start('moodle', true, $verbose); // does not store upgrade running flag

        $DB->get_manager()->install_from_xmldb_file("$CFG->libdir/db/install.xml");
        upgrade_started();     // we want the flag to be stored in config table ;-)

        // set all core default records and default settings
        require_once("$CFG->libdir/db/install.php");
        xmldb_main_install(); // installs the capabilities too

        // store version
        upgrade_main_savepoint(true, $version, false);

        // Continue with the installation
        log_update_descriptions('moodle');
        external_update_descriptions('moodle');
        events_update_definition('moodle');
        \core\task\manager::reset_scheduled_tasks_for_component('moodle');
        message_update_providers('moodle');
        \core\message\inbound\manager::update_handlers_for_component('moodle');

        // Write default settings unconditionally
        admin_apply_default_settings(NULL, true);

        print_upgrade_part_end(null, true, $verbose);

        // Purge all caches. They're disabled but this ensures that we don't have any persistent data just in case something
        // during installation didn't use APIs.
        cache_helper::purge_all();
    } catch (exception $ex) {
        upgrade_handle_exception($ex);
    }
}
Exemplo n.º 6
0
/**
 * KSES replacement cleaning function - uses HTML Purifier.
 *
 * @param string $text The (X)HTML string to purify
 * @param array $options Array of options; currently only option supported is 'allowid' (if set,
 *   does not remove id attributes when cleaning)
 * @return string
 */
function purify_html($text, $options = array())
{
    global $CFG;
    $text = (string) $text;
    static $purifiers = array();
    static $caches = array();
    // Purifier code can change only during major version upgrade.
    $version = empty($CFG->version) ? 0 : $CFG->version;
    $cachedir = "{$CFG->localcachedir}/htmlpurifier/{$version}";
    if (!file_exists($cachedir)) {
        // Purging of caches may remove the cache dir at any time,
        // luckily file_exists() results should be cached for all existing directories.
        $purifiers = array();
        $caches = array();
        gc_collect_cycles();
        make_localcache_directory('htmlpurifier', false);
        check_dir_exists($cachedir);
    }
    $allowid = empty($options['allowid']) ? 0 : 1;
    $allowobjectembed = empty($CFG->allowobjectembed) ? 0 : 1;
    $type = 'type_' . $allowid . '_' . $allowobjectembed;
    if (!array_key_exists($type, $caches)) {
        $caches[$type] = cache::make('core', 'htmlpurifier', array('type' => $type));
    }
    $cache = $caches[$type];
    // Add revision number and all options to the text key so that it is compatible with local cluster node caches.
    $key = "|{$version}|{$allowobjectembed}|{$allowid}|{$text}";
    $filteredtext = $cache->get($key);
    if ($filteredtext === true) {
        // The filtering did not change the text last time, no need to filter anything again.
        return $text;
    } else {
        if ($filteredtext !== false) {
            return $filteredtext;
        }
    }
    if (empty($purifiers[$type])) {
        require_once $CFG->libdir . '/htmlpurifier/HTMLPurifier.safe-includes.php';
        require_once $CFG->libdir . '/htmlpurifier/locallib.php';
        $config = HTMLPurifier_Config::createDefault();
        $config->set('HTML.DefinitionID', 'moodlehtml');
        $config->set('HTML.DefinitionRev', 2);
        $config->set('Cache.SerializerPath', $cachedir);
        $config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
        $config->set('Core.NormalizeNewlines', false);
        $config->set('Core.ConvertDocumentToFragment', true);
        $config->set('Core.Encoding', 'UTF-8');
        $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
        $config->set('URI.AllowedSchemes', array('http' => true, 'https' => true, 'ftp' => true, 'irc' => true, 'nntp' => true, 'news' => true, 'rtsp' => true, 'rtmp' => true, 'teamspeak' => true, 'gopher' => true, 'mms' => true, 'mailto' => true));
        $config->set('Attr.AllowedFrameTargets', array('_blank'));
        if ($allowobjectembed) {
            $config->set('HTML.SafeObject', true);
            $config->set('Output.FlashCompat', true);
            $config->set('HTML.SafeEmbed', true);
        }
        if ($allowid) {
            $config->set('Attr.EnableID', true);
        }
        if ($def = $config->maybeGetRawHTMLDefinition()) {
            $def->addElement('nolink', 'Block', 'Flow', array());
            // Skip our filters inside.
            $def->addElement('tex', 'Inline', 'Inline', array());
            // Tex syntax, equivalent to $$xx$$.
            $def->addElement('algebra', 'Inline', 'Inline', array());
            // Algebra syntax, equivalent to @@xx@@.
            $def->addElement('lang', 'Block', 'Flow', array(), array('lang' => 'CDATA'));
            // Original multilang style - only our hacked lang attribute.
            $def->addAttribute('span', 'xxxlang', 'CDATA');
            // Current very problematic multilang.
        }
        $purifier = new HTMLPurifier($config);
        $purifiers[$type] = $purifier;
    } else {
        $purifier = $purifiers[$type];
    }
    $multilang = strpos($text, 'class="multilang"') !== false;
    $filteredtext = $text;
    if ($multilang) {
        $filteredtextregex = '/<span(\\s+lang="([a-zA-Z0-9_-]+)"|\\s+class="multilang"){2}\\s*>/';
        $filteredtext = preg_replace($filteredtextregex, '<span xxxlang="${2}">', $filteredtext);
    }
    $filteredtext = (string) $purifier->purify($filteredtext);
    if ($multilang) {
        $filteredtext = preg_replace('/<span xxxlang="([a-zA-Z0-9_-]+)">/', '<span lang="${1}" class="multilang">', $filteredtext);
    }
    if ($text === $filteredtext) {
        // No need to store the filtered text, next time we will just return unfiltered text
        // because it was not changed by purifying.
        $cache->set($key, true);
    } else {
        $cache->set($key, $filteredtext);
    }
    return $filteredtext;
}
Exemplo n.º 7
0
 /**
  * Purge dataroot directory
  * @static
  * @return void
  */
 public static function reset_dataroot()
 {
     global $CFG;
     $childclassname = self::get_framework() . '_util';
     // Do not delete automatically installed files.
     self::skip_original_data_files($childclassname);
     // Clear file status cache, before checking file_exists.
     clearstatcache();
     // Clean up the dataroot folder.
     $handle = opendir(self::get_dataroot());
     while (false !== ($item = readdir($handle))) {
         if (in_array($item, $childclassname::$datarootskiponreset)) {
             continue;
         }
         if (is_dir(self::get_dataroot() . "/{$item}")) {
             remove_dir(self::get_dataroot() . "/{$item}", false);
         } else {
             unlink(self::get_dataroot() . "/{$item}");
         }
     }
     closedir($handle);
     // Clean up the dataroot/filedir folder.
     if (file_exists(self::get_dataroot() . '/filedir')) {
         $handle = opendir(self::get_dataroot() . '/filedir');
         while (false !== ($item = readdir($handle))) {
             if (in_array('filedir/' . $item, $childclassname::$datarootskiponreset)) {
                 continue;
             }
             if (is_dir(self::get_dataroot() . "/filedir/{$item}")) {
                 remove_dir(self::get_dataroot() . "/filedir/{$item}", false);
             } else {
                 unlink(self::get_dataroot() . "/filedir/{$item}");
             }
         }
         closedir($handle);
     }
     make_temp_directory('');
     make_cache_directory('');
     make_localcache_directory('');
     // Reset the cache API so that it recreates it's required directories as well.
     cache_factory::reset();
     // Purge all data from the caches. This is required for consistency.
     // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
     // and now we will purge any other caches as well.
     cache_helper::purge_all();
 }
Exemplo n.º 8
0
if ($rev > 0 and file_exists($candidate)) {
    if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
        // we do not actually need to verify the etag value because our files
        // never change in cache because we increment the rev parameter
        js_send_unmodified(filemtime($candidate), $etag);
    }
    js_send_cached($candidate, $etag);
}
//=================================================================================
// ok, now we need to start normal moodle script, we need to load all libs and $DB
define('ABORT_AFTER_CONFIG_CANCEL', true);
define('NO_MOODLE_COOKIES', true);
// Session not used here
define('NO_UPGRADE_CHECK', true);
// Ignore upgrade check
require "{$CFG->dirroot}/lib/setup.php";
$theme = theme_config::load($themename);
$themerev = theme_get_revision();
if ($themerev <= 0 or $rev != $themerev) {
    // Do not send caching headers if they do not request current revision,
    // we do not want to pollute browser caches with outdated JS.
    js_send_uncached($theme->javascript_content($type));
}
make_localcache_directory('theme', false);
js_write_cache_file_content($candidate, core_minify::js_files($theme->javascript_files($type)));
// Verify nothing failed in cache file creation.
clearstatcache();
if (file_exists($candidate)) {
    js_send_cached($candidate, $etag);
}
js_send_uncached($theme->javascript_content($type));
Exemplo n.º 9
0
 /**
  * Return an instance of the mustache class.
  *
  * @since 2.9
  * @return Mustache_Engine
  */
 protected function get_mustache()
 {
     global $CFG;
     if ($this->mustache === null) {
         require_once $CFG->dirroot . '/lib/mustache/src/Mustache/Autoloader.php';
         Mustache_Autoloader::register();
         $themename = $this->page->theme->name;
         $themerev = theme_get_revision();
         $target = $this->target;
         $cachedir = make_localcache_directory("mustache/{$themerev}/{$themename}/{$target}");
         $loaderoptions = array();
         // Where are all the places we should look for templates?
         $suffix = $this->component;
         if ($this->subtype !== null) {
             $suffix .= '_' . $this->subtype;
         }
         // Start with an empty list.
         $loader = new Mustache_Loader_CascadingLoader(array());
         $loaderdir = $CFG->dirroot . '/theme/' . $themename . '/templates/' . $suffix;
         if (is_dir($loaderdir)) {
             $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
         }
         // Search each of the parent themes second.
         foreach ($this->page->theme->parents as $parent) {
             $loaderdir = $CFG->dirroot . '/theme/' . $parent . '/templates/' . $suffix;
             if (is_dir($loaderdir)) {
                 $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
             }
         }
         // Look in a components templates dir for a base implementation.
         $compdirectory = core_component::get_component_directory($suffix);
         if ($compdirectory) {
             $loaderdir = $compdirectory . '/templates';
             if (is_dir($loaderdir)) {
                 $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
             }
         }
         // Look in the core templates dir as a final fallback.
         $compdirectory = $CFG->libdir;
         if ($compdirectory) {
             $loaderdir = $compdirectory . '/templates';
             if (is_dir($loaderdir)) {
                 $loader->addLoader(new \core\output\mustache_filesystem_loader($loaderdir, $loaderoptions));
             }
         }
         $stringhelper = new \core\output\mustache_string_helper();
         $jshelper = new \core\output\mustache_javascript_helper($this->page->requires);
         $pixhelper = new \core\output\mustache_pix_helper($this);
         // We only expose the variables that are exposed to JS templates.
         $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
         $helpers = array('config' => $safeconfig, 'str' => array($stringhelper, 'str'), 'js' => array($jshelper, 'help'), 'pix' => array($pixhelper, 'pix'));
         $this->mustache = new Mustache_Engine(array('cache' => $cachedir, 'escape' => 's', 'loader' => $loader, 'helpers' => $helpers));
     }
     return $this->mustache;
 }
Exemplo n.º 10
0
 /**
  * Purge dataroot directory
  * @static
  * @return void
  */
 public static function reset_dataroot()
 {
     global $CFG;
     $childclassname = self::get_framework() . '_util';
     $handle = opendir($CFG->dataroot);
     while (false !== ($item = readdir($handle))) {
         if (in_array($item, $childclassname::$datarootskiponreset)) {
             continue;
         }
         if (is_dir("{$CFG->dataroot}/{$item}")) {
             remove_dir("{$CFG->dataroot}/{$item}", false);
         } else {
             unlink("{$CFG->dataroot}/{$item}");
         }
     }
     closedir($handle);
     make_temp_directory('');
     make_cache_directory('');
     make_localcache_directory('');
     // Reset the cache API so that it recreates it's required directories as well.
     cache_factory::reset();
     // Purge all data from the caches. This is required for consistency.
     // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
     // and now we will purge any other caches as well.
     cache_helper::purge_all();
 }
Exemplo n.º 11
0
/**
 * KSES replacement cleaning function - uses HTML Purifier.
 *
 * @param string $text The (X)HTML string to purify
 * @param array $options Array of options; currently only option supported is 'allowid' (if set,
 *   does not remove id attributes when cleaning)
 * @return string
 */
function purify_html($text, $options = array()) {
    global $CFG;

    $text = (string)$text;

    static $purifiers = array();
    static $caches = array();

    // Purifier code can change only during major version upgrade.
    $version = empty($CFG->version) ? 0 : $CFG->version;
    $cachedir = "$CFG->localcachedir/htmlpurifier/$version";
    if (!file_exists($cachedir)) {
        // Purging of caches may remove the cache dir at any time,
        // luckily file_exists() results should be cached for all existing directories.
        $purifiers = array();
        $caches = array();
        gc_collect_cycles();

        make_localcache_directory('htmlpurifier', false);
        check_dir_exists($cachedir);
    }

    $allowid = empty($options['allowid']) ? 0 : 1;
    $allowobjectembed = empty($CFG->allowobjectembed) ? 0 : 1;

    $type = 'type_'.$allowid.'_'.$allowobjectembed;

    if (!array_key_exists($type, $caches)) {
        $caches[$type] = cache::make('core', 'htmlpurifier', array('type' => $type));
    }
    $cache = $caches[$type];

    // Add revision number and all options to the text key so that it is compatible with local cluster node caches.
    $key = "|$version|$allowobjectembed|$allowid|$text";
    $filteredtext = $cache->get($key);

    if ($filteredtext === true) {
        // The filtering did not change the text last time, no need to filter anything again.
        return $text;
    } else if ($filteredtext !== false) {
        return $filteredtext;
    }

    if (empty($purifiers[$type])) {
        require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.safe-includes.php';
        require_once $CFG->libdir.'/htmlpurifier/locallib.php';
        $config = HTMLPurifier_Config::createDefault();

        $config->set('HTML.DefinitionID', 'moodlehtml');
        $config->set('HTML.DefinitionRev', 6);
        $config->set('Cache.SerializerPath', $cachedir);
        $config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
        $config->set('Core.NormalizeNewlines', false);
        $config->set('Core.ConvertDocumentToFragment', true);
        $config->set('Core.Encoding', 'UTF-8');
        $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
        $config->set('URI.AllowedSchemes', array(
            'http' => true,
            'https' => true,
            'ftp' => true,
            'irc' => true,
            'nntp' => true,
            'news' => true,
            'rtsp' => true,
            'rtmp' => true,
            'teamspeak' => true,
            'gopher' => true,
            'mms' => true,
            'mailto' => true
        ));
        $config->set('Attr.AllowedFrameTargets', array('_blank'));

        if ($allowobjectembed) {
            $config->set('HTML.SafeObject', true);
            $config->set('Output.FlashCompat', true);
            $config->set('HTML.SafeEmbed', true);
        }

        if ($allowid) {
            $config->set('Attr.EnableID', true);
        }

        if ($def = $config->maybeGetRawHTMLDefinition()) {
            $def->addElement('nolink', 'Block', 'Flow', array());                       // Skip our filters inside.
            $def->addElement('tex', 'Inline', 'Inline', array());                       // Tex syntax, equivalent to $$xx$$.
            $def->addElement('algebra', 'Inline', 'Inline', array());                   // Algebra syntax, equivalent to @@xx@@.
            $def->addElement('lang', 'Block', 'Flow', array(), array('lang'=>'CDATA')); // Original multilang style - only our hacked lang attribute.
            $def->addAttribute('span', 'xxxlang', 'CDATA');                             // Current very problematic multilang.

            // Media elements.
            // https://html.spec.whatwg.org/#the-video-element
            $def->addElement('video', 'Block', 'Optional: #PCDATA | Flow | source | track', 'Common', [
                'src' => 'URI',
                'crossorigin' => 'Enum#anonymous,use-credentials',
                'poster' => 'URI',
                'preload' => 'Enum#auto,metadata,none',
                'autoplay' => 'Bool',
                'playsinline' => 'Bool',
                'loop' => 'Bool',
                'muted' => 'Bool',
                'controls' => 'Bool',
                'width' => 'Length',
                'height' => 'Length',
            ]);
            // https://html.spec.whatwg.org/#the-audio-element
            $def->addElement('audio', 'Block', 'Optional: #PCDATA | Flow | source | track', 'Common', [
                'src' => 'URI',
                'crossorigin' => 'Enum#anonymous,use-credentials',
                'preload' => 'Enum#auto,metadata,none',
                'autoplay' => 'Bool',
                'loop' => 'Bool',
                'muted' => 'Bool',
                'controls' => 'Bool'
            ]);
            // https://html.spec.whatwg.org/#the-source-element
            $def->addElement('source', false, 'Empty', null, [
                'src' => 'URI',
                'type' => 'Text'
            ]);
            // https://html.spec.whatwg.org/#the-track-element
            $def->addElement('track', false, 'Empty', null, [
                'src' => 'URI',
                'kind' => 'Enum#subtitles,captions,descriptions,chapters,metadata',
                'srclang' => 'Text',
                'label' => 'Text',
                'default' => 'Bool',
            ]);

            // Use the built-in Ruby module to add annotation support.
            $def->manager->addModule(new HTMLPurifier_HTMLModule_Ruby());
        }

        $purifier = new HTMLPurifier($config);
        $purifiers[$type] = $purifier;
    } else {
        $purifier = $purifiers[$type];
    }

    $multilang = (strpos($text, 'class="multilang"') !== false);

    $filteredtext = $text;
    if ($multilang) {
        $filteredtextregex = '/<span(\s+lang="([a-zA-Z0-9_-]+)"|\s+class="multilang"){2}\s*>/';
        $filteredtext = preg_replace($filteredtextregex, '<span xxxlang="${2}">', $filteredtext);
    }
    $filteredtext = (string)$purifier->purify($filteredtext);
    if ($multilang) {
        $filteredtext = preg_replace('/<span xxxlang="([a-zA-Z0-9_-]+)">/', '<span lang="${1}" class="multilang">', $filteredtext);
    }

    if ($text === $filteredtext) {
        // No need to store the filtered text, next time we will just return unfiltered text
        // because it was not changed by purifying.
        $cache->set($key, true);
    } else {
        $cache->set($key, $filteredtext);
    }

    return $filteredtext;
}