yf_core_blocks class

Core blocks methods.

Fileplugins/sys/classes/yf_core_blocks.class.php
Use_class('core_blocks')

Public properties

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

PropertyDefaultDescription
$CENTER_BLOCK_ID null
$CENTER_BLOCK_RULES null
$CHECK_RIGHTS_IN_TASKS
bool
false Apply the center block rules inside tasks() too, not only in prefetch_center() (AUDIT 5.8)
$CSRF_CHECK_POST_FOR
array
['admin' => true, 'user' => false] tasks() verifies the CSRF token of every POST for these MAIN_TYPEs, once csrf_guard is enabled for them
$FORCE_ALLOWED_CLASSES ['api']
$POST_ONLY_ACTIONS
array
['delete', 'delete_*', 'login_as', 'clear_*', 'user_var_delete', 'user_var_push'] fnmatch() patterns of state-changing actions that tasks() answers with a confirm form when requested by GET
$REDIRECT_CALLBACK_403 null
$REDIRECT_CALLBACK_404 null
$TASK_DENIED_403_HEADER false
$TASK_NOT_FOUND_404_HEADER false
$_blocks_infos null
$_blocks_rules null
$_error_no_blocks_raised null
$_rights_checked
bool
false @conf_skip
$cache__get_denied_tasks_names null Memo of _get_denied_tasks_names(), set through a variable property name.

Public methods 20

public function prefetch_center()

Try to run center block module/method if allowed.

    public function prefetch_center()
    {
        $block_name = 'center_area';
        if ( ! isset($this->_blocks_infos)) {
            $this->_blocks_infos = main()->get_data('blocks_all');
        }
        if (empty($this->_blocks_infos)) {
            return false;
        }
        $BLOCK_EXISTS = false;
        $block_id = null;
        foreach ((array) $this->_blocks_infos as $block_info) {
            if (trim($block_info['type']) != MAIN_TYPE) {
                continue;
            }
            if ($block_info['name'] == $block_name) {
                $BLOCK_EXISTS = true;
                $block_id = $block_info['id'];
                break;
            }
        }
        if ( ! $BLOCK_EXISTS) {
            return false;
        }
        if ( ! $this->_blocks_infos[$block_id]['active']) {
            return false;
        }
        if ( ! $this->_check_block_rights($block_id, ($_GET['object'] ?? null), ($_GET['action'] ?? null))) {
            return _class('graphics')->_action_on_block_denied($block_name);
        }
        $this->_rights_checked = true;
        return $this->tasks($allowed_check = true);
    }

show_block

public function show_block($params = [])

Alias for the '_show_block'.

    public function show_block($params = [])
    {
        return $this->_show_block($params);
    }

show_center

public function show_center()

Display main 'center' block contents.

    public function show_center()
    {
        return $this->tasks($allowed_check = true);
    }

tasks

public function tasks($allowed_check = false)

