yf_assets class

Fileplugins/assets/classes/yf_assets.class.php
Use_class('assets')

Public properties

A site sets them in its config with $PROJECT_CONF['assets']['NAME'] = ...;

PropertyDefaultDescription
$ADD_IS_DIRECT_OUT false
$ALLOW_GET_FROM_BOWER false
$ALLOW_GET_FROM_CDN false
$ALLOW_GET_FROM_GITHUB false
$ALLOW_URL_CONTROL true
$ASSETS_GET_PRIORITY ['github', 'cdn', 'versions']
$BOWER_COMPONENTS_DIR_TPL '{app_path}/bower_components/{name}/'
$BOWER_TARGET_DIR_TPL '{project_path}/uploads/bower_components/{name}/{version}/'
$CACHE_CHECK_SOURCE_MTIME false
$CACHE_CSS_KEEP_DIRS false
$CACHE_DIR_TPL '{project_path}/templates/{main_type}/cache/{lang}/{asset_'...
$CACHE_IMAGES_DATA_URI_MAX_SIZE 5000
$CACHE_IMAGES_USE_DATA_URI false
$CACHE_INLINE_ALLOW true
$CACHE_INLINE_MIN_SIZE 1000
$CACHE_OUT_ADD_MTIME true
$CACHE_TTL 86400
$CDN_TARGET_DIR_TPL '{project_path}/uploads/cdn_cache/{name}/{version}/'
$COMBINE false
$COMBINED_CHECK_SOURCES false
$COMBINED_CONFIG null
$COMBINED_LOCK false
$COMBINED_PREVIOUS_GRACE 3600
$COMBINED_VERSION_TPL '{year}{month}'
$FILE_ASSETS_DATA_URI_MAX_SIZE 5000
$FILE_ASSETS_USE_DATA_URI false
$FILTERS_AUTO_APPLY false
$FORCE_LOCAL_STORAGE false
$FORCE_LOCAL_TTL 86400000
$GITHUB_RAW_URL_TPL 'https://raw.githubusercontent.com/{repo}/{branch}/'
$GITHUB_TARGET_DIR_TPL '{project_path}/uploads/github_cache/{repo}/{version}/'
$INLINE_ASSETS_USE_DATA_URI false
$MAIN_TPL_CSS 'style_css'
$MAIN_TPL_JS 'script_js'
$OUT_ADD_ASSET_NAME true
$SHORTEN_LOCAL_URL true
$SRI false
$THEMES_TARGET_DIR_TPL '{project_path}/uploads/themes_cache/'
$URL_CONTROL_REQUIRE_DEBUG false
$URL_FILE_CACHE_DIR ''
$URL_FILE_CACHE_TTL 3600
$URL_REWRITES []
$URL_TIMEOUT 30
$USE_CACHE false
$USE_REQUIRE_JS false
$VENDOR_DIR ''
$VENDOR_LOCAL false
$VENDOR_TARGET_DIR_TPL '{project_path}/uploads/vendor_cache/'
$_abandoned_noticed []
$_assets_added []
$_autoload_registered null
$_avail_filters null
$_bundles_added []
$_cache_cdn []
$_cache_date null
$_cache_github []
$_cache_html5fw null
$_cache_language null
$_cache_themes []
$_cache_vendor []
$_cache_vendor_manifest null
$_ext_mismatch_noticed []
$_init_css_done null
$_init_js_done null
$_override []
$_theme_copy_warned false
$_time null
$_unknown_filters_noticed []
$_url_rewrites_bad
array
[] URL_REWRITES keys that are not valid regexes, reported once
$_url_rewrites_ok []
$already_required []
$inherit_asset_types_map ['js' => ['jquery']]
$supported_asset_types
['jquery', 'js', 'css', 'less', 'sass', 'coffee', ... (8 items)]
array (
  0 => 'jquery',
  1 => 'js',
  2 => 'css',
  3 => 'less',
  4 => 'sass',
  5 => 'coffee',
  6 => 'bundle',
  7 => 'asset',
)
$supported_content_types ['asset', 'url', 'file', 'inline']
$supported_out_types ['js', 'css']

Public methods 102

add

public function add($content, $asset_type = 'bundle', $content_type_hint = 'auto', $params = [])

Add asset item into current workflow.

$content: string/array
$asset_type: = bundle|asset|js|jquery|css (img and font throw)
$content_type_hint: = auto|asset|url|file|inline

    public function add($content, $asset_type = 'bundle', $content_type_hint = 'auto', $params = [])
    {
        $trace = DEBUG_MODE ? main()->trace_string() : '';
        if ( ! is_string($content) && is_callable($content)) {
            $content = $content($params, $this);
        }
        if ($asset_type === 'js' && ! $this->_init_js_done) {
            $this->init_js();
        } elseif ($asset_type === 'css' && ! $this->_init_css_done) {
            $this->init_css();
        }
        if (is_array($content_type_hint)) {
            $params = (array) $params + $content_type_hint;
            $content_type_hint = $params['type'] ?? null;
        }
        if ( ! $asset_type || $asset_type === 'asset') {
            $asset_type = 'bundle';
        }
        if ( ! in_array($asset_type, $this->supported_asset_types)) {
            throw new Exception('Assets add(): unsupported asset type: ' . $asset_type);
        }
        if (is_array($content) && isset($content['content'])) {
            if (is_array($content['params'] ?? null)) {
                $params += $content['params'];
            }
            $content = $content['content'];
        }
        if ($asset_type === 'jquery') {
            return $this->jquery($content, $params);
        }
        $DIRECT_OUT = isset($params['direct_out']) ? $params['direct_out'] : $this->ADD_IS_DIRECT_OUT;
        if (empty($content)) {
            return $DIRECT_OUT ? $this->show($asset_type) : $this;
        }
        if ( ! is_array($content)) {
            $content = [$content];
        }
        if ( ! $content_type_hint) {
            $content_type_hint = 'auto';
        }
        if (is_array($content_type_hint)) {
            $params = (array) $params + $content_type_hint;
            $content_type_hint = $params['type'] ?? null;
        }
        foreach ((array) $content as $_content) {
            $_params = $params;
            if ( ! is_string($_content) && is_callable($_content)) {
                $_content = $_content($_params, $this);
            }
            if (is_array($_content) && isset($_content['content'])) {
                if (is_array($_content['params'] ?? null)) {
                    $_params += $_content['params'];
                }
                $_content = $_content['content'];
            }
            if (is_array($_content)) {
                $this->add($_content, $asset_type, $content_type_hint, $_params);
                continue;
            }
            $_content = trim($_content);
            if ( ! $_content) {
                continue;
            }
            $md5 = md5($_content);
            if ($this->_is_content_added($asset_type, $md5)) {
                continue;
            }
            if ($asset_type === 'bundle') {
                $this->_add_bundle($_content, $_params);
                continue;
            }
            $content_type = '';
            if (in_array($content_type_hint, $this->supported_content_types)) {
                $content_type = $content_type_hint;
            } else {
                $content_type = $this->detect_content_type($asset_type, $_content);
            }
            if ($content_type === 'asset') {
                $this->_add_asset($_content, $asset_type, $_params);
            } elseif ($content_type === 'url') {
                $this->set_content($asset_type, $md5, 'url', $_content, $_params);
            } elseif ($content_type === 'file') {
                if (file_exists($_content)) {
                    $str = file_get_contents($_content);
                    if (strlen($str)) {
                        $this->set_content($asset_type, $md5, 'file', $_content, $_params);
                    }
                }
            } elseif ($content_type === 'inline') {
                $this->set_content($asset_type, $md5, 'inline', $_content, $_params);
            }
            if (DEBUG_MODE) {
                debug('assets_add[]', [
                    'asset_type' => $asset_type,
                    'content_type' => $content_type,
                    'md5' => $md5,
                    'content' => $_content,
                    'is_added' => $this->get_content($asset_type, $md5) !== null,
                    'preview' => '',
                    'params' => $_params,
                    'trace' => $trace,
                ]);
            }
        }
        return $DIRECT_OUT ? $this->show_css() . $this->show_js() : $this;
    }

add_asset

public function add_asset($content, $asset_type, $params = [])

Shortcut.

    public function add_asset($content, $asset_type, $params = [])
    {
        return $this->add($content, $asset_type, 'asset', $params);
    }

add_coffee

public function add_coffee($content, $content_type_hint = 'auto', $params = [])

Removed in AS-4: the SCSS / LESS / CoffeeScript compilers it needed are abandoned. Kept so that a site call does not fatal.

@deprecated
    public function add_coffee($content, $content_type_hint = 'auto', $params = [])
    {
        return $this->_removed_content_compiler('add_coffee');
    }

add_css

public function add_css($content, $content_type_hint = 'auto', $params = [])

Shortcut.

    public function add_css($content, $content_type_hint = 'auto', $params = [])
    {
        return $this->add($content, 'css', $content_type_hint, $params);
    }

add_file

public function add_file($content, $asset_type, $params = [])

Shortcut.

    public function add_file($content, $asset_type, $params = [])
    {
        return $this->add($content, $asset_type, 'file', $params);
    }

add_inline

public function add_inline($content, $asset_type, $params = [])

Shortcut.

    public function add_inline($content, $asset_type, $params = [])
    {
        return $this->add($content, $asset_type, 'inline', $params);
    }

add_js

public function add_js($content, $content_type_hint = 'auto', $params = [])

Shortcut.

    public function add_js($content, $content_type_hint = 'auto', $params = [])
    {
        return $this->add($content, 'js', $content_type_hint, $params);
    }

add_less

public function add_less($content, $content_type_hint = 'auto', $params = [])

Removed in AS-4: the SCSS / LESS / CoffeeScript compilers it needed are abandoned. Kept so that a site call does not fatal.

@deprecated
    public function add_less($content, $content_type_hint = 'auto', $params = [])
    {
        return $this->_removed_content_compiler('add_less');
    }

add_sass

public function add_sass($content, $content_type_hint = 'auto', $params = [])

Removed in AS-4: the SCSS / LESS / CoffeeScript compilers it needed are abandoned. Kept so that a site call does not fatal.

@deprecated
    public function add_sass($content, $content_type_hint = 'auto', $params = [])
    {
        return $this->_removed_content_compiler('add_sass');
    }

add_url

public function add_url($content, $asset_type, $params = [])

Shortcut.

    public function add_url($content, $asset_type, $params = [])
    {
        return $this->add($content, $asset_type, 'url', $params);
    }
public function bundle_register($name, $config)

Register new bundle or replace existing by name on-the-fly. $config is an array or a closure returning one (as descriptor files do).

    public function bundle_register($name, $config)
    {
        if ( ! is_string($config) && is_callable($config)) {
            $config = $config($this);
        }
        if ( ! is_array($config)) {
            throw new InvalidArgumentException(__CLASS__ . ': bundle "' . $name . '" must be an array or a closure returning an array');
        }
        $this->assets[$name] = $config;
        return $this;
    }

clean_all

public function clean_all()
    public function clean_all()
    {
        $this->content = [];
        $this->filters = [];
        $this->_assets_added = [];
        $this->_bundles_added = [];
    }

clean_content

public function clean_content($asset_type)

Clean content for given asset type.

    public function clean_content($asset_type)
    {
        if ( ! $this->ADD_IS_DIRECT_OUT) {
            $this->already_required[$asset_type] = [];
        }
        $this->content[$asset_type] = [];
        $this->_assets_added[$asset_type] = [];
        $this->_bundles_added = [];
        return [];
    }
public function detect_content_type($asset_type, $content = '')

Auto-detection on content type.

    public function detect_content_type($asset_type, $content = '')
    {
        $content = trim($content);
        $type = false;
        if (isset($this->assets[$content])) {
            $type = 'asset';
        } elseif ($asset_type === 'js') {
            if (preg_match('~^(http://|https://|//)[a-z0-9]+~ims', $content) && ! preg_match('~\s+~ims', $content)) {
                $type = 'url';
            } elseif (preg_match('~^/[a-z0-9\./_-]+\.js$~ims', $content) && file_exists($content)) {
                $type = 'file';
            } else {
                $type = 'inline';
            }
        } elseif ($asset_type === 'css') {
            if (preg_match('~^(http://|https://|//)[a-z0-9]+~ims', $content) && ! preg_match('~\s+~ims', $content)) {
                $type = 'url';
            } elseif (preg_match('~^/[a-z0-9\./_-]+\.css$~ims', $content) && file_exists($content)) {
                $type = 'file';
            } else {
                $type = 'inline';
            }
        } elseif ( ! in_array($asset_type, ['js', 'css'])) {
            $type = 'inline';
        }
        return $type;
    }

filters_add

public function filters_add($asset_type, $callback, $params = [])

Add filters to processing chain, both custom and built-in supported. They run on inline content only, when filters_process_js() / filters_process_css() is called, or in show() when FILTERS_AUTO_APPLY is on.

    public function filters_add($asset_type, $callback, $params = [])
    {
        if ( ! $asset_type) {
            throw new Exception('Assets: ' . __FUNCTION__ . ' missing asset_type');
            return $this;
        }
        if (is_array($callback)) {
            $func = __FUNCTION__;
            foreach ($callback as $k => $v) {
                $this->$func($asset_type, $v, $params);
            }
            return $this;
        }
        $this->filters[$asset_type][] = [
            'callback' => $callback,
            'params' => $params,
        ];
        return $this;
    }
public function filters_add_css($callback, $params = [])

