Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## **04.06.2026 Version 1.3.1**

- fix: verschiedene RexStan-Befunde behoben (u. a. null-safety beim Slice-Rendering, typisierte Konstruktor-Signaturen, generische Extension-Point-Annotationen)
- fix: stabileres Preview-Rendering durch defensivere Parameterbehandlung und saubere Ruecksetzung des REDAXO-Contexts bei `force_fe`
- fix: Settings-Formular abgesichert (Admin-null-check) und Template-Save validiert jetzt den erforderlichen Platzhalter `BLOCK_PEEK_CONTENT`
- ux: Assets werden nur noch auf `content/edit` geladen statt global im Backend
- chore: interne Dateizugriffe auf REDAXO-API (`rex_file::get`) vereinheitlicht

## **04.06.2026 Version 1.3.0**

- feat: resolve sprog wildcards (`{{ … }}`) in slice previews when the `sprog` addon is installed — parsed in the preview's clang before the iframe is built, so backend previews match the translated frontend output (sprog's own wildcard filter only runs on the frontend)
Expand Down
9 changes: 4 additions & 5 deletions boot.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

use FriendsOfRedaxo\BlockPeek\TemplateListHider;

/** @var rex_addon_interface $addon */
$addon = $this;
$addon = rex_addon::get('block_peek');