Main $_GET tasks handler.

    public function tasks($allowed_check = false)
    {
        $main = main();
        if ($main->is_console() || $main->is_ajax()) {
            $main->no_graphics(true);
        }
        // Singleton
        $_center_result = tpl()->_CENTER_RESULT;
        if (isset($_center_result)) {
            return $_center_result;
        }
        $not_found = false;
        $access_denied = false;
        $custom_handler_exists = false;
        $body = null;

        $OBJECT = &$_GET['object'];
        $ACTION = &$_GET['action'];

        _class('router')->_route_request();
        // Check if called class method is 'private' - then do not use it
        // Also we protect here core classes that can be instantinated before this method and can be allowed by mistake
        // Use other module names, think about this list as "reserved" words
        if (substr($ACTION, 0, 1) == '_' || ! strlen($OBJECT) || substr($OBJECT, 0, strlen(YF_PREFIX)) === YF_PREFIX || in_array($OBJECT, $this->_get_denied_tasks_names())) {
            $access_denied = true;
        }
        $rights_denied = false;
        if ( ! $access_denied && $this->CHECK_RIGHTS_IN_TASKS && ! $this->_rights_checked) {
            $center_block_id = $this->_get_center_block_id();
            if ($center_block_id && ! $this->_check_block_rights($center_block_id, $OBJECT, $ACTION)) {
                // same answer as prefetch_center() gives for a denied block
                $rights_denied = true;
                $access_denied = true;
            }
        }
        $csrf_verdict = $access_denied ? '' : $this->_csrf_verdict($ACTION);
        $csrf_denied = ($csrf_verdict === 'denied');
        if ($csrf_denied) {
            $access_denied = true;
            $allowed_check = true;
        }
        if ( ! $access_denied) {
            $obj = module($OBJECT);
            if ( ! is_object($obj)) {
                $not_found = true;
            }
            if ( ! $not_found && ! method_exists($obj, $ACTION)) {
                $not_found = true;
            }
            // Check if we have custom action handler in module (catch all requests to module methods)
            if (method_exists($obj, $main->MODULE_ACTION_HANDLER)) {
                $custom_handler_exists = true;
            }
            if ( ! $not_found || $custom_handler_exists) {
                if ($csrf_verdict === 'confirm') {
                    $not_found = false;
                    $body = $this->_confirm_action_form($OBJECT, $ACTION);
                } elseif ($custom_handler_exists) {
                    $not_found = false;
                    $body = $obj->{$main->MODULE_ACTION_HANDLER}($ACTION, $main->_ARGS_DIRTY);
                } else {
                    $is_banned = false;
                    if (MAIN_TYPE_USER && $main->AUTO_BAN_CHECKING) {
                        $is_banned = _class('ban_status')->_auto_check([]);
                    }
                    if ($is_banned) {
                        $body = _e();
                    } else {
                        $body = $obj->$ACTION();
                    }
                }
            }
        }
        $redirect_func = function ($url) use ($OBJECT, $ACTION) {
            $redir_params = [
                '%%object%%' => $OBJECT,
                '%%action%%' => $ACTION,
                '%%add_get_vars%%' => str_replace('&', ';', _add_get(['object', 'action'])),
            ];
            $redir_url = str_replace(array_keys($redir_params), array_values($redir_params), $url);
            if ( ! empty($redir_url)) {
                redirect($redir_url, 1, tpl()->parse('system/error_not_found'));
            }
        };
        if ($rights_denied) {
            $body = _class('graphics')->_action_on_block_denied('center_area');
        } elseif ($not_found) {
            $body = $this->_not_found_body($OBJECT, $ACTION, $redirect_func);
        } elseif ($allowed_check && $access_denied) {
            $body = $this->_denied_body($OBJECT, $ACTION, $csrf_denied, $redirect_func);
        }
        $block_name = 'center_area';
        $events = _class('core_events');
        $prepend = $events->fire('block.prepend[' . $block_name . ']');
        $append = $events->fire('block.append[' . $block_name . ']', [&$body]);
        $body = ($prepend ? implode(PHP_EOL, $prepend) : '') . $body . ($append ? implode(PHP_EOL, $append) : '');
        // Singleton
        tpl()->_CENTER_RESULT = (string) $body;
        // Output only center content, when we are inside AJAX_MODE
        if ($main->is_ajax()) {
            echo $body;
        }
        return $body;
    }

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);
    }
public function _action_on_block_denied($block_name = '')

Action to on denied block.

    public function _action_on_block_denied($block_name = '')
    {
        if ($block_name == 'center_area') {
            if ($this->TASK_DENIED_403_HEADER) {
                header((($_SERVER['SERVER_PROTOCOL'] ?? null) ? ($_SERVER['SERVER_PROTOCOL'] ?? null) : 'HTTP/1.1') . ' 403 Forbidden');
                main()->IS_403 = true;
            }
            if (MAIN_TYPE_USER && ! main()->USER_ID) {
                $redir_params = [
                    '%%object%%' => ($_GET['object'] ?? null),
                    '%%action%%' => ($_GET['action'] ?? null),
                    '%%add_get_vars%%' => str_replace('&', ';', _add_get(['object', 'action'])),
                ];
                $redir_url = str_replace(array_keys($redir_params), array_values($redir_params), main()->REDIR_URL_DENIED);
                if ( ! empty($redir_url)) {
                    if (($_GET['object'] ?? null) == 'login_form') {
                        return 'Access to login form denied on center block (' . __CLASS__ . '.' . __FUNCTION__ . ')';
                    }
                    return js_redirect($redir_url);
                }
            } elseif ((MAIN_TYPE_USER && main()->USER_ID) || (MAIN_TYPE_ADMIN && main()->ADMIN_ID)) {
                if ($this->_error_pages()) {
                    return $this->_denied_page();
                }
                return '<div class="alert alert-error alert-danger">' . t('Access denied') . '</div>';
            }
        }
        return false;
    }