Shortcut for filters_add with css asset.

    public function filters_add_css($callback, $params = [])
    {
        return $this->filters_add('css', $callback, $params);
    }
public function filters_add_js($callback, $params = [])

Shortcut for filters_add with js asset.

    public function filters_add_js($callback, $params = [])
    {
        return $this->filters_add('js', $callback, $params);
    }

filters_clean

public function filters_clean($asset_type = '')

Clean list of added filters (applied by filters_process_*(), and by show() when FILTERS_AUTO_APPLY is on).

    public function filters_clean($asset_type = '')
    {
        if ($asset_type) {
            $this->filters[$asset_type] = [];
        } else {
            $this->filters = [];
        }
    }
public function filters_get_added($asset_type)

Get list of filters, added to procesing chain, both custom and built-in.

    public function filters_get_added($asset_type)
    {
        if ( ! $asset_type) {
            return false;
        }
        return $this->filters[$asset_type] ?? [];
    }
public function filters_get_avail()

Get list of built-in filters.

    public function filters_get_avail()
    {
        if (isset($this->_avail_filters)) {
            return $this->_avail_filters;
        }
        $suffix = '.class.php';
        $prefix = 'assets_filter_';
        $prefix2 = YF_PREFIX;
        $slen = strlen($suffix);
        $plen = strlen($prefix);
        $plen2 = strlen($prefix2);
        $names = [];

        $patterns = [
            YF_PATH . 'classes/assets/*' . $prefix . '*' . $suffix,
            YF_PATH . 'plugins/*/classes/assets/*' . $prefix . '*' . $suffix,
            YF_PATH . 'classes/assets_filters/*' . $prefix . '*' . $suffix,
            YF_PATH . 'plugins/*/classes/assets_filters/*' . $prefix . '*' . $suffix,
            PROJECT_PATH . 'classes/assets/*' . $prefix . '*' . $suffix,
            PROJECT_PATH . 'plugins/*/classes/assets/*' . $prefix . '*' . $suffix,
            PROJECT_PATH . 'classes/assets_filters/*' . $prefix . '*' . $suffix,
            PROJECT_PATH . 'plugins/*/classes/assets_filters/*' . $prefix . '*' . $suffix,
            APP_PATH . 'classes/assets/*' . $prefix . '*' . $suffix,
            APP_PATH . 'plugins/*/classes/assets/*' . $prefix . '*' . $suffix,
            APP_PATH . 'classes/assets_filters/*' . $prefix . '*' . $suffix,
            APP_PATH . 'plugins/*/classes/assets_filters/*' . $prefix . '*' . $suffix,
        ];

        if (is_site_path()) {
            $patterns[] = SITE_PATH . 'classes/assets/*' . $prefix . '*' . $suffix;
            $patterns[] = SITE_PATH . 'plugins/*/classes/assets/*' . $prefix . '*' . $suffix;
            $patterns[] = SITE_PATH . 'classes/assets_filters/*' . $prefix . '*' . $suffix;
            $patterns[] = SITE_PATH . 'plugins/*/classes/assets_filters/*' . $prefix . '*' . $suffix;
        }

        foreach ($patterns as $glob) {
            foreach (glob($glob) as $path) {
                $name = substr(basename($path), 0, -$slen);
                if (substr($name, 0, $plen2) === $prefix2) {
                    $name = substr($name, $plen2);
                }
                if (substr($name, 0, $plen) === $prefix) {
                    $name = substr($name, $plen);
                }
                $names[$name] = $path;
            }
        }
        $this->_avail_filters = $names;
        return $names;
    }
public function filters_process_added($asset_type, $params = [])

Apply added filters to gathered content of the given asset type.

    public function filters_process_added($asset_type, $params = [])
    {
        if ( ! $asset_type) {
            return false;
        }
        $filters = $this->filters_get_added($asset_type);
        if ( ! $filters) {
            return $this;
        }
        $content = $this->get_content($asset_type);
        if ( ! $content) {
            return $this;
        }
        foreach ($content as $md5 => $info) {
            // TODO: support for other content types
            if ($info['content_type'] !== 'inline') {
                continue;
            }
            $_content = $info['content'];
            $processed = $this->filters_process_input($_content, $filters, $params);
            if ($_content !== $processed) {
                $this->content[$asset_type][$md5]['content'] = $processed;
            }
        }
        return $this;
    }
public function filters_process_css($params = [])

Shortcut for filters_content_process with css asset.

    public function filters_process_css($params = [])
    {
        return $this->filters_process_added('css', $params);
    }
public function filters_process_input($in, $filters = [], $params = [])

Apply filters from names array to input string.

    public function filters_process_input($in, $filters = [], $params = [])
    {
        if (is_array($in)) {
            $out = [];
            $func = __FUNCTION__;
            foreach ($in as $k => $v) {
                $out[$k] = $this->$func($v, $filters, $params);
            }
            return $out;
        }
        $this->_autoload_libs();

        if ( ! is_array($filters)) {
            $filters = [$filters];
        }
        $out = $in;
        $avail_filters = $this->filters_get_avail();
        foreach ($filters as $filter) {
            $_params = [];
            if (is_array($filter)) {
                $_params = $filter['params'] ?? [];
                $filter = $filter['callback'];
            }
            if ( ! $filter) {
                continue;
            }
            $result = null;
            if ( ! is_string($filter) && is_callable($filter)) {
                $result = $filter($out, $params, $this);
            } elseif (is_string($filter) && isset($avail_filters[$filter])) {
                $result = _class('assets_filter_' . $filter, 'classes/assets/')->apply($out, $params);
            } elseif (is_string($filter) && ! isset($this->_unknown_filters_noticed[$filter])) {
                $this->_unknown_filters_noticed[$filter] = true;
                trigger_error('Assets: filter "' . $filter . '" does not exist (the stub filters were removed in AS-4), content kept unchanged', E_USER_NOTICE);
            }
            // A filter returning null must not wipe the content
            if (null !== $result) {
                $out = $result;
            }
        }
        return $out;
    }
public function filters_process_js($params = [])

Shortcut for filters_content_process with js asset.

    public function filters_process_js($params = [])
    {
        return $this->filters_process_added('js', $params);
    }
public function find_asset_type_for_module($asset_type, $module = '')

Search for assets for current module in several places, where it can be stored.

    public function find_asset_type_for_module($asset_type, $module = '')
    {
        if ( ! $module) {
            $module = $_GET['object'] ?? '';
        }
        $ext = '.' . $asset_type;
        $path = $module . '/' . $module . $ext;
        $paths = [
            'yf_admin' => MAIN_TYPE_ADMIN ? YF_PATH . 'templates/admin/' . $path : '',
            'yf_user' => YF_PATH . 'templates/user/' . $path,
            'yf_plugins_admin' => MAIN_TYPE_ADMIN ? YF_PATH . 'plugins/' . $module . '/templates/admin/' . $path : '',
            'yf_plugins_user' => YF_PATH . 'plugins/' . $module . '/templates/user/' . $path,
            'project_admin' => MAIN_TYPE_ADMIN ? PROJECT_PATH . 'templates/admin/' . $path : '',
            'project_user' => PROJECT_PATH . 'templates/user/' . $path,
            'site_user' => is_site_path() ? SITE_PATH . 'templates/user/' . $path : '',
        ];
        $found = '';
        foreach (array_reverse($paths, true) as $path) {
            if ( ! strlen($path)) {
                continue;
            }
            if (file_exists($path)) {
                $found = $path;
                break;
            }
        }
        return $found;
    }
public function find_version_best_match($version = '', $avail_versions = [])

Versions idea from https://getcomposer.org/doc/01-basic-usage.md#package-versions In the previous example we were requiring version 1.0.* of monolog. This means any version in the 1.0 development branch. It would match 1.0.0, 1.0.2 or 1.0.20. Version constraints can be specified in a few different ways. Exact version 1.0.2 You can specify the exact version of a package. Range >=1.0 >=1.0,<2.0 >=1.0,<1.1 | >=1.2 By using comparison operators you can specify ranges of valid versions. Valid operators are >, >=, <, <=, !=. You can define multiple ranges. Ranges separated by a comma (,) will be treated as a logical AND. A pipe (|) will be treated as a logical OR. AND has higher precedence than OR. Wildcard 1.0.* You can specify a pattern with a * wildcard. 1.0.* is the equivalent of >=1.0,<1.1. Tilde Operator ~1.2 Very useful for projects that follow semantic versioning. ~1.2 is equivalent to >=1.2,<2.0. For more details, read the next section below.

    public function find_version_best_match($version = '', $avail_versions = [])
    {
        if (empty($avail_versions)) {
            return null;
        }
        if ( ! $version) {
            return current(array_slice($avail_versions, -1, 1, true));
        }
        $version = trim((string) $version);
        foreach ($avail_versions as $v) {
            if ((string) $v === $version) {
                return $v;
            }
        }
        if ($version === 'latest') {
            $version = '*';
        }
        // yf_assets works without the lock class: then only exact versions resolve
        $lock = _class('services_lock');
        if ( ! method_exists($lock, 'satisfies')) {
            return null;
        }
        $best = null;
        $best_norm = '';
        foreach ($avail_versions as $v) {
            try {
                $norm = $lock->normalize_version($v);
                if ( ! $lock->satisfies($v, $version)) {
                    continue;
                }
            } catch (Throwable $e) {
                // A key like "master" or a pattern that is not a constraint matches nothing
                continue;
            }
            if ($best === null || version_compare($norm, $best_norm, '>')) {
                $best = $v;
                $best_norm = $norm;
            }
        }
        return $best;
    }

get_asset

public function get_asset($name, $asset_type, $version = '')

Return named asset, also can return specific version.

    public function get_asset($name, $asset_type, $version = '')
    {
        if ( ! $name) {
            return null;
        }
        strpos($name, '#') !== false && list($name, $version) = explode('#', $name);
        $asset_data = $this->get_asset_details($name);
        if ( ! $asset_data) {
            return null;
        }
        if ( ! is_string($asset_data) && is_callable($asset_data)) {
            $asset_data = $asset_data($this);
        }
        foreach ((array) $this->ASSETS_GET_PRIORITY as $method) {
            if ($method == 'github') {
                $data = ($asset_data[$method] ?? null) ?: [];
                $_files = $data[$asset_type] ?? null;
                if ($this->ALLOW_GET_FROM_GITHUB && $_files) {
                    $_name = ($data['name'] ?? null) ?: $name;
                    strpos($_name, '#') !== false && list($_name, $_version) = explode('#', $_name);
                    $_version ??= $version;
                    $_version ??= $data['version'] ?? null;
                    $_version ??= 'master';
                    return $this->get_asset_from_github($_name, $_version, $data, $asset_type);
                }
            } elseif ($method == 'cdn' && $this->ALLOW_GET_FROM_CDN && isset($asset_data[$method]) && isset($asset_data[$method][$asset_type])) {
                $data = $asset_data[$method];
                $_version = $version ?: ($data['version'] ?? null) ?: 'master';
                return $this->get_asset_from_cdn($name, $_version, $data, $asset_type);
            } elseif ($method == 'versions') {
                if ( ! is_array($asset_data[$method] ?? null)) {
                    return null;
                }
                $data = ($asset_data[$method] ?? null) ?: [];
                $version = $this->find_version_best_match($version, array_keys($data));
                if ( ! $version || ! isset($data[$version])) {
                    return null;
                }
                $version_info = $data[$version];
                $content = $version_info[$asset_type] ?? null;
                return $this->_process_versions_content($content, $asset_type);
            }
        }
    }
public function get_asset_details($name)
    public function get_asset_details($name)
    {
        // If overall asset is callable - call it and save result to prevent multiple calls
        if ( ! is_string($this->assets[$name] ?? null) && is_callable($this->assets[$name] ?? null)) {
            $func = $this->assets[$name];
            $this->assets[$name] = $func($this);
        }
        return $this->assets[$name] ?? null;
    }
public function get_asset_from_cdn($name, $version = 'master', $asset_data = [], $asset_type = '')
    public function get_asset_from_cdn($name, $version = 'master', $asset_data = [], $asset_type = '')
    {
        if ( ! $name || ! $asset_data || ! isset($asset_data[$asset_type]) || empty($asset_data['url'])) {
            return false;
        }
        $cdn_url_tpl = $asset_data['url'];
        $files = $asset_data[$asset_type];
        ! $version && $version = 'master';
        $cache = &$this->_cache_cdn[$asset_type];
        if (isset($cache[$name])) {
            return $cache[$name];
        }
        $replace = $this->_get_asset_tpl_replace($name, $version);
        $target_dir = str_replace(['///', '//'], '/', str_replace(array_keys($replace), array_values($replace), $this->CDN_TARGET_DIR_TPL));
        // Please do not replace 2 slashes inside url
        $cdn_url = str_replace(array_keys($replace), array_values($replace), $cdn_url_tpl);
        $web = [];
        foreach ((array) $files as $from => $to) {
            if (is_numeric($from)) {
                $from = $to;
            }
            $to_path = rtrim($target_dir, '/') . '/' . ltrim($to, '/');
            if ( ! file_exists($to_path)) {
                $url = rtrim($cdn_url, '/') . '/' . ltrim($from, '/');
                $data = $this->_url_get_contents($url);
                if (strlen($data)) {
                    $dir_to = dirname($to_path);
                    ! file_exists($dir_to) && mkdir($dir_to, 0755, true);
                    $this->_atomic_write($to_path, $data);
                }
            }
            $web[] = MEDIA_PATH . substr($to_path, strlen(PROJECT_PATH));
        }
        $cache[$name] = $web;
        return $cache[$name];
    }