if ($addon->getConfig('inactive') !== '|1|') {
if (rex::isBackend() && rex::getUser()) {
rex_view::addJsFile($this->getAssetsUrl('BlockPeek.js'));
rex_view::addCssFile($this->getAssetsUrl('BlockPeek.css'));
if (rex::isBackend() && rex::getUser() && rex_be_controller::getCurrentPage() === 'content/edit') {
rex_view::addJsFile($addon->getAssetsUrl('BlockPeek.js'));
rex_view::addCssFile($addon->getAssetsUrl('BlockPeek.css'));
rex_extension::register('PACKAGES_INCLUDED', function () {
rex_extension::register('SLICE_BE_PREVIEW', \FriendsOfRedaxo\BlockPeek\Extension::register(...), rex_extension::LATE);
});
Expand Down
1 change: 1 addition & 0 deletions lang/de_de.lang
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ block_peek_no_permission = Du hast keine Berechtigung, diese Seite anzusehen.
block_peek_template = BlockPeek Template
block_peek_template_notice = <strong>Hinweis:</strong> Der Platzhalter <b>BLOCK_PEEK_CONTENT</b> ist Voraussetzung, um den eigentlichen Slice-Inhalt anzuzeigen. <br />Das Template wird als verstecktes REDAXO-Template gespeichert (Key: <code>block_peek_internal</code>). Bitte <code>REX_ARTICLE[...]</code> nicht hinzufügen, ausser alle Slices sollen gerendert werden. <br /><strong>Tailwind 4:</strong> Wenn das <code>developer</code> Addon installiert ist, wird das Template automatisch nach <code>data/addons/developer/templates/&lt;Name&gt; [&lt;id&gt;]/template.php</code> auf die Festplatte synchronisiert, sodass Tailwind die Utility-Klassen via <code>@source</code> findet.
block_peek_template_saved = Template gespeichert.
block_peek_template_missing_placeholder = Das Template muss den Platzhalter BLOCK_PEEK_CONTENT enthalten.

# Configuration — Cache
block_peek_cache_fieldset = Cache
Expand Down
1 change: 1 addition & 0 deletions lang/en_gb.lang
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ block_peek_no_permission = You do not have permission to view this page.
block_peek_template = BlockPeek Template
block_peek_template_notice = <strong>Note:</strong> The placeholder <b>BLOCK_PEEK_CONTENT</b> is required to display the actual slice content. <br />The template is stored as a hidden REDAXO template (key: <code>block_peek_internal</code>). Do not add <code>REX_ARTICLE[...]</code> unless you want all slices rendered. <br /><strong>Tailwind 4:</strong> if you use the <code>developer</code> addon, the template auto-syncs to disk at <code>data/addons/developer/templates/&lt;Name&gt; [&lt;id&gt;]/template.php</code> and Tailwind can discover utility classes via <code>@source</code>.
block_peek_template_saved = Template saved.
block_peek_template_missing_placeholder = The template must contain the BLOCK_PEEK_CONTENT placeholder.

# Configuration — Cache
block_peek_cache_fieldset = Cache
Expand Down
61 changes: 42 additions & 19 deletions lib/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,50 @@

class Extension
{
/**
* @param rex_extension_point<string> $ep
*/
public static function register(rex_extension_point $ep): void
{
/** @var rex_addon $addon */
$addon = rex_addon::get('block_peek');
$minHeight = (int) $addon->getConfig('iframe_min_height') ?: 300;
$zoomFactor = (float) $addon->getConfig('iframe_zoom_factor') ?: 0.5;
$sliceData = $ep->getParams();
$revision = $sliceData['revision'] ?? 0;
$slice = rex_article_slice::getArticleSliceById($sliceData['slice_id'], false, 0);
if (!$slice) {
$revision = 1;
$slice = rex_article_slice::getArticleSliceById($sliceData['slice_id'], false, 1);
}
$updateDate = $slice->getValue('updatedate');
$generator = new Generator(articleId: $sliceData['article_id'], clangId: $sliceData['clang'], sliceId: $sliceData['slice_id'], moduleId: $sliceData['module_id'], ctypeId: $sliceData['ctype'], updateDate: $updateDate, revision: $revision);
$content = $generator->getContent();
$html =
'<div class="block-peek-wrapper" data-zoom-factor="' . $zoomFactor . '" style="--block-peek-min-height: ' . $minHeight . 'px;">
<iframe data-iframe-preview data-slice-id="' . $sliceData['slice_id'] . '" scrolling="no"
srcdoc="' . htmlspecialchars($content) . '" frameborder="0" class="block-peek-iframe" style="--block-peek-zoom-factor: ' . $zoomFactor . ';"></iframe>
/** @var rex_addon $addon */
$addon = rex_addon::get('block_peek');
$minHeight = (int) $addon->getConfig('iframe_min_height') ?: 300;
$zoomFactor = (float) $addon->getConfig('iframe_zoom_factor') ?: 0.5;

$articleId = (int) $ep->getParam('article_id', 0);
$clangId = (int) $ep->getParam('clang', 0);
$sliceId = (int) $ep->getParam('slice_id', 0);
$revision = (int) $ep->getParam('revision', 0);

if ($articleId <= 0 || $clangId <= 0 || $sliceId <= 0) {
return;
}

$slice = rex_article_slice::getArticleSliceById($sliceId, false, 0);
if ($slice === null) {
$revision = 1;
$slice = rex_article_slice::getArticleSliceById($sliceId, false, 1);
}

$updateDateValue = $slice?->getValue('updatedate') ?? 0;
$updateDate = is_numeric($updateDateValue)
? (int) $updateDateValue
: (int) strtotime((string) $updateDateValue);

$generator = new Generator(
articleId: $articleId,
clangId: $clangId,
sliceId: $sliceId,
updateDate: $updateDate,
revision: $revision,
);

$content = $generator->getContent();
$html =
'<div class="block-peek-wrapper" data-zoom-factor="' . $zoomFactor . '" style="--block-peek-min-height: ' . $minHeight . 'px;">
<iframe data-iframe-preview data-slice-id="' . $sliceId . '" scrolling="no"
srcdoc="' . htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" frameborder="0" class="block-peek-iframe" style="--block-peek-zoom-factor: ' . $zoomFactor . ';"></iframe>
</div>';
$ep->setSubject($html);
$ep->setSubject($html);
}
}
71 changes: 36 additions & 35 deletions lib/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,19 @@ class Generator
private int $articleId = 0;
private int $clangId = 0;
private int $sliceId = 0;
private int $moduleId = 0;
private int $ctypeId = 0;
private int $updateDate = 0;
private int $revision = 0;

protected int $DEFAULT_TTL;
public bool $cacheActive = true;

public function __construct($articleId, $clangId, $sliceId, $ctypeId, $moduleId, $updateDate, $revision)
public function __construct(int $articleId, int $clangId, int $sliceId, int $updateDate, int $revision)
{
$this->addon = rex_addon::get('block_peek');

$this->articleId = $articleId;
$this->clangId = $clangId;
$this->sliceId = $sliceId;
$this->ctypeId = $ctypeId;
$this->moduleId = $moduleId;
$this->updateDate = $updateDate;
$this->revision = $revision;

Expand Down Expand Up @@ -104,41 +100,46 @@ private function fetchTemplateUpdateDate(int $templateId): int
private function prepareOutput(int $templateId): string
{
$forceFeContext = (bool) $this->addon->getConfig('force_fe', false);
$wasBackendContext = rex::getProperty('redaxo');
if ($forceFeContext) {
rex::setProperty('redaxo', false);
}

$context = new rex_article_content($this->articleId, $this->clangId);
$context->setSliceRevision($this->revision);
$context->setTemplateId($templateId);

$wrapperHtml = $context->getArticleTemplate();
$sliceHtml = $context->getSlice($this->sliceId);

$sliceHtml = '<div class="block-peek-content">' . $sliceHtml . '</div>';
$html = str_replace('BLOCK_PEEK_CONTENT', $sliceHtml, $wrapperHtml);

$html = $this->injectPosterAndStyles($html);
$html = $this->setHtmlLang($html);

// Resolve sprog wildcards ({{ … }}) for the preview's language. sprog only
// runs its OUTPUT_FILTER on the frontend (sprog/boot.php: if (!rex::isBackend())),
// so without this the backend preview shows raw wildcards. Pass the preview's
// clang explicitly — Wildcard::parse() defaults to rex_clang::getCurrentId(),
// which in the backend is the admin's clang, not necessarily the one previewed.
if (rex_addon::get('sprog')->isAvailable()) {
$html = \Sprog\Wildcard::parse($html, $this->clangId);
try {
$context = new rex_article_content($this->articleId, $this->clangId);
$context->setSliceRevision($this->revision);
$context->setTemplateId($templateId);

$wrapperHtml = $context->getArticleTemplate();
$sliceHtml = $context->getSlice($this->sliceId);

$sliceHtml = '<div class="block-peek-content">' . $sliceHtml . '</div>';
$html = str_replace('BLOCK_PEEK_CONTENT', $sliceHtml, $wrapperHtml);

$html = $this->injectPosterAndStyles($html);
$html = $this->setHtmlLang($html);

// Resolve sprog wildcards ({{ … }}) for the preview's language. sprog only
// runs its OUTPUT_FILTER on the frontend (sprog/boot.php: if (!rex::isBackend())),
// so without this the backend preview shows raw wildcards. Pass the preview's
// clang explicitly — Wildcard::parse() defaults to rex_clang::getCurrentId(),
// which in the backend is the admin's clang, not necessarily the one previewed.
if (rex_addon::get('sprog')->isAvailable()) {
$html = \Sprog\Wildcard::parse($html, $this->clangId);
}

return rex_extension::registerPoint(new rex_extension_point('BLOCK_PEEK_OUTPUT', $html, [
'article_id' => $this->articleId,
'clang' => $this->clangId,
'slice_id' => $this->sliceId,
'updateDate' => $this->updateDate,
'revision' => $this->revision,
]));
} finally {
if ($forceFeContext) {
rex::setProperty('redaxo', $wasBackendContext);
}
}

$html = rex_extension::registerPoint(new rex_extension_point('BLOCK_PEEK_OUTPUT', $html, [
'article_id' => $this->articleId,
'clang' => $this->clangId,
'slice_id' => $this->sliceId,
'updateDate' => $this->updateDate,
'revision' => $this->revision,
]));

return $html;
}

private function injectPosterAndStyles(string $html): string
Expand Down
5 changes: 3 additions & 2 deletions lib/TemplateInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use rex_config;
use rex_extension;
use rex_extension_point;
use rex_file;
use rex_sql;
use rex_template;
use rex_template_cache;
Expand Down Expand Up @@ -106,8 +107,8 @@ private static function buildContent(): string
private static function loadDefaultContent(): string
{
$path = rex_addon::get('block_peek')->getPath('templates/default.php');
$content = @file_get_contents($path);
if ($content === false) {
$content = rex_file::get($path);
if (!is_string($content) || $content === '') {
return "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n</head>\n<body>\nBLOCK_PEEK_CONTENT\n</body>\n</html>\n";
}
return $content;
Expand Down
5 changes: 2 additions & 3 deletions lib/TemplateListHider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class TemplateListHider
* OUTPUT_FILTER (LATE) on the templates list page only. Strips the <tr> for our
* hidden row by matching template_id=<id> with a non-digit boundary so id "1"
* doesn't accidentally match "10".
*
* @param rex_extension_point<string> $ep
*/
public static function register(rex_extension_point $ep): void
{
Expand All @@ -34,9 +36,6 @@ public static function register(rex_extension_point $ep): void
$hideId = $template->getId();

$content = $ep->getSubject();
if (!is_string($content)) {
return;
}

$result = preg_replace_callback(
'/<tr\b[^>]*>.*?<\/tr>/s',
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "block_peek-assets",
"version": "1.3.0",
"version": "1.3.1",
"description": "Asset build for BlockPeek Addon",
"scripts": {
"version:sync": "node scripts/sync-version.js",
Expand Down
2 changes: 1 addition & 1 deletion package.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package: block_peek
version: '1.3.0'
version: '1.3.1'
license: MIT
author: Friends Of REDAXO, Yves Torres
supportpage: https://github.com/FriendsOfREDAXO/block_peek
Expand Down
39 changes: 22 additions & 17 deletions pages/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

/** @var rex_addon $this */

if (!rex::getUser()->isAdmin()) {
$user = rex::getUser();
if ($user === null || !$user->isAdmin()) {
echo rex_view::error(rex_i18n::msg('block_peek_no_permission'));
return;
}
Expand All @@ -26,21 +27,25 @@
) {
$newContent = rex_post('block_peek_template_content', 'string', '');

$sql = rex_sql::factory();
$sql->setTable(rex::getTable('template'));
$sql->setWhere('id = :id', ['id' => $templateId]);
$sql->setValue('content', $newContent);
$sql->addGlobalUpdateFields();
$sql->update();

rex_template_cache::delete($templateId);
rex_template_cache::generate($templateId);
rex_extension::registerPoint(new rex_extension_point('TEMPLATE_UPDATED', '', ['id' => $templateId]));

$templateMessage = rex_view::success(rex_i18n::msg('block_peek_template_saved'));

// Re-fetch by id (we just confirmed it exists above; safer than forKey here).
$template = new rex_template($templateId);
if (strpos($newContent, 'BLOCK_PEEK_CONTENT') === false) {
$templateMessage = rex_view::error(rex_i18n::msg('block_peek_template_missing_placeholder'));
} else {
$sql = rex_sql::factory();
$sql->setTable(rex::getTable('template'));
$sql->setWhere('id = :id', ['id' => $templateId]);
$sql->setValue('content', $newContent);
$sql->addGlobalUpdateFields();
$sql->update();

rex_template_cache::delete($templateId);
rex_template_cache::generate($templateId);
rex_extension::registerPoint(new rex_extension_point('TEMPLATE_UPDATED', '', ['id' => $templateId]));

$templateMessage = rex_view::success(rex_i18n::msg('block_peek_template_saved'));

// Re-fetch by id (we just confirmed it exists above; safer than forKey here).
$template = new rex_template($templateId);
}
}

$currentContent = (string) $template->getTemplate();
Comment thread
skerbis marked this conversation as resolved.
Expand Down Expand Up @@ -124,7 +129,7 @@ class="form-control rex-code rex-html-code"
$field->setAttribute('min', '0.1');
$field->setAttribute('max', '1.0');
$field->setAttribute('step', '0.05');
$field->setAttribute('placeholder', '0,5');
$field->setAttribute('placeholder', '0.5');
$field->setAttribute('style', 'width: 80px;');

$field = $form->addCheckboxField('force_fe');
Expand Down