public function _check_block_rights($block_id = 0, $OBJECT = '', $ACTION = '')

Check rights for blocks.

    public function _check_block_rights($block_id = 0, $OBJECT = '', $ACTION = '')
    {
        if (empty($block_id) || empty($OBJECT)) {
            return false;
        }
        if (empty($ACTION)) {
            $ACTION = 'show';
        }
        $CUR_USER_GROUP = (int) (MAIN_TYPE_ADMIN ? ($_SESSION['admin_group'] ?? null) : ($_SESSION['user_group'] ?? null));
        $CUR_USER_THEME = conf('theme');
        $CUR_LOCALE = conf('language');
        $CUR_SITE = (int) conf('SITE_ID');
        $CUR_SERVER_ID = (int) conf('SERVER_ID');
        $CUR_SERVER_ROLE = conf('SERVER_ROLE');
        $RESULT = false;
        if ( ! isset($this->_blocks_rules)) {
            $this->_load_blocks_rules();
        }
        foreach ((array) $this->_blocks_rules as $rule_id => $rule_info) {
            if ($rule_info['block_id'] != $block_id) {
                continue;
            }
            $matched_method = false;
            $matched_user_group = false;
            $matched_theme = false;
            $matched_locale = false;
            $matched_site = false;
            $matched_server_id = false;
            $matched_server_role = false;
            // Check matches
            if (is_array($rule_info['methods']) && (in_array($OBJECT, $rule_info['methods']) || in_array($OBJECT . '.' . $ACTION, $rule_info['methods']))) {
                $matched_method = true;
            }
            if (is_array($rule_info['user_groups']) && in_array($CUR_USER_GROUP, $rule_info['user_groups'])) {
                $matched_user_group = true;
            }
            if (is_array($rule_info['themes']) && in_array($CUR_USER_THEME, $rule_info['themes'])) {
                $matched_theme = true;
            }
            if (is_array($rule_info['locales']) && in_array($CUR_LOCALE, $rule_info['locales'])) {
                $matched_locale = true;
            }
            if (is_array($rule_info['site_ids']) && in_array($CUR_SITE, $rule_info['site_ids'])) {
                $matched_site = true;
            }
            if (is_array($rule_info['server_ids']) && in_array($CUR_SERVER_ID, $rule_info['server_ids'])) {
                $matched_server_id = true;
            }
            if (is_array($rule_info['server_roles']) && in_array($CUR_SERVER_ROLE, $rule_info['server_roles'])) {
                $matched_server_role = true;
            }
            if (( ! is_array($rule_info['methods']) || $matched_method)
                && ( ! is_array($rule_info['user_groups']) || $matched_user_group)
                && ( ! is_array($rule_info['themes']) || $matched_theme || ! $CUR_USER_THEME)
                && ( ! is_array($rule_info['locales']) || $matched_locale || ! $CUR_LOCALE)
                && ( ! is_array($rule_info['site_ids']) || $matched_site || ! $CUR_SITE)
                && ( ! is_array($rule_info['server_ids']) || $matched_server_id || ! $CUR_SERVER_ID)
                && ( ! is_array($rule_info['server_roles']) || $matched_server_role || ! $CUR_SERVER_ROLE)
            ) {
                $RESULT = trim($rule_info['rule_type']) == 'ALLOW' ? true : false;
            }
        }
        return $RESULT;
    }
public function _confirm_action_form($object, $action)