public function get_asset_from_github($name, $version = 'master', $asset_data = [], $asset_type = '')
    public function get_asset_from_github($name, $version = 'master', $asset_data = [], $asset_type = '')
    {
        if ( ! $name || ! $asset_data || ! isset($asset_data[$asset_type])) {
            return false;
        }
        $files = $asset_data[$asset_type];
        ! $version && $version = 'master';
        $cache = &$this->_cache_github[$asset_type];
        if (isset($cache[$name])) {
            return $cache[$name];
        }
        $replace = $this->_get_asset_tpl_replace($name, $version);
        $target_dir = str_replace(['///', '//'], '/', str_replace(array_keys($replace), array_values($replace), $this->GITHUB_TARGET_DIR_TPL));
        // Please do not replace 2 slashes inside url
        $github_url = str_replace(array_keys($replace), array_values($replace), $this->GITHUB_RAW_URL_TPL);
        $web = [];
        foreach ((array) $files as $from => $to) {
            if (is_numeric($from)) {
                $from = $to;
            }
            $to_path = rtrim($target_dir, '/') . '/' . ltrim($to, '/');
            if ( ! file_exists($to_path)) {
                $url = rtrim($github_url, '/') . '/' . ltrim($from, '/');
                $data = $this->_url_get_contents($url);
                if (strlen($data)) {
                    $dir_to = dirname($to_path);
                    ! file_exists($dir_to) && mkdir($dir_to, 0755, true);
                    $this->_atomic_write($to_path, $data);
                }
            }
            $web[] = MEDIA_PATH . substr($to_path, strlen(PROJECT_PATH));
        }
        $cache[$name] = $web;
        return $cache[$name];
    }

get_cache

public function get_cache($out_type, $md5, $data = [])
    public function get_cache($out_type, $md5, $data = [])
    {
        $cache_path = $this->_cache_path($out_type, $md5, $data);
        if (file_exists($cache_path) && ! $this->_cache_expired($cache_path)) {
            if ($this->CACHE_CHECK_SOURCE_MTIME && ! $this->_cache_source_is_current($cache_path, $data)) {
                return false;
            }
            return $cache_path;
        }
        return false;
    }

get_content

public function get_content($asset_type, $params = [])

Return content for given asset type, optionally only for md5 of it.

    public function get_content($asset_type, $params = [])
    {
        $md5 = (is_string($params) && strlen($params) === 32) ? $params : (isset($params['md5']) ? $params['md5'] : '');
        if ( ! isset($this->content[$asset_type])) {
            $this->content[$asset_type] = [];
        }
        return $md5 ? ($this->content[$asset_type][$md5] ?? null) : $this->content[$asset_type];
    }
public function helper_js_library($lib_name, $content, $params = [])

Helper for JS library code.

    public function helper_js_library($lib_name, $content, $params = [])
    {
        $asset_type = 'js';
        if ( ! isset($this->already_required[$asset_type][$lib_name])) {
            $this->add_asset($lib_name, $asset_type, $this->ADD_IS_DIRECT_OUT ? ['direct_out' => false] : []);
            $this->already_required[$asset_type][$lib_name] = true;
        }
        return $this->add($content, $asset_type, 'inline', $params);
    }

html_out

public function html_out($out_type, $content_type, $str, $params = [])

Generate html output for desired asset out type and content type.

    public function html_out($out_type, $content_type, $str, $params = [])
    {
        if ( ! $out_type || ! $content_type || ! strlen($str)) {
            return false;
        }
        $func = __FUNCTION__;
        $out = '';
        $media_path = $this->_get_media_path();
        // try to find web path for file and show it as url
        if ($content_type === 'file') {
            if (substr($str, 0, strlen(PROJECT_PATH)) === PROJECT_PATH) {
                $url = $media_path . substr($str, strlen(PROJECT_PATH));
                return $this->$func($out_type, 'url', $url, $params);
            }
        }
        if ($content_type === 'url' && $this->SHORTEN_LOCAL_URL && MEDIA_PATH == WEB_PATH) {
            $slen = strlen($media_path);
            if (substr($str, 0, $slen) === $media_path) {
                $str = '/' . substr($str, $slen);
            }
        }
        if ($this->OUT_ADD_ASSET_NAME && ($params['asset_name'] ?? null) && ! isset($params['id']) && in_array($content_type, ['inline', 'file'])) {
            $params['data-asset'] = 'asset_' . $out_type . '_' . $content_type . '_' . $params['asset_name'];
        }
        if ($out_type === 'js') {
            $params['type'] = 'text/javascript';
            if ($content_type === 'file') {
                $str = file_get_contents($str);
            }
            if ($this->INLINE_ASSETS_USE_DATA_URI && $content_type === 'inline') {
                $content_type = 'url';
                $params['src'] = 'data:' . $params['type'] . ';base64,' . base64_encode($this->_strip_js_input($str));
            } elseif ($this->FILE_ASSETS_USE_DATA_URI && strlen($str) <= $this->FILE_ASSETS_DATA_URI_MAX_SIZE) {
                $content_type = 'url';
                $params['src'] = 'data:' . $params['type'] . ';base64,' . base64_encode($str);
            }
            if ($content_type === 'url') {
                if ( ! isset($params['src'])) {
                    $params['src'] = $str . $this->_cached_url_get_mtime($str);
                }
                $out = '<script' . _attrs($params, ['src', 'type', 'class', 'id', 'integrity', 'crossorigin']) . '></script>';
            } elseif ($content_type === 'file') {
                if ($params['raw'] ?? null) {
                    $out = $str;
                } else {
                    $out = '<script' . _attrs($params, ['type', 'class', 'id']) . '>' . PHP_EOL . $str . PHP_EOL . '</script>';
                }
            } elseif ($content_type === 'inline') {
                if ($params['raw'] ?? null) {
                    $out = $str;
                } else {
                    $str = $this->_strip_js_input($str);
                    $out = '<script' . _attrs($params, ['type', 'class', 'id']) . '>' . PHP_EOL . $str . PHP_EOL . '</script>';
                }
            }
        } elseif ($out_type === 'css') {
            $params['type'] = 'text/css';
            if ($content_type === 'file') {
                $str = file_get_contents($str);
            }
            if ($this->INLINE_ASSETS_USE_DATA_URI && $content_type === 'inline') {
                $content_type = 'url';
                $params['href'] = 'data:' . $params['type'] . ';base64,' . base64_encode($this->_strip_css_input($str));
            } elseif ($this->FILE_ASSETS_USE_DATA_URI && strlen($str) <= $this->FILE_ASSETS_DATA_URI_MAX_SIZE) {
                $content_type = 'url';
                $params['href'] = 'data:' . $params['type'] . ';base64,' . base64_encode($str);
            }
            if ($content_type === 'url') {
                $params['rel'] = 'stylesheet';
                if ( ! isset($params['href'])) {
                    $params['href'] = $str . $this->_cached_url_get_mtime($str);
                }
                $out = '<link' . _attrs($params, ['href', 'rel', 'class', 'id', 'integrity', 'crossorigin']) . ' />';
            } elseif ($content_type === 'file') {
                $out = '<style' . _attrs($params, ['type', 'class', 'id']) . '>' . PHP_EOL . $str . PHP_EOL . '</style>';
            } elseif ($content_type === 'inline') {
                $str = $this->_strip_css_input($str);
                $out = '<style' . _attrs($params, ['type', 'class', 'id']) . '>' . PHP_EOL . $str . PHP_EOL . '</style>';
            }
        }
        return $out;
    }
public function http_get_contents($url)
    public function http_get_contents($url)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        curl_setopt($ch, CURLOPT_USERAGENT, '');
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($ch, CURLOPT_TIMEOUT, $this->URL_TIMEOUT);
        $data = curl_exec($ch);
        return $data;
    }

init_css

public function init_css($force = false)

Main CSS from theme stpl.

    public function init_css($force = false)
    {
        if ($this->_init_css_done && ! $force) {
            return false;
        }
        $this->_init_css_done = true;
        // Check if disabled
        if ( ! $this->MAIN_TPL_CSS) {
            return false;
        }
        $main_style_css = trim(tpl()->parse_if_exists($this->MAIN_TPL_CSS));
        // single string = automatically generated by compass
        if (strlen($main_style_css) && strpos($main_style_css, "\n") === false && preg_match('~^css/style.css\?[0-9]{10}$~ims', $main_style_css)) {
            $media_path = $this->_get_media_path();
            $this->add($media_path . tpl()->TPL_PATH . $main_style_css, 'css', 'url');
        } else {
            $this->add($main_style_css, 'css', 'inline');
        }
    }

init_js

public function init_js($force = false)

Main JS from theme stpl.

    public function init_js($force = false)
    {
        if ($this->_init_js_done && ! $force) {
            return false;
        }
        $this->_init_js_done = true;
        // Check if disabled
        if ( ! $this->MAIN_TPL_JS) {
            return false;
        }
        $main_script_js = trim(tpl()->parse_if_exists($this->MAIN_TPL_JS));
        // single string = automatically generated by compass
        if (strlen($main_script_js) && strpos($main_script_js, "\n") === false && preg_match('~^js/script.js\?[0-9]{10}$~ims', $main_script_js)) {
            $media_path = $this->_get_media_path();
            $this->add($media_path . tpl()->TPL_PATH . $main_script_js, 'js', 'url');
        } else {
            $this->add($main_script_js, 'js', 'inline');
        }
    }

jquery

public function jquery($content, $params = [])

Helper for jquery on document ready.

    public function jquery($content, $params = [])
    {
        return $this->helper_js_library(__FUNCTION__, $content, $params + ['wrap' => '$(function(){' . PHP_EOL . '%s' . PHP_EOL . '})']);
    }
public function load_combined_config($force = false)
    public function load_combined_config($force = false)
    {
        if ( ! $this->COMBINE && ! $force) {
            return false;
        }
        if (isset($this->COMBINED_CONFIG) && ! $force) {
            return $this->COMBINED_CONFIG;
        }
        $path = CONFIG_PATH . 'assets_combine.php';
        if (file_exists($path)) {
            $this->COMBINED_CONFIG = include $path;
        }
        return $this->COMBINED_CONFIG;
    }
public function load_predefined_assets($force = false)
    public function load_predefined_assets($force = false)
    {
        // Cleanup previously filled assets
        if ($force) {
            $this->assets = [];
        }
        $assets = [];
        $suffix = '.php';
        $patterns = [
            'framework' => [
                YF_PATH . 'assets/*' . $suffix,
                YF_PATH . 'assets/*/*' . $suffix,
                YF_PATH . 'share/assets/*' . $suffix,
                YF_PATH . 'share/assets/*/*' . $suffix,
                YF_PATH . 'plugins/*/assets/*' . $suffix,
                YF_PATH . 'plugins/*/assets/*/*' . $suffix,
                YF_PATH . 'plugins/*/share/assets/*' . $suffix,
                YF_PATH . 'plugins/*/share/assets/*/*' . $suffix,
            ],
            'project' => [
                PROJECT_PATH . 'assets/*' . $suffix,
                PROJECT_PATH . 'assets/*/*' . $suffix,
                PROJECT_PATH . 'share/assets/*' . $suffix,
                PROJECT_PATH . 'share/assets/*/*' . $suffix,
                PROJECT_PATH . 'plugins/*/assets/*' . $suffix,
                PROJECT_PATH . 'plugins/*/assets/*/*' . $suffix,
                PROJECT_PATH . 'plugins/*/share/assets/*' . $suffix,
                PROJECT_PATH . 'plugins/*/share/assets/*/*' . $suffix,
            ],
            'app' => [
                APP_PATH . 'assets/*' . $suffix,
                APP_PATH . 'assets/*/*' . $suffix,
                APP_PATH . 'share/assets/*' . $suffix,
                APP_PATH . 'share/assets/*/*' . $suffix,
                APP_PATH . 'plugins/*/assets/*' . $suffix,
                APP_PATH . 'plugins/*/assets/*/*' . $suffix,
                APP_PATH . 'plugins/*/share/assets/*' . $suffix,
                APP_PATH . 'plugins/*/share/assets/*/*' . $suffix,
            ],
        ];

        if (is_site_path()) {
            $patterns['site'] = [
                SITE_PATH . 'assets/*' . $suffix,
                SITE_PATH . 'assets/*/*' . $suffix,
                SITE_PATH . 'share/assets/*' . $suffix,
                SITE_PATH . 'share/assets/*/*' . $suffix,
                SITE_PATH . 'plugins/*/assets/*' . $suffix,
                SITE_PATH . 'plugins/*/assets/*/*' . $suffix,
                SITE_PATH . 'plugins/*/share/assets/*' . $suffix,
                SITE_PATH . 'plugins/*/share/assets/*/*' . $suffix,
            ];
        }

        $slen = strlen($suffix);
        $names = [];
        foreach ($patterns as $glob_patterns) {
            foreach ($glob_patterns as $glob) {
                foreach (glob($glob) as $path) {
                    $name = substr(basename($path), 0, -$slen);
                    $names[$name] = $path;
                }
            }
        }
        // This double iterating code ensures we can inherit/replace assets with same name inside project
        foreach ($names as $name => $path) {
            $assets[$name] = include $path;
            if (defined('DEBUG_INFO') && constant('DEBUG_INFO')) {
                debug('assets_names[]', [
                    'name' => $name,
                    'path' => $path,
                    'content' => $assets[$name],
                ]);
            }
        }
        $this->assets += $assets;
        return $assets;
    }

set_cache

public function set_cache($out_type, $md5, $data = [])
    public function set_cache($out_type, $md5, $data = [])
    {
        if ( ! $this->USE_CACHE) {
            return false;
        }
        $cache_path = $this->_cache_path($out_type, $md5, $data);
        $content = $data['content'];
        $content_type = $data['content_type'];
        $content_url = '';
        if ($content_type === 'url') {
            $content_url = $content;
            $content = $this->_url_get_contents($content_url);
        } elseif ($content_type === 'file') {
            // mtime first: an edit between the two reads then only makes the next request refresh the copy
            clearstatcache(true, $content);
            $info_extra = is_file($content) ? ['source_mtime' => filemtime($content)] : [];
            $content = file_get_contents($content);
        }
        if ( ! strlen($content)) {
            return false;
        }
        $info_extra ??= [];
        // Content is same, no need to overwrite it
        $cache_existed = file_exists($cache_path);
        if ($cache_existed && file_get_contents($cache_path) === $content) {
            is_writable($cache_path) && touch($cache_path);
            $this->CACHE_CHECK_SOURCE_MTIME && $info_extra && $this->_write_cache_info($cache_path, '', $content, $info_extra);
            return $cache_path;
        }
        $this->_atomic_write($cache_path, $content);
        // Decode gzip content to not confuse browser and web server, as gzdecode($content) is PHP 5.4+ only
        // gzip file beginnning: \x1F\x8B
        if (bin2hex(substr($content, 0, 2)) === '1f8b') {
            ob_start();
            readgzfile($cache_path);
            $content = ob_get_clean();
            $this->_atomic_write($cache_path, $content);
        }
        if ($out_type === 'css' && in_array($content_type, ['url', 'inline', 'file'])) {
            $content_before = $content;
            $content = $this->_css_urls_rewrite_and_save($content, $content_url, $cache_path, $content_type, $data['content']);
            if ($content_before !== $content) {
                $this->_atomic_write($cache_path, $content);
            }
            unset($content_before);
        }
        if ($out_type === 'js' && $content_type === 'url') {
            $this->_js_map_save($content, $content_url, $cache_path);
        }
        // Content is same, no need to overwrite info about it
        // This is not a mistake to do this check again, after url rewrite content can still be same
        if ($cache_existed && file_get_contents($cache_path) === $content && ! ($this->CACHE_CHECK_SOURCE_MTIME && $info_extra)) {
            return $cache_path;
        }
        $this->_write_cache_info($cache_path, $content_url, $content, $info_extra);
        return $cache_path;
    }

set_content

public function set_content($asset_type, $md5, $content_type, $content, $params = [])

Set unique content entry for given asset type.

    public function set_content($asset_type, $md5, $content_type, $content, $params = [])
    {
        if (isset($this->content[$asset_type][$md5])) {
            return $this->content[$asset_type][$md5];
        }
        if (isset($params['wrap']) && false !== strpos($params['wrap'], '%s')) {
            $content = str_replace('%s', $content, $params['wrap']);
        }
        $name = '';
        if (isset($params['name'])) {
            $name = $params['name'];
            unset($params['name']);
        }
        // Skip any related content, that was specially cached or listed, but not need to be included here
        if (in_array($content_type, ['file']) && in_array($asset_type, ['js', 'css'])) {
            // This line needed to strip query string from file name like this:
            // /templates/user/css/style.css?1416914173 -> templates/user/css/style.css
            if (pathinfo((string) parse_url($content, PHP_URL_PATH), PATHINFO_EXTENSION) != $asset_type) {
                return $this->_skip_ext_mismatch($asset_type, $content);
            }
        } elseif (in_array($content_type, ['url']) && in_array($asset_type, ['js', 'css'])) {
            // This line needed to strip query string from file name like this:
            // /templates/user/css/style.css?1416914173 -> templates/user/css/style.css
            $parsed_url = parse_url($content);
            if ( ! strlen($parsed_url['host'] ?? '') && strlen($parsed_url['path'] ?? '') && pathinfo($parsed_url['path'] ?? '', PATHINFO_EXTENSION) != $asset_type) {
                return $this->_skip_ext_mismatch($asset_type, $content);
            }
        }
        return $this->content[$asset_type][$md5] = [
            'content_type' => $content_type,
            'content' => $content,
            'name' => $name,
            'version' => $name ?: 'master',
            'params' => $params,
        ];
    }

show

public function show($out_type, $params = [])

Main method to display overall content by out type (js, css, images, fonts). Can be called from main template like this: {exec_last(assets,show_js)} {exec_last(assets,show_css)}.

    public function show($out_type, $params = [])
    {
        if ( ! $out_type || ! in_array($out_type, $this->supported_out_types)) {
            throw new Exception('Assets: unsupported out content type: ' . $out_type);
            return null;
        }
        if ($out_type === 'js') {
            $this->init_js();
        } elseif ($out_type === 'css') {
            $this->init_css();
        }
        if ( ! is_array($params)) {
            $params = ! empty($params) ? [$params] : [];
        }
        // Assets from current module
        $module_assets_path = $this->find_asset_type_for_module($out_type, $_GET['object'] ?? '');
        if ($module_assets_path) {
            $this->add_file($module_assets_path, $out_type);
        }
        if ($out_type === 'js' && $this->USE_REQUIRE_JS) {
            // the RequireJS output is a stub (''): fall through to the normal output instead of a page without JS
            $require_js = $this->show_require_js($params);
            if ($require_js !== '') {
                return $require_js;
            }
        }
        if ($this->FILTERS_AUTO_APPLY) {
            $this->filters_process_added($out_type, $params);
        }
        if ($this->COMBINE) {
            $combined_file = $this->_get_combined_path($out_type);
            // with COMBINED_CHECK_SOURCES the file name depends on the elements, known only after the loop below
            $md5_inside_combined = $this->COMBINED_CHECK_SOURCES ? [] : $this->_combined_elements($combined_file);
        }
        $bs_current_theme = common()->bs_current_theme();
        $media_path = $this->_get_media_path();
        $prepend = _class('core_events')->fire('assets.prepend');
        // Process previously added content, depending on its type
        $out = [];
        $to_combine = [];
        foreach ((array) $this->_get_all_content_for_out($out_type) as $md5 => $v) {
            if ( ! is_array($v)) {
                continue;
            }
            $_params = ((array) $v['params'] + (array) $params) ?? [];
            $content_type = $v['content_type'];
            $cached_path = '';
            $use_cache = $this->USE_CACHE && ! ($_params['no_cache'] ?? false) && ! ($_params['config']['no_cache'] ?? false);
            if ($use_cache && $content_type === 'inline') {
                if ( ! $this->CACHE_INLINE_ALLOW || ! ($_params['config']['inline_cache'] ?? false) || strlen($v['content']) < $this->CACHE_INLINE_MIN_SIZE) {
                    $use_cache = false;
                }
            }
            if ($use_cache) {
                if ($v['name'] === 'bootstrap-theme') {
                    $v['name'] .= '-' . $bs_current_theme;
                }
                $cached_path = $this->get_cache($out_type, $md5, $v);
                if ( ! $cached_path && ! $this->FORCE_LOCAL_STORAGE) {
                    $cached_path = $this->set_cache($out_type, $md5, $v);
                }
                if ($cached_path) {
                    $content_type = 'url';
                    $v['content'] = $media_path . substr($cached_path, strlen(PROJECT_PATH));
                    // the local copy is rewritten by set_cache(), so the CDN hash no longer applies
                    unset($_params['integrity'], $_params['crossorigin']);
                }
            }
            $str = $v['content'];
            $before = $_params['config']['before'] ?? '';
            $after = $_params['config']['after'] ?? '';
            if ($_params['config']['class'] ?? '') {
                $_params['class'] = $_params['config']['class'];
            }
            $use_combine = $this->COMBINE && $use_cache && in_array($content_type, ['url', 'file']) && empty($before) && empty($after) && empty($_params['class']) && empty($_params['id']);
            if ($use_combine) {
                $to_combine[$md5] = [
                    'content' => $str,
                    'content_type' => $content_type,
                    'name' => $v['name'],
                ];
            }
            if (DEBUG_MODE) {
                $debug = [];
                foreach ((array) debug('assets_add') as $d) {
                    if ($d['md5'] === $md5) {
                        $debug = $d;
                        break;
                    }
                }
                $dname = $out_type . '_' . $md5;
                $trace_short = str_replace(['<', '>'], ['<', '>'], implode('; ', array_slice(explode(PHP_EOL, $debug['trace']), 2, 2, true)));
                $ctype = $debug['content_type'];
                if ($ctype === 'asset') {
                    $ctype .= ':' . $debug['content'];
                }
                $before = PHP_EOL . '<!-- asset start: ' . $dname . ' | ' . $ctype . ' | ' . $trace_short . ' -->' . PHP_EOL . $before;
                $after = $after . PHP_EOL . '<!-- asset end: ' . $dname . ' -->' . PHP_EOL;
                debug('assets_out[]', [
                    'out_type' => $out_type,
                    'name' => $v['name'],
                    'md5' => $md5,
                    'content_type' => $content_type,
                    'content' => $str,
                    'preview' => '',
                    'params' => $_params,
                    'cached' => $cached_path ? 1 : 0,
                    'combined' => (int) isset($to_combine[$md5]),
                    'trace' => $debug['trace'],
                ]);
            }
            $out[$md5] = $before . $this->html_out($out_type, $content_type, $str, $_params + ['asset_name' => $v['name']]) . $after;
        }
        if ($this->COMBINE && $to_combine) {
            if ($this->COMBINED_CHECK_SOURCES) {
                $combined_file = $this->_get_combined_path($out_type, $this->_combined_content_hash($to_combine));
                $md5_inside_combined = $this->_combined_elements($combined_file);
            }
            $out = $this->_combine_content($out, $out_type, $to_combine, $combined_file, $md5_inside_combined);
        }
        $append = _class('core_events')->fire('assets.append', ['out' => &$out]);
        $this->clean_content($out_type);
        return implode(PHP_EOL, $prepend) . implode(PHP_EOL, $out) . implode(PHP_EOL, $append);
    }

show_css

public function show_css($params = [])

Shortcut.

    public function show_css($params = [])
    {
        return $this->show('css', $params);
    }

show_js

public function show_js($params = [])

Shortcut.

    public function show_js($params = [])
    {
        return $this->show('js', $params);
    }
public function show_require_js($params = [])

Stub: the RequireJS integration (USE_REQUIRE_JS, ?assets_requirejs=1) was never finished, so it outputs nothing.

    public function show_require_js($params = [])
    {
        return '';
    }

Methods starting with _ hooks and helpers; not routable as page actions

__call

public function __call($name, $args)

Catch missing method call.

    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args);
    }

__clone

public function __clone()
    public function __clone()
    {
        return $this->clean_all();
    }

__toString

public function __toString(): string

Direct call to object as to string is not allowed, return empty string instead.

    public function __toString()
    {
        return '';
    }

_add_asset

public function _add_asset($_content, $asset_type, $_params = [])
    public function _add_asset($_content, $asset_type, $_params = [])
    {
        if ( ! $_content) {
            return false;
        }
        // Prevent recursion
        if (is_string($_content)) {
            if (isset($this->_assets_added[$asset_type][$_content])) {
                return false;
            }
            $this->_assets_added[$asset_type][$_content] = true;
        }
        $asset_data = $this->get_asset_details($_content);
        if ( ! $asset_data) {
            return false;
        }
        if ( ! is_string($asset_data) && is_callable($asset_data)) {
            $asset_data = $asset_data($_params, $this);
        }
        if ( ! $asset_data) {
            return false;
        }
        if (is_string($_content) && is_array($asset_data) && ! empty($asset_data['abandoned']) && is_string($asset_data['abandoned']) && ! isset($this->_abandoned_noticed[$_content])) {
            $this->_abandoned_noticed[$_content] = true;
            trigger_error('Assets: "' . $_content . '" is abandoned: ' . $asset_data['abandoned'], E_USER_NOTICE);
        }
        if (isset($asset_data['config']) && is_array($asset_data['config'])) {
            $_params['config'] = (array) ($_params['config'] ?? null) + (array) $asset_data['config'];
        }
        $DIRECT_OUT = isset($_params['direct_out']) ? $_params['direct_out'] : $this->ADD_IS_DIRECT_OUT;
        $_params += ($DIRECT_OUT ? ['direct_out' => false] : []);
        if (is_string($_content)) {
            $_params['name'] = $_content;
        }
        $__params = $_params;
        // Prevent inherit no_cache and other package-related settings into other required packages
        if (isset($__params['config'])) {
            unset($__params['config']);
        }

        $inherit_types_map = $this->inherit_asset_types_map;
        $types = [];
        $types[$asset_type] = $asset_type;
        $inherit_types = (array) ($inherit_types_map[$asset_type] ?? null) ?: [];
        foreach ((array) $inherit_types as $inherit_type) {
            $types[$inherit_type] = $inherit_type;
        }
        foreach ((array) $types as $atype) {
            $data = $asset_data['require'][$atype] ?? null;
            if ($data) {
                $this->_sub_add($data, $atype, $__params);
            }
        }
        foreach ((array) $types as $atype) {
            $data = $this->get_asset($_content, $atype);
            if ($data) {
                $this->_sub_add($data, $atype, $_params);
            }
        }
        foreach ((array) $types as $atype) {
            $data = $asset_data['add'][$atype] ?? null;
            if ($data) {
                $this->_sub_add($data, $atype, $__params);
            }
        }
    }