Interstitial for a GET to a POST-only action: a form that re-sends the same URL by POST with the session token.

    public function _confirm_action_form($object, $action)
    {
        $guard = _class('csrf_guard');
        if (main()->is_ajax()) {
            // scripts cannot click the confirm button: they must POST with the token (yf_js_admin does)
            if ( ! headers_sent()) {
                header(($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . ' 403 Forbidden');
            }
            return _e(t('This action changes data and needs a POST request with the CSRF token'));
        }
        if ( ! headers_sent()) {
            header('X-Frame-Options: SAMEORIGIN');
        }
        $replace = [
            'object' => _htmlchars($object),
            'action' => _htmlchars($action),
            'form_action' => _htmlchars($_SERVER['REQUEST_URI'] ?? './'),
            'token_name' => _htmlchars($guard->TOKEN_NAME),
            'token' => $guard->session_token(),
            'cancel_url' => _htmlchars($_SERVER['HTTP_REFERER'] ?? './'),
        ];
        $body = tpl()->parse('system/confirm_action', $replace);
        if ( ! strlen((string) $body)) {
            $body = '<div class="alert alert-warning">' . t('Confirm the action') . ': ' . $replace['object'] . '.' . $replace['action'] . '</div>'
                . '<form method="post" action="' . $replace['form_action'] . '">'
                . '<input type="hidden" name="' . $replace['token_name'] . '" value="' . $replace['token'] . '" />'
                . '<button type="submit" class="btn btn-danger">' . t('Confirm') . '</button> '
                . '<a href="' . $replace['cancel_url'] . '" class="btn btn-default">' . t('Cancel') . '</a>'
                . '</form>';
        }
        return $body;
    }

_csrf_verdict

public function _csrf_verdict($action, $main_type = null, $is_post = null)

'' (go on), 'denied' (POST without a valid token) or 'confirm' (GET to a state-changing action). Nothing happens until csrf_guard is enabled for the MAIN_TYPE and CSRF_CHECK_POST_FOR allows it.

  • $main_type null|string
  • $is_post null|bool
    public function _csrf_verdict($action, $main_type = null, $is_post = null)
    {
        $main_type = $main_type ?: MAIN_TYPE;
        $guard = _class('csrf_guard');
        if ( ! $guard->is_enabled($main_type) || empty($this->CSRF_CHECK_POST_FOR[$main_type])) {
            return '';
        }
        $guard->send_cookie();
        if ($is_post === null) {
            // PUT, PATCH and DELETE change state too: everything but GET/HEAD/OPTIONS carries a token
            $is_post = ! in_array(strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'), ['GET', 'HEAD', 'OPTIONS']);
        }
        if ($is_post) {
            return $guard->check(null, $main_type) ? '' : 'denied';
        }
        return $this->_is_post_only_action($action) ? 'confirm' : '';
    }

_denied_body

public function _denied_body($OBJECT, $ACTION, $csrf_denied, $redirect_func)

Decision of the access denied branch of tasks(), same contract as _not_found_body().

    public function _denied_body($OBJECT, $ACTION, $csrf_denied, $redirect_func)
    {
        $main = main();
        $body = null;
        $main->BLOCKS_TASK_403 = true;
        if ($this->TASK_DENIED_403_HEADER) {
            header(($_SERVER['SERVER_PROTOCOL'] ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1') . ' 403 Forbidden');
            $main->IS_403 = true;
        }
        trigger_error(__CLASS__ . ': Access denied: ' . $OBJECT . '.' . $ACTION, E_USER_WARNING);
        if ($csrf_denied) {
            _class('csrf_guard')->log_error(['form_id' => $OBJECT . '.' . $ACTION]);
            $body = _e(t('Invalid CSRF token. Reload the page and send the request again. If you did not send this request then close this page.'));
        } elseif (MAIN_TYPE_USER && $main->USER_ID && $this->_error_pages() && ! $this->REDIRECT_CALLBACK_403) {
            $body = $this->_denied_page();
        } elseif (MAIN_TYPE_USER) {
            $redir_func = $this->REDIRECT_CALLBACK_403 ?: $redirect_func;
            $redir_func($main->REDIR_URL_DENIED);
        }
        return $body;
    }

_denied_page

public function _denied_page()
    public function _denied_page()
    {
        return _class('core_errors')->_render_error_page(403, 'system/error_403');
    }

_error_pages

public function _error_pages()

Opt-in through yf_core_errors::$ERROR_PAGES.

    public function _error_pages()
    {
        return ! empty(_class('core_errors')->ERROR_PAGES);
    }
public function _get_center_block_id()

Try to find id of the center block.

    public function _get_center_block_id()
    {
        if ( ! isset($this->_blocks_infos)) {
            $this->_blocks_infos = main()->get_data('blocks_all');
        }
        $center_block_id = 0;
        foreach ((array) $this->_blocks_infos as $cur_block_id => $cur_block_info) {
            if ($cur_block_info['type'] == MAIN_TYPE && trim($cur_block_info['name']) == 'center_area') {
                $center_block_id = $cur_block_id;
                break;
            }
        }
        return $center_block_id;
    }
public function _get_center_block_rules()
    public function _get_center_block_rules()
    {
        $rules = &$this->CENTER_BLOCK_RULES;
        if (isset($rules)) {
            return $rules;
        }
        $this->CENTER_BLOCK_ID = $this->_get_center_block_id();

        $rules = [];
        foreach ((array) $this->_blocks_rules as $rid => $rinfo) {
            if ($rinfo != $this->CENTER_BLOCK_ID) {
                continue;
            }
            $rules[$rid] = $rinfo;
        }

        $this->CENTER_BLOCK_RULES = $rules;
        return $rules;
    }
public function _get_denied_tasks_names()
    public function _get_denied_tasks_names()
    {
        $cache = 'cache_' . __FUNCTION__;
        if (isset($this->$cache)) {
            return $this->$cache;
        }
        $names = [];
        $ext = '.class.php';
        $ext_len = strlen($ext);
        $prefix = YF_PREFIX;
        $plen = strlen($prefix);
        $patterns = [
            YF_PATH . 'classes/*' . $ext,
            YF_PATH . 'plugins/*/classes/*' . $ext,
            PROJECT_PATH . 'classes/*' . $ext,
            PROJECT_PATH . 'plugins/*/classes/*' . $ext,
            APP_PATH . 'classes/*' . $ext,
            APP_PATH . 'plugins/*/classes/*' . $ext,
        ];
        foreach ($patterns as $glob) {
            foreach (glob($glob) as $path) {
                $name = substr(basename($path), 0, -$ext_len);
                if (substr($name, 0, $plen) === $prefix) {
                    $name = substr($name, $plen);
                }
                $names[$name] = $name;
            }
        }
        if ($exclude = $this->FORCE_ALLOWED_CLASSES) {
            foreach ((array) $exclude as $name) {
                if (isset($names[$name])) {
                    unset($names[$name]);
                }
            }
        }
        $this->$cache = $names;
        return $names;
    }
public function _is_post_only_action($action)
    public function _is_post_only_action($action)
    {
        foreach ((array) $this->POST_ONLY_ACTIONS as $pattern) {
            if (fnmatch($pattern, (string) $action)) {
                return true;
            }
        }
        return false;
    }
public function _load_blocks_rules()

Load array of blocks rules.

    public function _load_blocks_rules()
    {
        if ( ! empty($this->_blocks_rules)) {
            return false;
        }
        $rules = main()->get_data('blocks_rules');
        $rule_names_to_skip = ['id', 'block_id', 'rule_type', 'active', 'order'];
        foreach ((array) $rules as $rule_id => $rule_info) {
            foreach ((array) $rule_info as $rule_name => $rule_text) {
                if (in_array($rule_name, $rule_names_to_skip) || empty($rule_text)) {
                    continue;
                }
                $rule_text = trim(str_replace([' ', "\t", "\r", "\n", '"', "'", ',,'], '', $rule_text), ',');
                $rule_text = explode(',', $rule_text);
                $rules[$rule_id][$rule_name] = $rule_text;
            }
        }
        $this->_blocks_rules = $rules;
    }
public function _not_found_body($OBJECT, $ACTION, $redirect_func)

Decision of the not found branch of tasks(): body of the center block, null when a redirect or output was made.

    public function _not_found_body($OBJECT, $ACTION, $redirect_func)
    {
        $main = main();
        $body = null;
        $main->BLOCKS_TASK_404 = true;
        if ($this->TASK_NOT_FOUND_404_HEADER) {
            header(($_SERVER['SERVER_PROTOCOL'] ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1') . ' 404 Not Found');
            $main->IS_404 = true;
        }
        if (_class('graphics')->NOT_FOUND_RAISE_WARNING) {
            trigger_error(__CLASS__ . ': Task not found: ' . $OBJECT . '.' . $ACTION, E_USER_WARNING);
        }
        if (MAIN_TYPE_USER) {
            $u = $main->REDIR_URL_NOT_FOUND;
            if (is_array($u) && ! empty($u)) {
                // Prefill GET keys from redirect url
                foreach (['object', 'action', 'id', 'page'] as $k) {
                    $_GET[$k] = $u[$k];
                }
                if ( ! empty($u['object'])) {
                    $action = ($u['action'] ?? null) ?: 'show';
                    $body = _class_safe($u['object'], $u['path'])->$action();
                } elseif (isset($u['stpl'])) {
                    $main->no_graphics(true);
                    echo tpl()->parse($u['stpl']);
                }
            } elseif ($this->_error_pages() && ! $this->REDIRECT_CALLBACK_404) {
                $body = $this->_not_found_page($OBJECT, $ACTION);
            } else {
                $redir_func = $this->REDIRECT_CALLBACK_404 ?: $redirect_func;
                $redir_func($u);
            }
        } elseif ($this->_error_pages()) {
            $body = $this->_not_found_page($OBJECT, $ACTION);
        }
        return $body;
    }
public function _not_found_page($object = '', $action = '')
    public function _not_found_page($object = '', $action = '')
    {
        return _class('core_errors')->_render_error_page(404, 'system/error_404', [
            'object' => _prepare_html($object),
            'action' => _prepare_html($action),
        ]);
    }

_show_block

public function _show_block($input = [])

Show custom block contents.

    public function _show_block($input = [])
    {
        if ( ! isset($this->_blocks_infos)) {
            $this->_blocks_infos = main()->get_data('blocks_all');
        }
        if (empty($this->_blocks_infos)) {
            if ( ! $this->_error_no_blocks_raised) {
                trigger_error(__CLASS__ . ': Blocks names not loaded', E_USER_WARNING);
                $this->_error_no_blocks_raised = true;
            }
            return false;
        }
        $BLOCK_EXISTS = false;
        $block_id = null;
        $block_name = null;
        if (isset($input['block_id']) && is_numeric($input['block_id'])) {
            $block_info = $this->_blocks_infos[$input['block_id']];
            if ($block_info && trim($block_info['type']) == MAIN_TYPE) {
                $block_id = $input['block_id'];
                $block_name = $block_info['name'];
                $BLOCK_EXISTS = true;
            }
        } else {
            $block_name = $input['name'];
            if (empty($block_name)) {
                trigger_error(__CLASS__ . ': Given empty block name to show', E_USER_WARNING);
                return false;
            }
            foreach ((array) $this->_blocks_infos as $block_info) {
                // Skip blocks from other init type ('admin' or 'user')
                if (trim($block_info['type']) != MAIN_TYPE) {
                    continue;
                }
                // Found!
                if ($block_info['name'] == $block_name) {
                    $BLOCK_EXISTS = true;
                    $block_id = $block_info['id'];
                    break;
                }
            }
        }
        if ( ! $BLOCK_EXISTS) {
            trigger_error(__CLASS__ . ': block "' . _prepare_html($block_name) . '" not found in blocks list', E_USER_WARNING);
            return false;
        }
        if ( ! $this->_blocks_infos[$block_id]['active']) {
            return false;
        }
        if ( ! $this->_check_block_rights($block_id, ($_GET['object'] ?? null), ($_GET['action'] ?? null))) {
            return $this->_action_on_block_denied($block_name);
        }
        $cur_block_info = $this->_blocks_infos[$block_id];
        // 	If special object method specified - then call it
        //	Syntax: [path_to]$class_name.$method_name
        //	@example 'static_pages.show'
        //	@example 'classes/minicalendar.createcalendar'
        if ( ! empty($cur_block_info['method_name'])) {
            $special_path = '';
            if (false !== strpos($cur_block_info['method_name'], '/')) {
                $special_path = substr($cur_block_info['method_name'], 0, strrpos($cur_block_info['method_name'], '/') + 1);
                $cur_block_info['method_name'] = substr($cur_block_info['method_name'], strrpos($cur_block_info['method_name'], '/') + 1);
            }
            list($special_class_name, $special_method_name) = explode('.', $cur_block_info['method_name']);
            $special_params = [
                'block_name' => $block_name,
                'block_id' => $block_id,
            ];
            if ( ! empty($special_class_name) && ! empty($special_method_name)) {
                $obj = _class_safe($special_class_name, $special_path);
                if (is_object($obj) && method_exists($obj, $special_method_name)) {
                    return $obj->$special_method_name($special_params);
                }
                trigger_error(__CLASS__ . ': block "' . _prepare_html($block_name) . '" custom php module.method not exists: ' . _prepare_html($cur_block_info['method_name']), E_USER_WARNING);
                return false;
            }
        }
        $prepend = _class('core_events')->fire('block.prepend[' . $block_name . ']');

        $body = tpl()->parse(($cur_block_info['stpl_name'] ?? null) ?: $block_name, [
            'block_name' => $block_name,
            'block_id' => $block_id,
        ]);

        $append = _class('core_events')->fire('block.append[' . $block_name . ']', [&$body]);

        return ($prepend ? implode(PHP_EOL, $prepend) : '') . $body . ($append ? implode(PHP_EOL, $append) : '');
    }