_add_bundle

public function _add_bundle($_content, $_params = [])
    public function _add_bundle($_content, $_params = [])
    {
        if ( ! $_content) {
            return false;
        }
        // Prevent recursion
        if (is_string($_content)) {
            if (isset($this->_bundles_added[$_content])) {
                return false;
            }
            $this->_bundles_added[$_content] = true;
        }
        $bundle_details = $this->get_asset_details($_content);
        if ( ! $bundle_details) {
            return false;
        }
        if ( ! is_string($bundle_details) && is_callable($bundle_details)) {
            $bundle_details = $bundle_details($_params, $this);
        }
        if ( ! $bundle_details) {
            return false;
        }
        if (isset($bundle_details['config']) && is_array($bundle_details['config'])) {
            $_params['config'] = (array) ($_params['config'] ?? null) + (array) $bundle_details['config'];
        }
        $DIRECT_OUT = isset($_params['direct_out']) ? $_params['direct_out'] : $this->ADD_IS_DIRECT_OUT;
        $_params += ($DIRECT_OUT ? ['direct_out' => false] : []);
        if (is_string($_content)) {
            $_params['name'] = $_content;
        }
        $__params = $_params;
        // Prevent inherit no_cache and other package-related settings into other required packages
        if (isset($__params['config'])) {
            unset($__params['config']);
        }
        $inherit_types_map = $this->inherit_asset_types_map;
        $types = [];
        foreach ((array) $this->supported_asset_types as $k => $atype) {
            if ($atype === 'jquery') {
                continue;
            }
            $types[$atype] = $atype;
            $inherit_types = (array) ($inherit_types_map[$atype] ?? null) ?: [];
            foreach ((array) $inherit_types as $inherit_type) {
                $types[$inherit_type] = $inherit_type;
            }
        }
        foreach ((array) $types as $atype) {
            $data = $bundle_details['require'][$atype] ?? null;
            if ($data) {
                $this->_sub_add($data, $atype, $__params);
            }
        }
        foreach ((array) $types as $atype) {
            $data = $this->get_asset($_content, $atype);
            if ($data) {
                $this->_sub_add($data, $atype, $_params);
            }
        }
        foreach ((array) $types as $atype) {
            $data = $bundle_details['add'][$atype] ?? null;
            if ($data) {
                $this->_sub_add($data, $atype, $__params);
            }
        }
    }

_atomic_write

public function _atomic_write($path, $content)

Write through a temp file in the same dir and rename(), so a concurrent request never reads a half written file.

    public function _atomic_write($path, $content)
    {
        $tmp = $path . '.' . bin2hex(random_bytes(6)) . '.tmp';
        $written = file_put_contents($tmp, $content, LOCK_EX);
        if (false === $written) {
            @unlink($tmp);
            return false;
        }
        if ( ! rename($tmp, $path)) {
            unlink($tmp);
            return false;
        }
        return $written;
    }
public function _autoload_libs()
    public function _autoload_libs()
    {
        if (isset($this->_autoload_registered)) {
            return (bool) $this->_autoload_registered;
        }
        $this->_autoload_registered = false;
        $paths = [
            APP_PATH . 'vendor/autoload.php',
            YF_PATH . 'vendor/autoload.php',
            '/usr/local/share/composer/vendor/autoload.php',
        ];
        foreach ($paths as $path) {
            if (file_exists($path)) {
                $this->_autoload_registered = $path;
                require_once $path;
                break;
            }
        }
        return (bool) $this->_autoload_registered;
    }

_cache_dir

public function _cache_dir($out_type, $asset_name = '', $version = '')
    public function _cache_dir($out_type, $asset_name = '', $version = '')
    {
        if ( ! is_string($this->CACHE_DIR_TPL) && is_callable($this->CACHE_DIR_TPL)) {
            $func = $this->CACHE_DIR_TPL;
            $cache_dir = $func($out_type, $asset_name, $version, $this);
        } else {
            $main_type = ($this->_override['main_type'] ?? null) ?: MAIN_TYPE;
            $replace = $this->_tpl_placeholders($main_type, $out_type) + [
                '{asset_name}' => $asset_name,
                '{version}' => $version,
            ];
            $cache_dir = str_replace(['///', '//'], '/', str_replace(array_keys($replace), array_values($replace), $this->CACHE_DIR_TPL));
        }
        ! is_dir($cache_dir) && mkdir($cache_dir, 0755, true);
        return $cache_dir;
    }
public function _cache_expired($cache_path)
    public function _cache_expired($cache_path)
    {
        return file_exists($cache_path) && filemtime($cache_path) <= (time() - $this->CACHE_TTL);
    }

_cache_name

public function _cache_name($out_type, $md5, $data = [])
    public function _cache_name($out_type, $md5, $data = [])
    {
        $content = $data['content'] ?? '';
        $content_type = $data['content_type'] ?? '';
        $_name = '';
        if ($content_type === 'url') {
            $_name = pathinfo((string) parse_url($content, PHP_URL_PATH), PATHINFO_FILENAME);
            while (pathinfo($_name, PATHINFO_EXTENSION) === $out_type && $out_type) {
                $_name = pathinfo($_name, PATHINFO_FILENAME);
            }
        } elseif ($content_type === 'file') {
            $_name = pathinfo($content, PATHINFO_FILENAME);
        } elseif ($content_type === 'inline') {
            $_name = md5($content);
        }
        return $_name;
    }

_cache_path

public function _cache_path($out_type, $md5, $data = [])
    public function _cache_path($out_type, $md5, $data = [])
    {
        $cache_dir = $this->_cache_dir($out_type, $data['name'], $data['version']);
        $cache_name = $this->_cache_name($out_type, $md5, $data);
        return $cache_dir . ($cache_name ?: ($data['name'] ? $md5 . '_' . $data['name'] : '') ?: $md5) . '.' . $out_type;
    }
public function _cache_source_is_current($cache_path, $data = [])

Local file asset: is the source mtime kept in the .info the same as now. A cache without that (written by an older version or not a file) counts as current, the TTL rule decides.

    public function _cache_source_is_current($cache_path, $data = [])
    {
        if (($data['content_type'] ?? '') !== 'file' || ! is_file($data['content'])) {
            return true;
        }
        $info = is_file($cache_path . '.info') ? json_decode((string) file_get_contents($cache_path . '.info'), $as_array = true) : null;
        if ( ! is_array($info) || ! isset($info['source_mtime'])) {
            return true;
        }
        clearstatcache(true, $data['content']);
        return (int) $info['source_mtime'] === (int) filemtime($data['content']);
    }
public function _cached_url_get_mtime($str = '')

Add modification time to url, if this is local file, no matter cached or not, we just need to be able to get its mtime.

    public function _cached_url_get_mtime($str = '')
    {
        $url_param = 'yfmt';
        if ( ! $this->CACHE_OUT_ADD_MTIME || ! strlen($str) || false !== strpos($str, $url_param . '=')) {
            return false;
        }
        $mtime = '';
        $file = '';
        $has_question_sign = (false !== strpos($str, '?'));
        $media_path = $this->_get_media_path();
        $media_path_len = strlen($media_path);
        // short url paths like /templates/user/cache/..., but not //domain.com/
        if (substr($str, 0, 1) === '/' && substr($str, 1, 1) !== '/') {
            if (substr($str, 0, strlen(PROJECT_PATH)) === PROJECT_PATH) {
                $file = $str;
            } else {
                $file = PROJECT_PATH . ltrim($str, '/');
            }
        } elseif (substr($str, 0, $media_path_len) === $media_path) {
            $file = PROJECT_PATH . ltrim(substr($str, $media_path_len), '/');
        }
        // Avoid different url params when checking local file path
        if ($file) {
            $file = parse_url($file, PHP_URL_PATH);
        }
        if ($file && file_exists($file)) {
            $mtime = filemtime($file);
        }
        return $mtime ? ($has_question_sign ? '&' : '?') . urlencode($url_param) . '=' . $mtime : '';
    }
public function _combine_content(array $out, $out_type, array $to_combine, $combined_file, array $md5_inside_combined)
  • $out array
  • $to_combine array
  • $md5_inside_combined array
    public function _combine_content(array $out, $out_type, array $to_combine, $combined_file, array $md5_inside_combined)
    {
        $combined = '';
        $lock = false;
        if ($this->COMBINED_LOCK && ! file_exists($combined_file)) {
            $lock = fopen($combined_file . '.lock', 'c');
            if ( ! $lock || ! flock($lock, LOCK_EX | LOCK_NB)) {
                $lock && fclose($lock);
                // Another request is building it, do not wait: the elements go out one by one
                return $out;
            }
            clearstatcache();
        }
        if ( ! file_exists($combined_file)) {
            $divider = PHP_EOL;
            if ($out_type === 'js') {
                $divider = PHP_EOL . ';' . PHP_EOL;
            }
            $combined = [];
            $combined_names = [];
            foreach ($to_combine as $md5 => $info) {
                $content_type = $info['content_type'];
                $content = $info['content'];
                if ($content_type === 'url') {
                    $combined[$md5] = $this->_url_get_contents($content);
                } elseif ($content_type === 'file' && file_exists($content)) {
                    $combined[$md5] = file_get_contents($content);
                }
                if ($out_type === 'css' && in_array($content_type, ['url', 'inline'])) {
                    $combined[$md5] = $this->_css_urls_rewrite_and_save($combined[$md5], $content, $combined_file, $content_type);
                }
                if ($out_type === 'js' && $content_type === 'url') {
                    $this->_js_map_save($combined[$md5], $content, $combined_file);
                }
                $md5_inside_combined[$md5] = $md5;
                $combined_names[$info['name']] = $info['name'];
            }
            if ($combined) {
                $combined_md5 = array_keys($combined);
                $combined = implode($divider, $combined);
                // .info first: _combined_elements() deletes a combined file without one, a request between the two writes must not see that
                $this->_write_cache_info($combined_file, '', $combined, ['elements' => implode(',', $combined_md5), 'names' => implode(',', $combined_names)]);
                $this->_atomic_write($combined_file, $combined);
                $this->COMBINED_CHECK_SOURCES && $this->_combined_remove_previous($combined_file, implode(',', $combined_md5));
            }
        }
        if ($lock) {
            flock($lock, LOCK_UN);
            fclose($lock);
            unlink($combined_file . '.lock');
        }
        foreach ($to_combine as $md5 => $info) {
            if (isset($md5_inside_combined[$md5])) {
                unset($out[$md5]);
            }
        }
        $before = '';
        $after = '';
        if (DEBUG_MODE) {
            $dname = 'combined';
            $trace = main()->trace_string();
            $trace_short = str_replace(['<', '>'], ['<', '>'], implode('; ', array_slice(explode(PHP_EOL, $trace), 2, 2, true)));
            $before = PHP_EOL . '<!-- asset start: ' . $dname . ' | ' . $out_type . ' | ' . $trace_short . ' -->' . PHP_EOL . $before;
            $after = $after . PHP_EOL . '<!-- asset end: ' . $dname . ' -->' . PHP_EOL;
            debug('assets_out[]', [
                'out_type' => $out_type,
                'name' => $dname,
                'md5' => '',
                'content_type' => 'file',
                'content' => $combined_file,
                'preview' => '',
                'params' => '',
                'cached' => '1',
                'combined' => '',
                'trace' => $trace,
            ]);
        }
        return [
            md5(is_string($combined) ? $combined : '') => $before . $this->html_out($out_type, 'file', $combined_file) . $after,
        ] + $out;
    }
public function _combine_element_content($info)
    public function _combine_element_content($info)
    {
        if ($info['content_type'] === 'url') {
            return $this->_url_get_contents($info['content']);
        }
        return file_exists($info['content']) ? file_get_contents($info['content']) : null;
    }
public function _combined_content_hash(array $to_combine)
  • $to_combine array
    public function _combined_content_hash(array $to_combine)
    {
        $hash = [];
        $media_path = $this->_get_media_path();
        foreach ($to_combine as $md5 => $info) {
            if ($info['content_type'] === 'url' && substr($info['content'], 0, strlen($media_path)) !== $media_path) {
                // Remote and not cached locally: never fetched at request time, so its content changes are not detected
                $hash[] = md5('url:' . $info['content']);
                continue;
            }
            $hash[] = md5((string) $this->_combine_element_content($info));
        }
        return md5(implode(',', $hash));
    }
public function _combined_elements($combined_file)

Ids of the elements inside an existing combined file, an unusable file (no .info) is removed.

    public function _combined_elements($combined_file)
    {
        $md5_inside_combined = [];
        if (file_exists($combined_file)) {
            $combined_info = is_file($combined_file . '.info') ? json_decode(file_get_contents($combined_file . '.info'), $as_array = true) : null;
            if (is_array($combined_info) && isset($combined_info['elements'])) {
                $md5_inside_combined = explode(',', $combined_info['elements']);
                $md5_inside_combined = array_combine($md5_inside_combined, $md5_inside_combined);
            } else {
                // Combined file without a usable .info is unusable, rebuild it
                unlink($combined_file);
            }
        }
        return $md5_inside_combined;
    }
public function _combined_remove_previous($combined_file, $elements)

Remove the previous combined file(s) of the same set of elements (same element ids in the same order, other content).

    public function _combined_remove_previous($combined_file, $elements)
    {
        $ext = pathinfo($combined_file, PATHINFO_EXTENSION);
        foreach ((array) glob(dirname($combined_file) . '/*combined.' . $ext . '.info') as $info_file) {
            $file = substr($info_file, 0, -5);
            if ($file === $combined_file) {
                continue;
            }
            $info = json_decode((string) file_get_contents($info_file), $as_array = true);
            // Kept for a while: pages already served or cached elsewhere still link it
            if (is_array($info) && ($info['elements'] ?? null) === $elements && ! (is_file($file) && filemtime($file) > time() - $this->COMBINED_PREVIOUS_GRACE)) {
                is_file($file) && unlink($file);
                unlink($info_file);
            }
        }
    }
public function _css_keep_dirs_rel($path, $host = '')

Path of a file referenced by CSS under the cache dir, with its dirs: no "..", so it cannot leave the cache dir.

  • $path mixed absolute local path or url path
  • $host mixed set for a remote file, becomes the first dir
    public function _css_keep_dirs_rel($path, $host = '')
    {
        $path = $this->_get_absolute_path($path);
        if (strlen($host)) {
            $host = preg_replace('~[^a-z0-9._-]~i', '_', $host);
            $path = (trim($host, '.') === '' ? '_' : $host) . '/' . $path;
        } elseif (strpos('/' . $path, PROJECT_PATH) === 0) {
            $path = substr('/' . $path, strlen(PROJECT_PATH));
        }
        return ltrim($path, '/');
    }

_css_rel_url

public function _css_rel_url($from_dir, $to_path)

Relative url from a dir to a file under the same root.

    public function _css_rel_url($from_dir, $to_path)
    {
        $from = array_values(array_filter(explode('/', $from_dir), 'strlen'));
        $to = array_values(array_filter(explode('/', $to_path), 'strlen'));
        while (count($to) > 1 && $from && $from[0] === $to[0]) {
            array_shift($from);
            array_shift($to);
        }
        return str_repeat('../', count($from)) . implode('/', $to);
    }
public function _css_urls_rewrite_and_save($content, $content_url, $cache_path, $content_type = 'url', $orig_content = '', $base_dir = '')

process and save CSS url() and @import.

  • $base_dir mixed dir of the CSS file the urls are written into, empty means the dir of $cache_path (CACHE_CSS_KEEP_DIRS only)
    public function _css_urls_rewrite_and_save($content, $content_url, $cache_path, $content_type = 'url', $orig_content = '', $base_dir = '')
    {
        $_this = $this;
        $self_func = __FUNCTION__;
        return preg_replace_callback('~url\([\'"\s]*(?P<url>[^\'"\)]+?)[\'"\s]*\)~ims', function ($m) use ($_this, $content_url, $cache_path, $content_type, $orig_content, $self_func, $base_dir) {
            $url = trim($m['url']);
            if (strpos($url, 'data:') === 0) {
                return $m[0];
            }
            // Inline CSS has no base url: a relative or root path cannot be fetched, the original text is the best we have
            if ($_this->CACHE_CSS_KEEP_DIRS && $content_type === 'inline' && ! preg_match('~^(https?:)?//~i', $url)) {
                return $m[0];
            }
            $str = '';
            $save_path = '';
            $is_local_file = false;
            if ($content_type === 'file') {
                // examples: ../ ./
                if (substr($url, 0, 1) === '.') {
                    $is_local_file = true;
                    // full url like http://test.dev/image.png
                } elseif (false === strpos($url, 'http://') && false === strpos($url, 'https://')) {
                    $is_local_file = true;
                    // should not match: //domain.com/some_path
                } elseif (substr($url, 0, 1) === '/' && substr($url, 1, 1) !== '/') {
                    $is_local_file = true;
                }
            }
            if ($is_local_file) {
                $basename = basename($url);
                // Fix for local files and url params inside path
                if (false !== ($pos = strpos($basename, '?'))) {
                    $url = dirname($url) . '/' . substr($basename, 0, $pos);
                }
                // /templates/user/theme/default/image/smile_1.0_unactive.png
                if (substr($url, 0, 1) === '/') {
                    $try_path = PROJECT_PATH . ltrim($url, '/');
                } else {
                    $try_path = dirname($orig_content) . '/' . ltrim($url, '/');
                }
                $path = $_this->_get_absolute_path($try_path);
                $path = '/' . ltrim($path, '/');

                $save_path = $_this->_get_absolute_path(dirname($cache_path) . '/' . ($_this->CACHE_CSS_KEEP_DIRS ? $_this->_css_keep_dirs_rel($path) : basename($path)));
                $save_path = '/' . ltrim($save_path, '/');
                // path cleanup from *.*#someworlds
                $path = preg_replace('/([\/\w\d]+?\.[a-z]{1,4})#[\w\d\-]+$/is', '${1}', $path);
                // singleton for getting urls contents
                if ( ! file_exists($save_path) && file_exists($path)) {
                    $str = file_get_contents($path);
                }
            } else {
                $orig_url = $url;
                if (substr($url, 0, 2) !== '//' && substr($url, 0, strlen('http')) !== 'http') {
                    $url = dirname($content_url) . '/' . $url;
                }
                $u = parse_url($url);
                $host = $u['host'] ?? '';
                $path = $u['path'] ?? '';
                $query = $u['query'] ?? '';
                // example: //fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700
                if ($host === 'fonts.googleapis.com') {
                    $ext = '.' . ($path === '/css' ? 'css' : trim($path, '/')); // example: /css
                    $path = preg_replace('~[^a-z0-9_-]~ims', '_', strtolower($query)) . $ext;
                }
                $save_path = $_this->_get_absolute_path(dirname($cache_path) . '/' . ($_this->CACHE_CSS_KEEP_DIRS ? $_this->_css_keep_dirs_rel($path, $host) : basename($path)));
                $save_path = '/' . ltrim($save_path, '/');
                // singleton for getting urls contents
                if ( ! file_exists($save_path)) {
                    $url = $_this->_get_absolute_url($url) . ($query ? '?' . $query : '');
                    $str = $_this->_url_get_contents($url);
                }
            }
            if (strlen($str)) {
                $str = $_this->$self_func($str, $content_url, $cache_path, $content_type, $orig_content, dirname($save_path));
                ! is_dir(dirname($save_path)) && mkdir(dirname($save_path), 0755, true);
                $_this->_atomic_write($save_path, $str);
                $_this->_write_cache_info($save_path, $url, $str);
            }
            if ($_this->CACHE_IMAGES_USE_DATA_URI) {
                $ext = strtolower(pathinfo($save_path, PATHINFO_EXTENSION));
                if (in_array($ext, ['png', 'gif', 'jpg', 'jpeg'])) {
                    $max_size = $_this->CACHE_IMAGES_DATA_URI_MAX_SIZE;
                    $size = filesize($save_path);
                    if ($size <= $max_size) {
                        $data_type = 'image/' . ($ext == 'jpg' ? 'jpeg' : $ext);
                        return 'url(\'data:' . $data_type . ';base64,' . base64_encode(file_get_contents($save_path)) . '\')';
                    }
                }
            }
            if ($_this->CACHE_CSS_KEEP_DIRS) {
                return 'url(\'' . $_this->_css_rel_url($base_dir ?: dirname($cache_path), $save_path) . '\')';
            }
            return 'url(\'' . basename($save_path) . '\')';
        }, $content);
    }

_do_cache

public function _do_cache()
    public function _do_cache()
    {
        $this->_do_purge();

        $bak['ADD_IS_DIRECT_OUT'] = $this->ADD_IS_DIRECT_OUT;
        $this->ADD_IS_DIRECT_OUT = true;
        $bak['USE_CACHE'] = $this->USE_CACHE;
        $this->USE_CACHE = true;

        $combined_names = $this->load_combined_config($force = true);
        foreach ((array) $this->supported_out_types as $out_type) {
            foreach ((array) ($combined_names[MAIN_TYPE] ?? []) as $name) {
                $this->add_asset($name, $out_type);
            }
        }
        header('X-YF-assets-do-cache: true');

        $this->ADD_IS_DIRECT_OUT = $bak['ADD_IS_DIRECT_OUT'];
        $this->USE_CACHE = $bak['USE_CACHE'];
    }

_do_combine

public function _do_combine()
    public function _do_combine()
    {
        $this->_do_cache();

        $bak['ADD_IS_DIRECT_OUT'] = $this->ADD_IS_DIRECT_OUT;
        $this->ADD_IS_DIRECT_OUT = true;
        $bak['USE_CACHE'] = $this->USE_CACHE;
        $this->USE_CACHE = true;
        $bak['COMBINE'] = $this->COMBINE;
        $this->COMBINE = true;

        $combined_names = $this->load_combined_config($force = true);
        foreach ((array) $this->supported_out_types as $out_type) {
            $combined_path = $this->_get_combined_path($out_type);
            if (file_exists($combined_path)) {
                unlink($combined_path);
                unlink($combined_path . '.info');
            }
            foreach ((array) ($combined_names[MAIN_TYPE] ?? []) as $name) {
                $this->add_asset($name, $out_type);
            }
            $this->show($out_type);
        }
        header('X-YF-assets-do-combine: true');

        $this->ADD_IS_DIRECT_OUT = $bak['ADD_IS_DIRECT_OUT'];
        $this->USE_CACHE = $bak['USE_CACHE'];
        $this->COMBINE = $bak['COMBINE'];
    }

_do_purge

public function _do_purge()
    public function _do_purge()
    {
        $cache_dir_tpl = preg_replace('~/+~', '/', str_replace('{project_path}', PROJECT_PATH, $this->CACHE_DIR_TPL));
        $cache_dir = substr($cache_dir_tpl, 0, strpos($cache_dir_tpl, '{')) ?: $cache_dir_tpl;
        if (substr($cache_dir, 0, strlen(PROJECT_PATH)) === PROJECT_PATH && strlen($cache_dir) > strlen(PROJECT_PATH)) {
            _class('dir')->delete($cache_dir, $and_start_dir = true);
        }
        headers_sent() || header('X-YF-assets-do-purge: true');
    }
public function _get_absolute_path($path)
    public function _get_absolute_path($path)
    {
        $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
        $absolutes = [];
        foreach ($parts as $part) {
            if ('.' == $part) {
                continue;
            }
            if ('..' == $part) {
                array_pop($absolutes);
            } else {
                $absolutes[] = $part;
            }
        }
        return implode(DIRECTORY_SEPARATOR, $absolutes);
    }
public function _get_absolute_url($url)
    public function _get_absolute_url($url)
    {
        $u = parse_url($url);
        if (substr($url, 0, 2) === '//') {
            $u['scheme'] = 'http';
        }
        $host = $u['host'] ?? '';
        $path = $this->_get_absolute_path($u['path'] ?? '');
        $port = isset($u['port']) ? ':' . $u['port'] : '';
        return strlen($host) && strlen($path) ? ($u['scheme'] ?? 'http') . '://' . $host . $port . '/' . ltrim($path, '/') : null;
    }
public function _get_all_content_for_out($out_type, $params = [])
    public function _get_all_content_for_out($out_type, $params = [])
    {
        $is_ajax = main()->is_ajax();
        // Move down inlined content
        $all_content = $this->get_content($out_type);
        $top = [];
        $bottom = [];
        $last = [];
        $names_to_md5 = [];
        $out_before = [];
        $out_after = [];
        foreach ((array) $all_content as $md5 => $v) {
            if ($v['name']) {
                $names_to_md5[$v['name']] = $md5;
            }
        }
        foreach ((array) $all_content as $md5 => $v) {
            if ($v['params']['out_before'] ?? null) {
                $out_before[$md5] = $names_to_md5[$v['params']['out_before']] ?? null;
            } elseif ($v['params']['out_after'] ?? null) {
                $out_after[$md5] = $names_to_md5[$v['params']['out_after']] ?? null;
            }
            $content_type = $v['content_type'];
            if ($is_ajax && $content_type !== 'inline') {
                continue;
            }
            if ($v['params']['is_last'] ?? null) {
                $last[$md5] = $v;
            } elseif (in_array($content_type, ['inline'])) {
                $bottom[$md5] = $v;
            } else {
                $top[$md5] = $v;
            }
        }
        $data = $top + $bottom + $last;
        if ($out_before) {
            foreach ((array) $out_before as $self_md5 => $before_md5) {
                $pos = 0;
                $self_data = [$self_md5 => $data[$self_md5]];
                unset($data[$self_md5]);
                foreach ($data as $_md5 => $v) {
                    if ($_md5 === $before_md5) {
                        break;
                    }
                    $pos++;
                }
                if ($pos && $self_data) {
                    $data_before = array_slice($data, 0, $pos, $preserve_keys = true);
                    $data_after = array_slice($data, $pos, null, $preserve_keys = true);
                    $data = $data_before + $self_data + $data_after;
                }
            }
        }
        if ($out_after) {
            foreach ((array) $out_after as $self_md5 => $after_md5) {
                $pos = 0;
                $self_data = [$self_md5 => $data[$self_md5]];
                unset($data[$self_md5]);
                foreach ($data as $_md5 => $v) {
                    if ($_md5 === $after_md5) {
                        break;
                    }
                    $pos++;
                }
                if ($pos && $self_data) {
                    $data_before = array_slice($data, 0, $pos + 1, $preserve_keys = true);
                    $data_after = array_slice($data, $pos + 1, null, $preserve_keys = true);
                    $data = $data_before + $self_data + $data_after;
                }
            }
        }
        return $data;
    }
public function _get_asset_tpl_replace($name, $version = 'master')
    public function _get_asset_tpl_replace($name, $version = 'master')
    {
        ! isset($this->_cache_language) && $this->_cache_language = conf('language') ?: 'en';
        return [
            '{site_path}' => rtrim(SITE_PATH, '/'),
            '{app_path}' => rtrim(APP_PATH, '/'),
            '{project_path}' => rtrim(PROJECT_PATH, '/'),
            '{repo}' => $name,
            '{name}' => $name,
            '{version}' => $version,
            '{branch}' => $version,
            '{main_type}' => ($this->_override['main_type'] ?? null) ?: MAIN_TYPE,
            '{host}' => ($this->_override['host'] ?? null) ?: ($_SERVER['HTTP_HOST'] ?? ''),
            '{lang}' => $this->_cache_language,
        ];
    }
public function _get_combined_path($out_type, $content_hash = '')
  • $content_hash mixed md5 of the elements content, empty for the file of the month
    public function _get_combined_path($out_type, $content_hash = '')
    {
        return $this->_cache_path($out_type, $content_hash, [
            'name' => 'combined',
            'version' => $this->_get_combined_version($out_type),
        ]);
    }
public function _get_combined_version($out_type = '')
    public function _get_combined_version($out_type = '')
    {
        if ( ! is_string($this->COMBINED_VERSION_TPL) && is_callable($this->COMBINED_VERSION_TPL)) {
            $func = $this->COMBINED_VERSION_TPL;
            $version = $func($out_type, $this);
        } else {
            $replace = $this->_tpl_placeholders(MAIN_TYPE, $out_type);
            $version = str_replace(['///', '//'], '/', str_replace(array_keys($replace), array_values($replace), $this->COMBINED_VERSION_TPL));
        }
        return $version;
    }
public function _get_media_path()
    public function _get_media_path()
    {
        $media_path = MEDIA_PATH;
        if (substr($media_path, 0, 2) === '//') {
            $media_path = 'http:' . $media_path;
        }
        return $media_path;
    }

_init

public function _init()
    public function _init()
    {
        $this->_url_control();
        if ($this->FORCE_LOCAL_STORAGE) {
            $this->USE_CACHE = true;
            $this->CACHE_TTL = $this->FORCE_LOCAL_TTL;
        }
        $this->load_predefined_assets();
        $this->load_combined_config();
    }
public function _is_content_added($asset_type, $md5)
    public function _is_content_added($asset_type, $md5)
    {
        return isset($this->content[$asset_type][$md5]);
    }

_js_map_save

public function _js_map_save($content, $content_url, $cache_path)

Try to find and save JS map file. It is used for navigating minified files inside browser's developer tools.

    public function _js_map_save($content, $content_url, $cache_path)
    {
        $map_ext = '.map';
        $map_url = '';
        // Parse inline map url suggest, example: //# sourceMappingURL=lightbox.min.map
        if (preg_match('~#\s*sourceMappingURL=(?P<map_url>.+\.map)~ims', $content, $m)) {
            $map_url = trim($m['map_url']);
            if (strlen($map_url) && strpos($map_url, '/') === false) {
                $map_url = dirname($content_url) . '/' . $map_url;
            }
        }
        if ( ! $map_url) {
            return false;
        }
        $map_path = dirname($cache_path) . '/' . basename($map_url);
        if (file_exists($map_path)) {
            return true;
        }
        $map_content = $this->_url_get_contents($map_url);
        if ( ! strlen($map_content)) {
            return false;
        }
        if (file_exists($map_path) && file_get_contents($map_path) === $map_content) {
            is_writable($map_path) && touch($map_path);
            return true;
        }
        $this->_write_cache_info($map_path, $map_url, $map_content);
        return $this->_atomic_write($map_path, $map_content);
    }
public function _process_versions_content($content, $asset_type = '')
    public function _process_versions_content($content, $asset_type = '')
    {
        if ( ! $this->URL_REWRITES && ! $this->VENDOR_LOCAL && ! $this->SRI) {
            return $content;
        }
        if (is_string($content)) {
            $content = $this->_rewrite_url($content);
            if ( ! preg_match('~^(?:https?:)?//[^\s]+$~', $content)) {
                return $content;
            }
            $local = $this->_vendor_local_url($content);
            if ($local !== null) {
                return $local;
            }
            $params = $this->_sri_params($content);
            return $params ? ['content' => $content, 'params' => $params] : $content;
        }
        if (is_array($content)) {
            if (isset($content['content']) && is_string($content['content'])) {
                $item = $this->_process_versions_content($content['content'], $asset_type);
                if (is_array($item)) {
                    $content['params'] = (array) ($content['params'] ?? []) + $item['params'];
                    $item = $item['content'];
                }
                $content['content'] = $item;
                return $content;
            }
            foreach ($content as $k => $v) {
                $content[$k] = $this->_process_versions_content($v, $asset_type);
            }
        }
        return $content;
    }
public function _removed_content_compiler($method)
    public function _removed_content_compiler($method)
    {
        trigger_error('Assets: ' . $method . '() was removed, compile the source ahead of time and add the result with add_css() / add_js()', E_USER_DEPRECATED);
        return $this;
    }

_rewrite_url

public function _rewrite_url($str)
    public function _rewrite_url($str)
    {
        foreach ((array) $this->URL_REWRITES as $regex => $replace) {
            if (isset($this->_url_rewrites_bad[$regex])) {
                continue;
            }
            if ( ! isset($this->_url_rewrites_ok[$regex])) {
                set_error_handler(static fn () => true);
                $valid = false !== preg_match($regex, '');
                restore_error_handler();
                if ( ! $valid) {
                    $this->_url_rewrites_bad[$regex] = true;
                    trigger_error(__CLASS__ . ': URL_REWRITES key is not a valid regex, ignored: ' . $regex, E_USER_WARNING);
                    continue;
                }
                $this->_url_rewrites_ok[$regex] = true;
            }
            $str = preg_replace($regex, $replace, $str) ?? $str;
        }
        return $str;
    }
public function _skip_ext_mismatch($asset_type, $content)

A file or local url whose extension does not match the asset type is not stored: it used to leave an empty entry that printed a blank line.

    public function _skip_ext_mismatch($asset_type, $content)
    {
        if ( ! isset($this->_ext_mismatch_noticed[$asset_type . ':' . $content])) {
            $this->_ext_mismatch_noticed[$asset_type . ':' . $content] = true;
            trigger_error('Assets: "' . $content . '" skipped, its extension does not match the "' . $asset_type . '" asset type', E_USER_NOTICE);
        }
        return null;
    }

_sri_params

public function _sri_params($url)
    public function _sri_params($url)
    {
        if ( ! $this->SRI) {
            return null;
        }
        $entry = $this->_vendor_manifest()[$url] ?? null;
        if ( ! is_array($entry) || empty($entry['integrity'])) {
            return null;
        }
        return ['integrity' => $entry['integrity'], 'crossorigin' => 'anonymous'];
    }
public function _strip_css_input($str)

Cleanup for CSS strings.

    public function _strip_css_input($str)
    {
        // Extracting url from <link rel="stylesheet" href="path.to/style.css">
        $str = preg_replace_callback('~<link[\s]+[^>]*href=["\']([^"\']+?)["\'][^>]*>~ims', function ($m) {
            return $m[1];
        }, $str);

        for ($i = 0; $i < 10; $i++) {
            if (strpos($str, 'style') === false) {
                break;
            }
            $str = preg_replace('~^<style[^>]*?>~ims', '', $str);
            $str = preg_replace('~</style>$~ims', '', $str);
        }
        return $str;
    }
public function _strip_js_input($str)

Cleanup for JS strings.

    public function _strip_js_input($str)
    {
        // Extracting url from <script src="path.to/scripts.js"></script>
        $str = preg_replace_callback('~<script[\s]+[^>]*src=["\']([^"\']+?)["\'][^>]*>~ims', function ($m) {
            return $m[1];
        }, $str);

        for ($i = 0; $i < 10; $i++) {
            if (strpos($str, 'script') === false) {
                break;
            }
            $str = preg_replace('~^<script[^>]*?>~ims', '', $str);
            $str = preg_replace('~</script>$~ims', '', $str);
        }
        return $str;
    }

_sub_add

public function _sub_add($info, $asset_type, $_params = [])
    public function _sub_add($info, $asset_type, $_params = [])
    {
        if ( ! $info) {
            return false;
        }
        if ( ! is_string($info) && is_callable($info)) {
            $info = $info($_params, $this);
        }
        if ( ! $info) {
            return false;
        }
        if ( ! is_array($info)) {
            $info = [$info];
        }
        if (is_array($info) && isset($info['content'])) {
            if (is_array($info['params'] ?? null)) {
                $_params += $info['params'];
            }
            $info = $info['content'];
            if ( ! $info) {
                return false;
            }
            if ( ! is_array($info)) {
                $info = [$info];
            }
        }
        if ( ! $info) {
            return false;
        }
        foreach ((array) $info as $_info) {
            if ($_info) {
                $this->add($_info, $asset_type, 'auto', $_params);
            }
        }
    }
public function _theme_copy_warning($reason)

One warning per request when an internal theme cannot be served (the page then uses stock Bootstrap).

  • $reason string
    public function _theme_copy_warning($reason)
    {
        if ( ! $this->_theme_copy_warned) {
            $this->_theme_copy_warned = true;
            trigger_error('assets: internal theme not copied (' . $reason . '), using stock Bootstrap; see yf_assets::$THEMES_TARGET_DIR_TPL', E_USER_WARNING);
        }
    }
public function _theme_file_url($rel)

Web url of a file of the internal Bootstrap themes (YF_PATH/assets/themes/). The file is copied once into THEMES_TARGET_DIR_TPL/<content hash>/ with the files its relative url()s point at (fonts), so the framework dir is only read and a framework upgrade gets a new url.

  • $rel string path below assets/themes/, e.g. "bs5/slate/bootstrap.min.css"

Returns null|string null when the file is unknown or cannot be copied

    public function _theme_file_url($rel)
    {
        $rel = (string) $rel;
        $target_dir = $this->_theme_target_dir();
        $memo = $rel . '|' . $target_dir;
        if (array_key_exists($memo, $this->_cache_themes)) {
            return $this->_cache_themes[$memo];
        }
        $this->_cache_themes[$memo] = null;
        $base = realpath(YF_PATH . 'assets/themes');
        $from = $base ? realpath($base . '/' . $rel) : false;
        if ( ! $from || strpos($from, $base . DIRECTORY_SEPARATOR) !== 0 || ! is_file($from)) {
            return null;
        }
        if ( ! $this->_theme_target_ok($target_dir, false)) {
            return null;
        }
        // the lock pins the fonts, so a font update changes the dir even when the stylesheet stays the same
        $hash = md5((string) md5_file($from) . (string) @md5_file($base . '/themes.lock'));
        $target = $target_dir . substr($hash, 0, 12) . '/';
        $to = $target . substr($from, strlen($base) + 1);
        if ( ! file_exists($to)) {
            if ( ! $this->_theme_target_ok($target_dir, true)) {
                return null;
            }
            $data = (string) file_get_contents($from);
            // the referenced files first: an existing stylesheet copy means its fonts are in place
            preg_match_all('~url\(\s*([\'"]?)(?!data:|[a-z]+:|//|#)([^\'")?#]+)~i', $data, $m);
            foreach (array_unique($m[2]) as $ref) {
                $ref_from = realpath(dirname($from) . '/' . trim($ref));
                if ( ! $ref_from || strpos($ref_from, $base . DIRECTORY_SEPARATOR) !== 0 || ! is_file($ref_from)) {
                    continue;
                }
                $ref_to = $target . substr($ref_from, strlen($base) + 1);
                if ( ! file_exists($ref_to)) {
                    ! is_dir(dirname($ref_to)) && @mkdir(dirname($ref_to), 0755, true);
                    if (false === $this->_atomic_write($ref_to, file_get_contents($ref_from))) {
                        $this->_theme_copy_warning('cannot write ' . $ref_to);
                        return null;
                    }
                }
            }
            ! is_dir(dirname($to)) && @mkdir(dirname($to), 0755, true);
            if (false === $this->_atomic_write($to, $data)) {
                $this->_theme_copy_warning('cannot write ' . $to);
                return null;
            }
        }
        $this->_cache_themes[$memo] = MEDIA_PATH . substr($to, strlen(PROJECT_PATH));
        return $this->_cache_themes[$memo];
    }
public function _theme_generated_url($css)

Web url of a stylesheet generated by yf_bs_theme (site themes and overrides), written once to THEMES_TARGET_DIR_TPL/site/<md5>.css.

  • $css string

Returns null|string null when it cannot be written (one warning per request)

    public function _theme_generated_url($css)
    {
        $css = (string) $css;
        $target_dir = $this->_theme_target_dir();
        $to = $target_dir . 'site/' . md5($css) . '.css';
        if (array_key_exists($to, $this->_cache_themes)) {
            return $this->_cache_themes[$to];
        }
        $this->_cache_themes[$to] = null;
        if ( ! $this->_theme_target_ok($target_dir, ! file_exists($to))) {
            return null;
        }
        if ( ! file_exists($to)) {
            ! is_dir(dirname($to)) && @mkdir(dirname($to), 0755, true);
            if (false === $this->_atomic_write($to, $css)) {
                $this->_theme_copy_warning('cannot write ' . $to);
                return null;
            }
        }
        $this->_cache_themes[$to] = MEDIA_PATH . substr($to, strlen(PROJECT_PATH));
        return $this->_cache_themes[$to];
    }
public function _theme_target_dir()

THEMES_TARGET_DIR_TPL with its placeholders resolved, with a trailing slash.

    public function _theme_target_dir()
    {
        $replace = $this->_get_asset_tpl_replace('themes', 'local');
        return rtrim(str_replace(['///', '//'], '/', str_replace(array_keys($replace), array_values($replace), $this->THEMES_TARGET_DIR_TPL)), '/') . '/';
    }
public function _theme_target_ok($target_dir, $write)

The target dir is below PROJECT_PATH (it is served by url) and, when something must be written, its nearest existing parent is writable.

  • $target_dir string
  • $write bool
    public function _theme_target_ok($target_dir, $write)
    {
        if (strpos($target_dir, PROJECT_PATH) !== 0) {
            $this->_theme_copy_warning('THEMES_TARGET_DIR_TPL "' . $target_dir . '" is not below PROJECT_PATH');
            return false;
        }
        if ($write) {
            $parent = $target_dir;
            while ( ! is_dir($parent) && dirname($parent) !== $parent) {
                $parent = dirname($parent);
            }
            if ( ! is_writable($parent)) {
                $this->_theme_copy_warning($parent . ' is not writable');
                return false;
            }
        }
        return true;
    }
public function _tpl_placeholders($main_type, $out_type)

Placeholders shared by CACHE_DIR_TPL and COMBINED_VERSION_TPL.

    public function _tpl_placeholders($main_type, $out_type)
    {
        $host = ($this->_override['host'] ?? null) ?: ($_SERVER['HTTP_HOST'] ?? '');

        ! isset($this->_cache_language) && $this->_cache_language = conf('language') ?: 'en';
        $lang = ($this->_override['language'] ?? null) ?: $this->_cache_language;

        ! isset($this->_cache_html5fw) && $this->_cache_html5fw = conf('css_framework') ?: 'bs3';
        $html5fw = ($this->_override['html5fw'] ?? null) ?: $this->_cache_html5fw;

        ! isset($this->_cache_date) && $this->_cache_date = explode('-', date('Y-m-d-H-i-s'));
        $date = ($this->_override['date'] ?? null) ?: $this->_cache_date;

        return [
            '{site_path}' => SITE_PATH,
            '{app_path}' => APP_PATH,
            '{project_path}' => PROJECT_PATH,
            '{main_type}' => $main_type,
            '{host}' => $host,
            '{lang}' => $lang,
            '{out_type}' => $out_type,
            '{html5fw}' => $html5fw,
            '{year}' => $date[0],
            '{month}' => $date[1],
            '{day}' => $date[2],
            '{hour}' => $date[3],
            '{minute}' => $date[4],
            '{second}' => $date[5],
        ];
    }
public function _url_cache_path($url)

Cache file of a fetched url. Site id and project path are in the name: two sites sharing the dir do not read each other's entries.

    public function _url_cache_path($url)
    {
        $cache_dir = strlen($this->URL_FILE_CACHE_DIR) ? rtrim($this->URL_FILE_CACHE_DIR, '/') : '/tmp/yf_assets';
        return $cache_dir . '/' . md5($url) . '_' . (int) conf('SITE_ID') . '_' . substr(md5(PROJECT_PATH), 0, 8) . '.cache';
    }

_url_control

public function _url_control()
    public function _url_control()
    {
        if ( ! $this->ALLOW_URL_CONTROL) {
            return false;
        }
        if ($this->URL_CONTROL_REQUIRE_DEBUG && ! DEBUG_MODE && ! main()->is_console() && ! main()->ADMIN_ID && empty($_SESSION['admin_id'])) {
            return false;
        }
        $used = [];
        if (isset($_GET['assets_cache'])) {
            $this->USE_CACHE = (bool) ($_GET['assets_cache'] ?? null);
            $used[] = 'assets_cache';
        }
        if (isset($_GET['assets_combine'])) {
            $this->COMBINE = (bool) ($_GET['assets_combine'] ?? null);
            if ($this->COMBINE) {
                $this->USE_CACHE = true;
            }
            $used[] = 'assets_combine';
        }
        if (isset($_GET['assets_requirejs'])) {
            $this->USE_REQUIRE_JS = (bool) $_GET['assets_requirejs'];
            $used[] = 'assets_requirejs';
        }
        if (isset($_GET['assets_out_mtime'])) {
            $this->CACHE_OUT_ADD_MTIME = (bool) ($_GET['assets_out_mtime'] ?? null);
            $used[] = 'assets_out_mtime';
        }
        if (DEBUG_MODE) {
            if ($_GET['assets_do_cache'] ?? false) {
                $this->_do_cache();
                $used[] = 'assets_do_cache';
            }
            if ($_GET['assets_do_combine'] ?? false) {
                $this->_do_combine();
                $used[] = 'assets_do_combine';
            }
            if ($_GET['assets_do_purge'] ?? false) {
                $this->_do_purge();
                $used[] = 'assets_do_purge';
            }
        }
        if ($used) {
            header('X-Robots-Tag: noindex,nofollow,noarchive,nosnippet');
        }
    }
public function _url_get_contents($url)

Smart wrapper with temp file cache.

    public function _url_get_contents($url)
    {
        if ( ! strlen($url)) {
            return false;
        }
        // Possibly inline code: a URL never contains a line break or a tab
        if (preg_match('~[\r\n\t]~', $url)) {
            return $url;
        }
        $media_path = $this->_get_media_path();
        // Do not use web server for self-accessible paths
        if (substr($url, 0, strlen($media_path)) === $media_path) {
            $path = PROJECT_PATH . substr($url, strlen($media_path));
            // This line needed to strip query string from file name like this:
            // /templates/user/css/style.css?1416914173 -> templates/user/css/style.css
            $path = parse_url($path, PHP_URL_PATH);
            return file_get_contents($path);
        }
        $url = (substr($url, 0, 2) === '//' ? 'http:' : '') . $url;
        // Save syscall
        if ( ! isset($this->_time)) {
            $this->_time = time();
        }
        $cache_path = $this->_url_cache_path($url);
        $cache_dir = dirname($cache_path);
        // 24 hours tmp file cache
        if (file_exists($cache_path) && filemtime($cache_path) > ($this->_time - $this->URL_FILE_CACHE_TTL)) {
            return file_get_contents($cache_path);
        }
        $data = $this->http_get_contents($url);
        // Do not cache a failed fetch, it would be served as an empty file for the whole TTL
        if (false !== $data) {
            if ( ! file_exists($cache_dir)) {
                mkdir($cache_dir, strlen($this->URL_FILE_CACHE_DIR) ? 0750 : 0755, $recurse = true);
            }
            $this->_atomic_write($cache_path, $data);
        }
        return $data;
    }
public function _vendor_copy_css_ref($url, array $local)

Copy a vendored font / image of a stylesheet into the stylesheet's target dir, same layout as assets/vendor/.

  • $url string scheme-less CDN url of the file
  • $local array see _vendor_css_rewrite_urls()

Returns bool false: not vendored (or not copyable), the caller keeps the CDN url

    public function _vendor_copy_css_ref($url, array $local)
    {
        $entry = $this->_vendor_manifest()[$url] ?? null;
        if ( ! is_array($entry) || empty($entry['via'])) {
            return false;
        }
        if (preg_match('~^//cdn\.jsdelivr\.net/gh/(?P<owner>[^/@]+)/(?P<repo>[^/@]+)@(?P<ref>[^/]+)/(?P<path>.+)$~', $url, $m)) {
            $rel = $m['owner'] . '/' . $m['repo'] . '/' . $m['ref'] . '/' . $m['path'];
        } elseif (preg_match('~^//(?P<host>[^/@?#\s]+)/(?P<path>[^?#\s]+)$~', $url, $m)) {
            $rel = strtolower($m['host']) . '/' . $m['path'];
        } else {
            return false;
        }
        if (in_array('..', explode('/', $rel), true)) {
            return false;
        }
        $from = realpath($local['vendor_dir'] . '/' . $rel);
        if ( ! $from || strpos($from, $local['vendor_dir'] . DIRECTORY_SEPARATOR) !== 0 || ! is_file($from)) {
            return false;
        }
        $to = $local['target_dir'] . '/' . $rel;
        if (file_exists($to)) {
            return true;
        }
        $dir = dirname($to);
        ! file_exists($dir) && mkdir($dir, 0755, true);
        return false !== $this->_atomic_write($to, file_get_contents($from));
    }
public function _vendor_css_rewrite_urls($css, $base, $local = null)

Relative url() references inside a copied stylesheet: a font / image vendored for it (a manifest entry with `via`) is copied next to the copied stylesheet and the reference stays relative; any other one points at the CDN directory of the original file.

  • $base mixed CDN directory of the stylesheet, scheme-less
  • $local null|array ['vendor_dir' => realpath of assets/vendor, 'target_dir' => where the stylesheet copy lives]
    public function _vendor_css_rewrite_urls($css, $base, $local = null)
    {
        return preg_replace_callback('~url\(\s*([\'"]?)(?!data:|https?:|//|#)([^\'")]+)\1\s*\)~i', function ($m) use ($base, $local) {
            $parts = [];
            foreach (explode('/', rtrim($base, '/') . '/' . trim($m[2])) as $part) {
                if ($part === '..') {
                    array_pop($parts);
                } elseif ($part !== '.') {
                    $parts[] = $part;
                }
            }
            $url = implode('/', $parts);
            if (is_array($local) && $this->_vendor_copy_css_ref(preg_replace('~[?#].*$~', '', $url), $local)) {
                return $m[0];
            }
            return 'url(' . $m[1] . $url . $m[1] . ')';
        }, $css);
    }
public function _vendor_local_url($url)
    public function _vendor_local_url($url)
    {
        if ( ! $this->VENDOR_LOCAL) {
            return null;
        }
        if (preg_match('~^(?:https?:)?//cdn\.jsdelivr\.net/gh/(?P<owner>[^/@]+)/(?P<repo>[^/@]+)@(?P<ref>[^/]+)/(?P<path>.+)$~', $url, $m)) {
            $rel = $m['owner'] . '/' . $m['repo'] . '/' . $m['ref'] . '/' . $m['path'];
            $name = $m['repo'];
            $version = $m['ref'];
            $css_base = '//cdn.jsdelivr.net/gh/' . $m['owner'] . '/' . $m['repo'] . '@' . $m['ref'] . '/' . dirname($m['path']) . '/';
        } elseif (isset($this->_vendor_manifest()[$url]) && preg_match('~^(?:https?:)?//(?P<host>[^/@?#\s]+)/(?P<path>[^?#\s]+)$~', $url, $m)) {
            // Other hosts (cdnjs, ...) are vendored under assets/vendor/<host>/<path>; only manifest keys are looked up
            $rel = $m['host'] . '/' . $m['path'];
            $name = $m['host'];
            $version = 'master';
            $css_base = '//' . substr($rel, 0, (int) strrpos($rel, '/') + 1);
        } else {
            return null;
        }
        if (array_key_exists($url, $this->_cache_vendor)) {
            return $this->_cache_vendor[$url];
        }
        $this->_cache_vendor[$url] = null;
        if (in_array('..', explode('/', $rel), true)) {
            return null;
        }
        $vendor_dir = $this->VENDOR_DIR ?: YF_PATH . 'assets/vendor/';
        $base = realpath($vendor_dir);
        $from = realpath(rtrim($vendor_dir, '/') . '/' . $rel);
        if ( ! $base || ! $from || strpos($from, $base . DIRECTORY_SEPARATOR) !== 0 || ! is_file($from)) {
            return null;
        }
        $replace = $this->_get_asset_tpl_replace($name, $version);
        $target_dir = str_replace(['///', '//'], '/', str_replace(array_keys($replace), array_values($replace), $this->VENDOR_TARGET_DIR_TPL));
        $to_path = rtrim($target_dir, '/') . '/' . $rel;
        if ( ! file_exists($to_path)) {
            $dir_to = dirname($to_path);
            ! file_exists($dir_to) && mkdir($dir_to, 0755, true);
            $data = file_get_contents($from);
            if (substr($rel, -4) === '.css') {
                $data = $this->_vendor_css_rewrite_urls($data, $css_base, ['vendor_dir' => $base, 'target_dir' => rtrim($target_dir, '/')]);
            }
            if (false === $this->_atomic_write($to_path, $data)) {
                return null;
            }
        }
        $this->_cache_vendor[$url] = MEDIA_PATH . substr($to_path, strlen(PROJECT_PATH));
        return $this->_cache_vendor[$url];
    }
public function _vendor_manifest()
    public function _vendor_manifest()
    {
        if ($this->_cache_vendor_manifest === null) {
            $file = rtrim($this->VENDOR_DIR ?: YF_PATH . 'assets/vendor/', '/') . '/manifest.json';
            $data = is_file($file) ? json_decode((string) file_get_contents($file), true) : null;
            $this->_cache_vendor_manifest = is_array($data) ? $data : [];
        }
        return $this->_cache_vendor_manifest;
    }
public function _write_cache_info($cache_path, $url, $content, $extra = [])
    public function _write_cache_info($cache_path, $url, $content, $extra = [])
    {
        $data = [
            'url' => $url,
            'date' => date('Y-m-d H:i:s'),
            'md5' => md5($content),
        ];
        if ($extra) {
            foreach ($extra as $k => $v) {
                $data[$k] = $v;
            }
        }
        return $this->_atomic_write($cache_path . '.info', json_encode($data, JSON_PRETTY_PRINT));
    }