use namespace (WIP)

master
Jean-Christian Paul Denis 2023-04-24 20:28:05 +02:00
parent 707be9dd95
commit a2455b148d
Signed by: JcDenis
GPG Key ID: 1B5B8C5B90B6C951
10 changed files with 674 additions and 480 deletions

View File

@ -10,50 +10,87 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_CONTEXT_ADMIN')) {
return null;
}
require __DIR__ . '/_widgets.php';
// Admin menu
if (dcCore::app()->blog->settings->get(basename(__DIR__))->get('active')) {
dcCore::app()->menu[dcAdmin::MENU_PLUGINS]->addItem(
__('Post widget text'),
dcCore::app()->adminurl->get('admin.plugin.' . basename(__DIR__)),
dcPage::getPF(basename(__DIR__) . '/icon.svg'),
preg_match('/' . preg_quote(dcCore::app()->adminurl->get('admin.plugin.' . basename(__DIR__))) . '(&.*)?$/', $_SERVER['REQUEST_URI']),
dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([dcAuth::PERMISSION_CONTENT_ADMIN]), dcCore::app()->blog->id)
);
dcCore::app()->addBehavior('adminDashboardFavoritesV2', ['adminPostWidgetText', 'adminDashboardFavoritesV2']);
}
dcCore::app()->addBehaviors([
// Pref
'adminFiltersListsV2' => ['adminPostWidgetText', 'adminFiltersListsV2'],
'adminBlogPreferencesFormV2' => ['adminPostWidgetText', 'adminBlogPreferencesFormV2'],
'adminBeforeBlogSettingsUpdate' => ['adminPostWidgetText', 'adminBeforeBlogSettingsUpdate'],
// Post
'adminPostHeaders' => ['adminPostWidgetText', 'adminPostHeaders'],
'adminPostFormItems' => ['adminPostWidgetText', 'adminPostFormItems'],
'adminAfterPostUpdate' => ['adminPostWidgetText', 'adminAfterPostSave'],
'adminAfterPostCreate' => ['adminPostWidgetText', 'adminAfterPostSave'],
'adminBeforePostDelete' => ['adminPostWidgetText', 'adminBeforePostDelete'],
// Plugin "pages"
'adminPageHeaders' => ['adminPostWidgetText', 'adminPostHeaders'],
'adminPageFormItems' => ['adminPostWidgetText', 'adminPostFormItems'],
'adminAfterPageUpdate' => ['adminPostWidgetText', 'adminAfterPostSave'],
'adminAfterPageCreate' => ['adminPostWidgetText', 'adminAfterPostSave'],
'adminBeforePageDelete' => ['adminPostWidgetText', 'adminBeforePostDelete'],
]);
// Plugin "importExport"
if (dcCore::app()->blog->settings->get(basename(__DIR__))->get('importexport_active')) {
dcCore::app()->addBehaviors([
'exportFullV2' => ['adminPostWidgetText', 'exportFullV2'],
'exportSingleV2' => ['adminPostWidgetText', 'exportSingleV2'],
'importInitV2' => ['adminPostWidgetText', 'importInitV2'],
'importSingleV2' => ['adminPostWidgetText', 'importSingleV2'],
'importFullV2' => ['adminPostWidgetText', 'importFullV2'],
]);
declare(strict_types=1);
namespace Dotclear\Plugin\postWidgetText;
use dcAdmin;
use dcCore;
use dcPage;
use dcNsProcess;
class Backend extends dcNsProcess
{
public static function init(): bool
{
static::$init = defined('DC_CONTEXT_ADMIN')
&& My::phpCompliant()
&& !is_null(dcCore::app()->auth) && !is_null(dcCore::app()->blog)
&& dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([
dcCore::app()->auth::PERMISSION_USAGE,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]), dcCore::app()->blog->id);
return static::$init;
}
public static function process(): bool
{
if (!static::$init) {
return false;
}
// nullsafe
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->blog) || is_null(dcCore::app()->adminurl)) {
return false;
}
// backend sidebar menu icon
if (dcCore::app()->blog->settings->get(My::id())->get('active')) {
dcCore::app()->menu[dcAdmin::MENU_PLUGINS]->addItem(
My::name(),
dcCore::app()->adminurl->get('admin.plugin.' . My::id()),
dcPage::getPF(My::id() . '/icon.svg'),
preg_match('/' . preg_quote((string) dcCore::app()->adminurl->get('admin.plugin.' . My::id())) . '(&.*)?$/', $_SERVER['REQUEST_URI']),
dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([dcCore::app()->auth::PERMISSION_CONTENT_ADMIN]), dcCore::app()->blog->id)
);
// backend user dashboard favorites icon
dcCore::app()->addBehavior('adminDashboardFavoritesV2', [BackendBehaviors::class, 'adminDashboardFavoritesV2']);
}
// backend pwt management
dcCore::app()->addBehaviors([
// user pref
'adminFiltersListsV2' => [BackendBehaviors::class, 'adminFiltersListsV2'],
'adminBlogPreferencesFormV2' => [BackendBehaviors::class, 'adminBlogPreferencesFormV2'],
'adminBeforeBlogSettingsUpdate' => [BackendBehaviors::class, 'adminBeforeBlogSettingsUpdate'],
// post
'adminPostHeaders' => [BackendBehaviors::class, 'adminPostHeaders'],
'adminPostFormItems' => [BackendBehaviors::class, 'adminPostFormItems'],
'adminAfterPostUpdate' => [BackendBehaviors::class, 'adminAfterPostSave'],
'adminAfterPostCreate' => [BackendBehaviors::class, 'adminAfterPostSave'],
'adminBeforePostDelete' => [BackendBehaviors::class, 'adminBeforePostDelete'],
// Plugin "pages"
'adminPageHeaders' => [BackendBehaviors::class, 'adminPostHeaders'],
'adminPageFormItems' => [BackendBehaviors::class, 'adminPostFormItems'],
'adminAfterPageUpdate' => [BackendBehaviors::class, 'adminAfterPostSave'],
'adminAfterPageCreate' => [BackendBehaviors::class, 'adminAfterPostSave'],
'adminBeforePageDelete' => [BackendBehaviors::class, 'adminBeforePostDelete'],
// widgets registration
'initWidgets' => [Widgets::class, 'initWidgets'],
]);
// add plugin "importExport" features
if (!is_null(dcCore::app()->blog) && dcCore::app()->blog->settings->get(My::id())->get('importexport_active')) {
dcCore::app()->addBehaviors([
'exportFullV2' => [ImportExport::class, 'exportFullV2'],
'exportSingleV2' => [ImportExport::class, 'exportSingleV2'],
'importInitV2' => [ImportExport::class, 'importInitV2'],
'importSingleV2' => [ImportExport::class, 'importSingleV2'],
'importFullV2' => [ImportExport::class, 'importFullV2'],
]);
}
return true;
}
}

View File

@ -10,22 +10,29 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
declare(strict_types=1);
namespace Dotclear\Plugin\postWidgetText;
use ArrayObject;
use dcCore;
use dcFavorites;
use dcPage;
use dcSettings;
use Dotclear\Database\{
Cursor,
MetaRecord
};
use Dotclear\Helper\Html\Html;
use form;
/**
* @ingroup DC_PLUGIN_POSTWIDGETTEXT
* @brief postWidgetText - admin methods.
* @since 2.20
* Backend behaviors.
*/
class adminPostWidgetText
class BackendBehaviors
{
private static $ie_cursor;
private static $ie_pwt;
private static function id()
{
return basename(dirname(__DIR__));
}
public static function sortbyCombo()
public static function sortbyCombo(): array
{
return [
__('Post title') => 'post_title',
@ -35,7 +42,7 @@ class adminPostWidgetText
];
}
public static function adminFiltersListsV2($sorts)
public static function adminFiltersListsV2(ArrayObject $sorts): void
{
$sorts['pwt'] = [
__('Post widget text'),
@ -46,7 +53,7 @@ class adminPostWidgetText
];
}
public static function adminBlogPreferencesFormV2(dcSettings $blog_settings)
public static function adminBlogPreferencesFormV2(dcSettings $blog_settings): void
{
echo '
<div class="fieldset">
@ -54,12 +61,12 @@ class adminPostWidgetText
<div class="two-cols">
<div class="col">
<p><label for="active">' .
form::checkbox('active', 1, (bool) $blog_settings->get(self::id())->get('active')) .
form::checkbox('active', 1, (bool) $blog_settings->get(My::id())->get('active')) .
__('Enable post widget text on this blog') . '</label></p>
</div>
<div class="col">
<p><label for="importexport_active">' .
form::checkbox('importexport_active', 1, (bool) $blog_settings->get(self::id())->get('importexport_active')) .
form::checkbox('importexport_active', 1, (bool) $blog_settings->get(My::id())->get('importexport_active')) .
__('Enable import/export behaviors') . '</label></p>
</div>
</div>
@ -67,52 +74,59 @@ class adminPostWidgetText
</div>';
}
public static function adminBeforeBlogSettingsUpdate(dcSettings $blog_settings)
public static function adminBeforeBlogSettingsUpdate(dcSettings $blog_settings): void
{
$blog_settings->get(self::id())->put('active', !empty($_POST['active']));
$blog_settings->get(self::id())->put('importexport_active', !empty($_POST['importexport_active']));
$blog_settings->get(My::id())->put('active', !empty($_POST['active']));
$blog_settings->get(My::id())->put('importexport_active', !empty($_POST['importexport_active']));
}
public static function adminDashboardFavoritesV2(dcFavorites $favs)
public static function adminDashboardFavoritesV2(dcFavorites $favs): void
{
$favs->register(self::id(), [
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->adminurl)) {
return;
}
$favs->register(My::id(), [
'title' => __('Post widget text'),
'url' => dcCore::app()->adminurl->get('admin.plugin.' . self::id()),
'small-icon' => dcPage::getPF(self::id() . '/icon.svg'),
'large-icon' => dcPage::getPF(self::id() . '/icon.svg'),
'url' => dcCore::app()->adminurl->get('admin.plugin.' . My::id()),
'small-icon' => dcPage::getPF(My::id() . '/icon.svg'),
'large-icon' => dcPage::getPF(My::id() . '/icon.svg'),
'permissions' => dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_USAGE,
dcAuth::PERMISSION_CONTENT_ADMIN,
dcCore::app()->auth::PERMISSION_USAGE,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]),
]);
}
public static function adminPostHeaders()
public static function adminPostHeaders(): string
{
if (is_null(dcCore::app()->auth)) {
return '';
}
$editor = dcCore::app()->auth->getOption('editor');
return
dcCore::app()->callBehavior('adminPostEditor', $editor['xhtml'], 'pwt', ['#post_wtext'], 'xhtml') .
dcPage::jsModuleLoad(self::id() . '/js/post.js');
dcPage::jsModuleLoad(My::id() . '/js/backend.js');
}
public static function adminPostFormItems($main, $sidebar, $post)
public static function adminPostFormItems(ArrayObject $main, ArrayObject $sidebar, ?MetaRecord $post): void
{
# _POST fields
$title = $_POST['post_wtitle'] ?? '';
$content = $_POST['post_wtext'] ?? '';
# Existing post
if ($post) {
$post_id = (int) $post->post_id;
if (!is_null($post)) {
$post_id = (int) $post->f('post_id');
$pwt = new postWidgetText();
$w = $pwt->getWidgets(['post_id' => $post_id]);
$w = Utils::getWidgets(['post_id' => $post_id]);
# Existing widget
if (!$w->isEmpty()) {
$title = $w->option_title;
$content = $w->option_content;
$title = $w->f('option_title');
$content = $w->f('option_content');
}
}
@ -121,157 +135,60 @@ class adminPostWidgetText
'<p class="col">' .
'<label class="bold" for="post_wtitle">' . __('Widget title:') . '</label>' .
form::field('post_wtitle', 20, 255, html::escapeHTML($title), 'maximal') .
form::field('post_wtitle', 20, 255, Html::escapeHTML($title), 'maximal') .
'</p>' .
'<p class="area" id="post-wtext">' .
'<label class="bold" for="post_wtext">' . __('Wigdet text:') . '</label>' .
form::textarea('post_wtext', 50, 5, html::escapeHTML($content)) .
form::textarea('post_wtext', 50, 5, Html::escapeHTML($content)) .
'</p>' .
'</div>';
}
public static function adminAfterPostSave($cur, $post_id)
public static function adminAfterPostSave(Cursor $cur, int $post_id): void
{
$post_id = (int) $post_id;
# _POST fields
$title = $_POST['post_wtitle'] ?? '';
$content = $_POST['post_wtext'] ?? '';
# Object
$pwt = new postWidgetText();
# Get existing widget
$w = $pwt->getWidgets(['post_id' => $post_id]);
$w = Utils::getWidgets(['post_id' => (int) $post_id]);
# If new content is empty, delete old existing widget
if (empty($title) && empty($content) && !$w->isEmpty()) {
$pwt->delWidget($w->option_id);
Utils::delWidget((int) $w->f('option_id'));
}
# If new content is not empty
if (!empty($title) || !empty($content)) {
$wcur = $pwt->openCursor();
$wcur->post_id = $post_id;
$wcur->option_type = self::id();
$wcur->option_lang = $cur->post_lang;
$wcur->option_format = $cur->post_format;
$wcur->option_title = $title;
$wcur->option_content = $content;
$wcur = Utils::openCursor();
$wcur->setField('post_id', (int) $post_id);
$wcur->setField('option_type', My::id());
$wcur->setField('option_lang', $cur->getField('post_lang'));
$wcur->setField('option_format', $cur->getField('post_format'));
$wcur->setField('option_title', $title);
$wcur->setField('option_content', $content);
# Create widget
if ($w->isEmpty()) {
$id = $pwt->addWidget($wcur);
$id = Utils::addWidget($wcur);
}
# Upddate widget
else {
$pwt->updWidget($w->option_id, $wcur);
Utils::updWidget($w->f('option_id'), $wcur);
}
}
}
public static function adminBeforePostDelete($post_id)
public static function adminBeforePostDelete(int $post_id): void
{
$post_id = (int) $post_id;
# Object
$pwt = new postWidgetText();
# Get existing widget
$w = $pwt->getWidgets(['post_id' => $post_id]);
$w = Utils::getWidgets(['post_id' => (int) $post_id]);
# If new content is empty, delete old existing widget
if (!$w->isEmpty()) {
$pwt->delWidget($w->option_id);
}
}
public static function exportSingleV2($exp, $blog_id)
{
$exp->export(
self::id(),
'SELECT option_type, option_content, ' .
'option_content_xhtml, W.post_id ' .
'FROM ' . dcCore::app()->prefix . initPostWidgetText::PWT_TABLE_NAME . ' W ' .
'LEFT JOIN ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P ' .
'ON P.post_id = W.post_id ' .
"WHERE P.blog_id = '" . $blog_id . "' " .
"AND W.option_type = '" . dcCore::app()->con->escape(self::id()) . "' "
);
}
public static function exportFullV2($exp)
{
$exp->export(
self::id(),
'SELECT option_type, option_content, ' .
'option_content_xhtml, W.post_id ' .
'FROM ' . dcCore::app()->prefix . initPostWidgetText::PWT_TABLE_NAME . ' W ' .
'LEFT JOIN ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P ' .
'ON P.post_id = W.post_id ' .
"WHERE W.option_type = '" . dcCore::app()->con->escape(self::id()) . "' "
);
}
public static function importInitV2($bk)
{
self::$ie_cursor = dcCore::app()->con->openCursor(
dcCore::app()->prefix . initPostWidgetText::PWT_TABLE_NAME
);
self::$ie_pwt = new postWidgetText();
}
public static function importSingleV2($line, $bk)
{
if ($line->__name == self::id()
&& isset($bk->old_ids['post'][(int) $line->post_id])
) {
$line->post_id = $bk->old_ids['post'][(int) $line->post_id];
$exists = self::$ie_pwt->getWidgets([
'post_id' => $line->post_id,
]);
if ($exists->isEmpty()) {
self::$ie_cursor->clean();
self::$ie_cursor->post_id = (int) $line->post_id;
self::$ie_cursor->option_type = (string) $line->option_type;
self::$ie_cursor->option_lang = (string) $line->option_lang;
self::$ie_cursor->option_format = (string) $line->option_format;
self::$ie_cursor->option_content = (string) $line->option_content;
self::$ie_cursor->option_content_xhtml = (string) $line->option_content_xhtml;
self::$ie_pwt->addWidget(
self::$ie_cursor
);
}
}
}
public static function importFullV2($line, $bk)
{
if ($line->__name == self::id()) {
$exists = self::$ie_pwt->getWidgets([
'post_id' => $line->post_id,
]);
if ($exists->isEmpty()) {
self::$ie_cursor->clean();
self::$ie_cursor->post_id = (int) $line->post_id;
self::$ie_cursor->option_type = (string) $line->option_type;
self::$ie_cursor->option_format = (string) $line->option_format;
self::$ie_cursor->option_content = (string) $line->option_content;
self::$ie_cursor->option_content = (string) $line->option_content;
self::$ie_cursor->option_content_xhtml = (string) $line->option_content_xhtml;
self::$ie_pwt->addWidget(
self::$ie_cursor
);
}
Utils::delWidget($w->f('option_id'));
}
}
}

View File

@ -10,8 +10,30 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_RC_PATH')) {
return null;
}
declare(strict_types=1);
require __DIR__ . '/_widgets.php';
namespace Dotclear\Plugin\postWidgetText;
use dcCore;
use dcNsProcess;
class Frontend extends dcNsProcess
{
public static function init(): bool
{
static::$init = My::phpCompliant();
return static::$init;
}
public static function process(): bool
{
if (!static::$init) {
return false;
}
dcCore::app()->addBehavior('initWidgets', [Widgets::class, 'initWidgets']);
return true;
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* @brief postWidgetText, a plugin for Dotclear 2
*
* @package Dotclear
* @subpackage Plugin
*
* @author Jean-Christian Denis and Contributors
*
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
declare(strict_types=1);
namespace Dotclear\Plugin\postWidgetText;
use dcCore;
use dcBlog;
/**
* Plugin importExport features.
*/
class ImportExport
{
private static $ie_cursor;
public static function exportSingleV2($exp, $blog_id)
{
$exp->export(
My::id(),
'SELECT option_type, option_content, ' .
'option_content_xhtml, W.post_id ' .
'FROM ' . dcCore::app()->prefix . My::TABLE_NAME . ' W ' .
'LEFT JOIN ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P ' .
'ON P.post_id = W.post_id ' .
"WHERE P.blog_id = '" . $blog_id . "' " .
"AND W.option_type = '" . dcCore::app()->con->escapeStr((string) My::id()) . "' "
);
}
public static function exportFullV2($exp)
{
$exp->export(
My::id(),
'SELECT option_type, option_content, ' .
'option_content_xhtml, W.post_id ' .
'FROM ' . dcCore::app()->prefix . My::TABLE_NAME . ' W ' .
'LEFT JOIN ' . dcCore::app()->prefix . dcBlog::POST_TABLE_NAME . ' P ' .
'ON P.post_id = W.post_id ' .
"WHERE W.option_type = '" . dcCore::app()->con->escapeStr((string) My::id()) . "' "
);
}
public static function importInitV2($bk)
{
self::$ie_cursor = dcCore::app()->con->openCursor(
dcCore::app()->prefix . My::TABLE_NAME
);
}
public static function importSingleV2($line, $bk)
{
if ($line->__name == My::id()
&& isset($bk->old_ids['post'][(int) $line->post_id])
) {
$line->post_id = $bk->old_ids['post'][(int) $line->post_id];
$exists = Utils::getWidgets([
'post_id' => $line->post_id,
]);
if ($exists->isEmpty()) {
self::$ie_cursor->clean();
self::$ie_cursor->post_id = (int) $line->post_id;
self::$ie_cursor->option_type = (string) $line->option_type;
self::$ie_cursor->option_lang = (string) $line->option_lang;
self::$ie_cursor->option_format = (string) $line->option_format;
self::$ie_cursor->option_content = (string) $line->option_content;
self::$ie_cursor->option_content_xhtml = (string) $line->option_content_xhtml;
Utils::addWidget(
self::$ie_cursor
);
}
}
}
public static function importFullV2($line, $bk)
{
if ($line->__name == My::id()) {
$exists = Utils::getWidgets([
'post_id' => $line->post_id,
]);
if ($exists->isEmpty()) {
self::$ie_cursor->clean();
self::$ie_cursor->post_id = (int) $line->post_id;
self::$ie_cursor->option_type = (string) $line->option_type;
self::$ie_cursor->option_format = (string) $line->option_format;
self::$ie_cursor->option_content = (string) $line->option_content;
self::$ie_cursor->option_content = (string) $line->option_content;
self::$ie_cursor->option_content_xhtml = (string) $line->option_content_xhtml;
Utils::addWidget(
self::$ie_cursor
);
}
}
}
}

View File

@ -10,90 +10,82 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_CONTEXT_ADMIN')) {
return null;
}
declare(strict_types=1);
try {
// check installed version
if (!dcCore::app()->newVersion(
basename(__DIR__),
dcCore::app()->plugins->moduleInfo(basename(__DIR__), 'version')
)) {
return null;
namespace Dotclear\Plugin\postWidgetText;
use dcCore;
use dcNsProcess;
use Dotclear\Database\Structure;
use Exception;
class Install extends dcNsProcess
{
public static function init(): bool
{
static::$init = defined('DC_CONTEXT_ADMIN')
&& My::phpCompliant()
&& dcCore::app()->newVersion(My::id(), dcCore::app()->plugins->moduleInfo(My::id(), 'version'));
return static::$init;
}
// Table is the same for plugins pollsFactory, postTask, postWidgetText
$st = new dbStruct(dcCore::app()->con, dcCore::app()->prefix);
$st->{initPostWidgetText::PWT_TABLE_NAME}
->option_id('bigint', 0, false)
->post_id('bigint', 0, false)
->option_creadt('timestamp', 0, false, 'now()')
->option_upddt('timestamp', 0, false, 'now()')
->option_type('varchar', 32, false, "''")
->option_format('varchar', 32, false, "'xhtml'")
->option_lang('varchar', 5, true, null)
->option_title('varchar', 255, true, null)
->option_content('text', 0, true, null)
->option_content_xhtml('text', 0, false)
->index('idx_post_option_option', 'btree', 'option_id')
->index('idx_post_option_post', 'btree', 'post_id')
->index('idx_post_option_type', 'btree', 'option_type');
$si = new dbStruct(dcCore::app()->con, dcCore::app()->prefix);
$changes = $si->synchronize($st);
$current = dcCore::app()->getVersion(basename(__DIR__));
// Update settings id, ns
if ($current && version_compare($current, '2022.12.23', '<=')) {
$record = dcCore::app()->con->select(
'SELECT * FROM ' . dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME . ' ' .
"WHERE setting_ns = 'postwidgettext' "
);
while ($record->fetch()) {
if (preg_match('/^postwidgettext_(.*?)$/', $record->setting_id, $match)) {
$cur = dcCore::app()->con->openCursor(dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME);
$cur->setting_id = $match[1];
$cur->setting_ns = basename(__DIR__);
$cur->update(
"WHERE setting_id = '" . $record->setting_id . "' and setting_ns = 'postwidgettext' " .
'AND blog_id ' . (null === $record->blog_id ? 'IS NULL ' : ("= '" . dcCore::app()->con->escape($record->blog_id) . "' "))
);
}
public static function process(): bool
{
if (!static::$init) {
return false;
}
} else {
// Settings
dcCore::app()->blog->settings->get(basename(__DIR__))->put(
'active',
true,
'boolean',
'post widget text plugin enabled',
false,
true
);
dcCore::app()->blog->settings->get(basename(__DIR__))->put(
'importexport_active',
true,
'boolean',
'activate import/export behaviors',
false,
true
);
}
# Transfert records from old table to the new one
if (dcCore::app()->getVersion(basename(__DIR__)) !== null
&& version_compare(dcCore::app()->getVersion(basename(__DIR__)), '0.5', '<')
) {
require_once __DIR__ . '/inc/patch.0.5.php';
}
// nullsafe
if (is_null(dcCore::app()->blog)) {
return false;
}
return true;
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
try {
// Table is the same for plugins pollsFactory, postTask, postWidgetText
$s = new Structure(dcCore::app()->con, dcCore::app()->prefix);
$s->__get(My::TABLE_NAME)
->field('option_id', 'bigint', 0, false)
->field('post_id', 'bigint', 0, false)
->field('option_creadt', 'timestamp', 0, false, 'now()')
->field('option_upddt', 'timestamp', 0, false, 'now()')
->field('option_type', 'varchar', 32, false, "''")
->field('option_format', 'varchar', 32, false, "'xhtml'")
->field('option_lang', 'varchar', 5, true, null)
->field('option_title', 'varchar', 255, true, null)
->field('option_content', 'text', 0, true, null)
->field('option_content_xhtml', 'text', 0, false)
->index('idx_post_option_option', 'btree', 'option_id')
->index('idx_post_option_post', 'btree', 'post_id')
->index('idx_post_option_type', 'btree', 'option_type');
(new Structure(dcCore::app()->con, dcCore::app()->prefix))->synchronize($s);
// Settings
$s = dcCore::app()->blog->settings->get(My::id());
$s->put(
'active',
true,
'boolean',
'post widget text plugin enabled',
false,
true
);
$s->put(
'importexport_active',
true,
'boolean',
'activate import/export behaviors',
false,
true
);
return true;
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
return false;
}
}
}
return false;

View File

@ -10,84 +10,129 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_CONTEXT_ADMIN')) {
return null;
}
declare(strict_types=1);
dcPage::check(dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_USAGE,
dcAuth::PERMISSION_CONTENT_ADMIN,
]));
namespace Dotclear\Plugin\postWidgetText;
$pwt = new postWidgetText();
use dcAdminFilters;
use adminGenericFilterV2;
use dcCore;
use dcNsProcess;
use dcPage;
use Dotclear\Helper\Network\Http;
use Exception;
# Delete widgets
if (!empty($_POST['save']) && !empty($_POST['widgets'])) {
try {
foreach ($_POST['widgets'] as $k => $id) {
$id = (int) $id;
$pwt->delWidget($id);
use form;
class Manage extends dcNsProcess
{
public static function init(): bool
{
static::$init = defined('DC_CONTEXT_ADMIN')
&& My::phpCompliant()
&& !is_null(dcCore::app()->auth) && !is_null(dcCore::app()->blog)
&& dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([
dcCore::app()->auth::PERMISSION_USAGE,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]), dcCore::app()->blog->id);
return static::$init;
}
public static function process(): bool
{
if (!static::$init) {
return false;
}
dcAdminNotices::addSuccessNotice(
__('Posts widgets successfully delete.')
// nullsafe check
if (is_null(dcCore::app()->blog) || is_null(dcCore::app()->adminurl)) {
return false;
}
# Delete widgets
if (!empty($_POST['save']) && !empty($_POST['widgets'])) {
try {
foreach ($_POST['widgets'] as $k => $id) {
Utils::delWidget((int) $id);
}
dcPage::addSuccessNotice(
__('Posts widgets successfully delete.')
);
if (!empty($_POST['redir'])) {
Http::redirect($_POST['redir']);
} else {
dcCore::app()->adminurl->redirect('admin.plugin.' . My::id());
}
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
}
}
return true;
}
public static function render(): void
{
if (!static::$init) {
return;
}
// nullsafe check
if (is_null(dcCore::app()->blog) || is_null(dcCore::app()->adminurl)) {
return;
}
# filters
$filter = new adminGenericFilterV2('pwt');
$filter->add(dcAdminFilters::getPageFilter());
$params = $filter->params();
# Get posts with text widget
try {
$posts = Utils::getWidgets($params);
$counter = Utils::getWidgets($params, true);
$posts_list = new ManageList($posts, $counter->f(0));
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
$posts_list = null;
}
// display
dcPage::openModule(
My::name(),
dcPage::jsPageTabs() .
dcPage::jsModuleLoad(My::id() . '/js/manage.js') .
$filter->js(dcCore::app()->adminurl->get('admin.plugin.' . My::id()) . '#record')
);
if (!empty($_POST['redir'])) {
http::redirect($_POST['redir']);
} else {
dcCore::app()->adminurl->redirect('admin.plugin.' . basename(__DIR__));
echo
dcPage::breadcrumb([
__('Plugins') => '',
My::name() => '',
]) .
dcPage::notices();
if ($posts_list) {
$filter->display('admin.plugin.' . My::id(), form::hidden('p', My::id()));
$posts_list->display(
(int) $filter->value('page'),
(int) $filter->value('nb'),
'<form action="' . dcCore::app()->adminurl->get('admin.plugin.' . My::id()) . '" method="post" id="form-entries">' .
'%s' .
'<div class="two-cols">' .
'<p class="col checkboxes-helpers"></p>' .
'<p class="col right">' .
'<input id="do-action" type="submit" name="save" value="' . __('Delete selected widgets') . '" /></p>' .
dcCore::app()->adminurl->getHiddenFormFields('admin.plugin.' . My::id(), array_merge(['p' => My::id()], $filter->values(true))) .
dcCore::app()->formNonce() .
'</div>' .
'</form>'
);
}
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
dcPage::closeModule();
}
}
# filters
$filter = new adminGenericFilter(dcCore::app(), 'pwt');
$filter->add(dcAdminFilters::getPageFilter());
$params = $filter->params();
# Get posts with text widget
try {
$posts = $pwt->getWidgets($params);
$counter = $pwt->getWidgets($params, true);
$posts_list = new listPostWidgetText(dcCore::app(), $posts, $counter->f(0));
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
$posts_list = null;
}
# Display
echo '
<html><head><title>' . __('Post widget text') . '</title>' .
dcPage::jsModuleLoad(basename(__DIR__) . '/js/index.js') .
$filter->js(dcCore::app()->adminurl->get('admin.plugin.' . basename(__DIR__))) . '
</head>
<body>' .
dcPage::breadcrumb([
__('Plugins') => '',
__('Posts widgets') => '',
]) .
dcPage::notices();
if ($posts_list) {
$filter->display('admin.plugin.' . basename(__DIR__), form::hidden('p', basename(__DIR__)));
$posts_list->display(
$filter->page,
$filter->nb,
'<form action="' . dcCore::app()->adminurl->get('admin.plugin.' . basename(__DIR__)) . '" method="post" id="form-entries">' .
'%s' .
'<div class="two-cols">' .
'<p class="col checkboxes-helpers"></p>' .
'<p class="col right">' .
'<input id="do-action" type="submit" name="save" value="' . __('Delete selected widgets') . '" /></p>' .
dcCore::app()->adminurl->getHiddenFormFields('admin.plugin.' . basename(__DIR__), array_merge(['p' => basename(__DIR__)], $filter->values(true))) .
dcCore::app()->formNonce() .
'</div>' .
'</form>'
);
}
echo '</body></html>';

View File

@ -10,24 +10,33 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_CONTEXT_ADMIN')) {
return null;
}
declare(strict_types=1);
namespace Dotclear\Plugin\postWidgetText;
use adminGenericListV2;
use context;
use dcCore;
use dcPager;
use Dotclear\Helper\Date;
use Dotclear\Helper\Html\Html;
use form;
/**
* @ingroup DC_PLUGIN_POSTWIDGETTEXT
* @brief postWidgetText - admin list methods.
* @since 2.6
*/
class listPostWidgetText extends adminGenericList
class ManageList extends adminGenericListV2
{
public function display($page, $nb_per_page, $enclose = '')
public function display(int $page, int $nb_per_page, string $enclose = ''): void
{
if ($this->rs->isEmpty()) {
return '<p><strong>' . __('No widget') . '</strong></p>';
echo '<p><strong>' . __('No widget') . '</strong></p>';
}
$pager = new dcPager($page, $this->rs_count, $nb_per_page, 10);
$pager = new dcPager($page, (int) $this->rs_count, $nb_per_page, 10);
$pager->html_prev = $this->html_prev;
$pager->html_next = $this->html_next;
$pager->var_page = 'page';
@ -45,7 +54,7 @@ class listPostWidgetText extends adminGenericList
'</tr></thead><tbody>';
while ($this->rs->fetch()) {
$w_title = html::escapeHTML($this->rs->option_title);
$w_title = Html::escapeHTML($this->rs->option_title);
if ($w_title == '') {
$w_title = '<em>' . context::global_filters(
$this->rs->option_content,
@ -75,14 +84,14 @@ class listPostWidgetText extends adminGenericList
$this->rs->post_type,
$this->rs->post_id
) . '#post-wtext-form">' .
html::escapeHTML($this->rs->post_title) .
Html::escapeHTML($this->rs->post_title) .
'</a></td>' .
'<td class="nowrap">' . dt::dt2str(
'<td class="nowrap">' . Date::dt2str(
__('%Y-%m-%d %H:%M'),
$this->rs->post_dt
) . '</td>' .
'<td class="nowrap">' . $w_title . '</td>' .
'<td class="nowrap">' . dt::dt2str(
'<td class="nowrap">' . Date::dt2str(
__('%Y-%m-%d %H:%M'),
$this->rs->option_upddt
) . '</td>' .

53
src/My.php 100644
View File

@ -0,0 +1,53 @@
<?php
/**
* @brief postWidgetText, a plugin for Dotclear 2
*
* @package Dotclear
* @subpackage Plugin
*
* @author Jean-Christian Denis and Contributors
*
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
declare(strict_types=1);
namespace Dotclear\Plugin\postWidgetText;
use dcCore;
/**
* module definitions shortcuts
*/
class My
{
/** @var string Required php version */
public const PHP_MIN = '8.1';
/** @var string Plugin table name */
public const TABLE_NAME = 'post_option';
/**
* This module id
*/
public static function id(): string
{
return basename(dirname(__DIR__));
}
/**
* This module name
*/
public static function name(): string
{
return __((string) dcCore::app()->plugins->moduleInfo(self::id(), 'name'));
}
/**
* Check php version
*/
public static function phpCompliant(): bool
{
return version_compare(phpversion(), self::PHP_MIN, '>=');
}
}

View File

@ -10,55 +10,37 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_RC_PATH')) {
return null;
}
declare(strict_types=1);
namespace Dotclear\Plugin\postWidgetText;
use dcCore;
use Dotclear\Database\{
Cursor,
MetaRecord,
Structure
};
use Exception;
/**
* @ingroup DC_PLUGIN_POSTWIDGETTEXT
* @brief postWidgetText - admin and public methods.
* @since 2.6
*/
class postWidgetText
class Utils
{
public $con;
private $table;
private $blog;
public function __construct()
public static function openCursor(): Cursor
{
$this->con = dcCore::app()->con;
$this->table = dcCore::app()->prefix . initPostWidgetText::PWT_TABLE_NAME;
$this->blog = dcCore::app()->con->escape(dcCore::app()->blog->id);
return dcCore::app()->con->openCursor(dcCore::app()->prefix . My::TABLE_NAME);
}
public function tableName()
public static function getWidgets(array $params, bool $count_only = false): MetaRecord
{
return $this->table;
}
if (is_null(dcCore::app()->blog)) {
throw new Exception('blog is not set');
}
public function openCursor()
{
return $this->con->openCursor($this->table);
}
public function lockTable()
{
$this->con->writeLock($this->table);
}
public function unlockTable()
{
$this->con->unlock();
}
public function triggerBlog()
{
dcCore::app()->blog->triggerBlog();
}
public function getWidgets($params, $count_only = false)
{
if (!isset($params['columns'])) {
$params['columns'] = [];
}
@ -75,15 +57,15 @@ class postWidgetText
if (!isset($params['from'])) {
$params['from'] = '';
}
$params['join'] = 'LEFT JOIN ' . $this->table . ' W ON P.post_id=W.post_id ';
$params['join'] = 'LEFT JOIN ' . dcCore::app()->prefix . My::TABLE_NAME . ' W ON P.post_id=W.post_id ';
if (!isset($params['sql'])) {
$params['sql'] = '';
}
if (isset($params['option_type'])) {
$params['sql'] .= "AND W.option_type = '" . $this->con->escape($params['option_type']) . "' ";
$params['sql'] .= "AND W.option_type = '" . dcCore::app()->con->escapeStr((string) $params['option_type']) . "' ";
} else {
$params['sql'] .= "AND W.option_type = '" . $this->con->escape(basename(dirname(__DIR__))) . "' ";
$params['sql'] .= "AND W.option_type = '" . dcCore::app()->con->escapeStr((string) My::id()) . "' ";
}
unset($params['option_type']);
if (!isset($params['post_type'])) {
@ -93,51 +75,59 @@ class postWidgetText
return dcCore::app()->blog->getPosts($params, $count_only);
}
public function addWidget($cur)
public static function addWidget(Cursor $cur): int
{
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->blog)) {
throw new Exception('blog is not set');
}
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_USAGE,
dcAuth::PERMISSION_CONTENT_ADMIN,
]), $this->blog)) {
dcCore::app()->auth::PERMISSION_USAGE,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]), dcCore::app()->blog->id)) {
throw new Exception(__('You are not allowed to create an entry text widget'));
}
if ($cur->post_id == '') {
throw new Exception('No such entry ID');
}
$this->lockTable();
dcCore::app()->con->writeLock(dcCore::app()->prefix . My::TABLE_NAME);
try {
$rs = $this->con->select(
$rs = dcCore::app()->con->select(
'SELECT MAX(option_id) ' .
'FROM ' . $this->table
'FROM ' . dcCore::app()->prefix . My::TABLE_NAME
);
$cur->option_id = (int) $rs->f(0) + 1;
$cur->option_creadt = date('Y-m-d H:i:s');
$cur->option_upddt = date('Y-m-d H:i:s');
$this->getWidgetContent($cur, $cur->option_id);
self::getWidgetContent($cur, (int) $cur->option_id);
$cur->insert();
$this->unlockTable();
dcCore::app()->con->unlock();
} catch (Exception $e) {
$this->unlockTable();
dcCore::app()->con->unlock();
throw $e;
}
$this->triggerBlog();
dcCore::app()->blog->triggerBlog();
return $cur->option_id;
return (int) $cur->option_id;
}
public function updWidget($id, &$cur)
public static function updWidget(int $id, Cursor $cur): void
{
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->blog)) {
throw new Exception('blog is not set');
}
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_USAGE,
dcAuth::PERMISSION_CONTENT_ADMIN,
]), $this->blog)) {
dcCore::app()->auth::PERMISSION_USAGE,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]), dcCore::app()->blog->id)) {
throw new Exception(__('You are not allowed to update entries text widget'));
}
@ -147,17 +137,17 @@ class postWidgetText
throw new Exception(__('No such ID'));
}
$this->getWidgetContent($cur, $id);
self::getWidgetContent($cur, $id);
$cur->option_upddt = date('Y-m-d H:i:s');
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([dcAuth::PERMISSION_CONTENT_ADMIN]), $this->blog)) {
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([dcCore::app()->auth::PERMISSION_CONTENT_ADMIN]), dcCore::app()->blog->id)) {
$params['option_id'] = $id;
$params['user_id'] = $this->con->escape(dcCore::app()->auth->userID());
$params['user_id'] = dcCore::app()->con->escapeStr((string) dcCore::app()->auth->userID());
$params['no_content'] = true;
$params['limit'] = 1;
$rs = $this->getWidgets($params);
$rs = self::getWidgets($params);
if ($rs->isEmpty()) {
throw new Exception(__('You are not allowed to delete this entry text widget'));
@ -165,65 +155,52 @@ class postWidgetText
}
$cur->update('WHERE option_id = ' . $id . ' ');
$this->triggerBlog();
dcCore::app()->blog->triggerBlog();
}
public function delWidget($id, $type = '')
public static function delWidget(int $id, ?string $type = null): void
{
if (is_null(dcCore::app()->auth) || is_null(dcCore::app()->blog)) {
throw new Exception('blog is not set');
}
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_DELETE,
dcAuth::PERMISSION_CONTENT_ADMIN,
]), $this->blog)) {
dcCore::app()->auth::PERMISSION_DELETE,
dcCore::app()->auth::PERMISSION_CONTENT_ADMIN,
]), dcCore::app()->blog->id)) {
throw new Exception(__('You are not allowed to delete entries text widget'));
}
$id = (int) $id;
$type ??= basename(__DIR__);
$type ??= My::id();
if (empty($id)) {
throw new Exception(__('No such ID'));
}
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([dcAuth::PERMISSION_CONTENT_ADMIN]), $this->blog)) {
if (!dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([dcCore::app()->auth::PERMISSION_CONTENT_ADMIN]), dcCore::app()->blog->id)) {
$params['option_id'] = $id;
$params['user_id'] = $this->con->escape(dcCore::app()->auth->userID());
$params['user_id'] = dcCore::app()->con->escapeStr((string) dcCore::app()->auth->userID());
$params['no_content'] = true;
$params['limit'] = 1;
$rs = $this->getWidgets($params);
$rs = self::getWidgets($params);
if ($rs->isEmpty()) {
throw new Exception(__('You are not allowed to delete this entry text widget'));
}
}
$this->con->execute(
'DELETE FROM ' . $this->table . ' ' .
dcCore::app()->con->execute(
'DELETE FROM ' . dcCore::app()->prefix . My::TABLE_NAME . ' ' .
'WHERE option_id = ' . $id . ' ' .
"AND option_type = '" . $this->con->escape($type) . "' "
"AND option_type = '" . dcCore::app()->con->escapeStr((string) $type) . "' "
);
$this->triggerBlog();
dcCore::app()->blog->triggerBlog();
}
private function getWidgetContent(&$cur, $option_id)
{
$option_content = $cur->option_content;
$option_content_xhtml = $cur->option_content_xhtml;
$this->setWidgetContent(
$option_id,
$cur->option_format,
$cur->option_lang,
$option_content,
$option_content_xhtml
);
$cur->option_content = $option_content;
$cur->option_content_xhtml = $option_content_xhtml;
}
public function setWidgetContent($option_id, $format, $lang, &$content, &$content_xhtml)
public static function setWidgetContent(int $option_id, string $format, string $lang, ?string &$content, ?string &$content_xhtml): void
{
if ($format == 'wiki') {
dcCore::app()->initWikiPost();
@ -250,4 +227,21 @@ class postWidgetText
'content_xhtml' => &$content_xhtml,
]);
}
private static function getWidgetContent(Cursor $cur, int $option_id): void
{
$option_content = $cur->option_content;
$option_content_xhtml = $cur->option_content_xhtml;
self::setWidgetContent(
$option_id,
$cur->option_format,
$cur->option_lang,
$option_content,
$option_content_xhtml
);
$cur->option_content = $option_content;
$cur->option_content_xhtml = $option_content_xhtml;
}
}

View File

@ -10,26 +10,34 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_RC_PATH')) {
return null;
}
declare(strict_types=1);
dcCore::app()->addBehavior('initWidgets', ['postWidgetTextWidget', 'init']);
namespace Dotclear\Plugin\postWidgetText;
use dcCore;
use Dotclear\Helper\Html\Html;
use Dotclear\Plugin\widgets\WidgetsStack;
use Dotclear\Plugin\widgets\WidgetsElement;
/**
* @ingroup DC_PLUGIN_POSTWIDGETTEXT
* @brief postWidgetText - admin and public widget methods.
* @since 2.6
*/
class postWidgetTextWidget
class Widgets
{
public static function init($w)
/**
* Widget initialisation.
*
* @param WidgetsStack $w WidgetsStack instance
*/
public static function initWidgets(WidgetsStack $w): void
{
$w
->create(
basename(__DIR__),
__('Post widget text'),
['postWidgetTextWidget', 'display'],
[self::class, 'parseWidget'],
null,
__('Add a widget with a text related to an entry')
)
@ -51,41 +59,46 @@ class postWidgetTextWidget
->addOffline();
}
public static function display($w)
/**
* Parse widget.
*
* @param WidgetsElement $w WidgetsElement instance
*/
public static function parseWidget(WidgetsElement $w): string
{
if ($w->offline
|| !dcCore::app()->blog->settings->get(basename(__DIR__))->get('active')
if (is_null(dcCore::app()->blog)
|| is_null(dcCore::app()->ctx)
|| $w->__get('offline')
|| !dcCore::app()->blog->settings->get(My::id())->get('active')
|| !dcCore::app()->ctx->exists('posts')
|| !dcCore::app()->ctx->__get('posts')->post_id
|| !dcCore::app()->ctx->__get('posts')->f('post_id')
) {
return null;
return '';
}
$title = $w->title ?: null;
$title = $w->__get('title') ?: null;
$content = '';
$pwt = new postWidgetText();
$rs = $pwt->getWidgets(['post_id' => dcCore::app()->ctx->__get('posts')->post_id]);
$rs = Utils::getWidgets(['post_id' => dcCore::app()->ctx->__get('posts')->f('post_id')]);
if ($rs->isEmpty()) {
return null;
return '';
}
if ('' != $rs->option_title) {
$title = $rs->option_title;
if ('' != $rs->f('option_title')) {
$title = $rs->f('option_title');
}
if ('' != $rs->option_content_xhtml) {
$content = $rs->option_content_xhtml;
if ('' != $rs->f('option_content_xhtml')) {
$content = $rs->f('option_content_xhtml');
}
if ('' == $content && $w->excerpt) {
$content = dcCore::app()->ctx->__get('posts')->post_excerpt_xhtml;
if ('' == $content && $w->__get('excerpt')) {
$content = dcCore::app()->ctx->__get('posts')->f('post_excerpt_xhtml');
}
return $w->renderDiv(
$w->content_only,
basename(__DIR__) . ' ' . $w->class,
(bool) $w->__get('content_only'),
My::id() . ' ' . $w->__get('class'),
'',
($title ? $w->renderTitle(html::escapeHTML($title)) : '') . $content
($title ? $w->renderTitle(Html::escapeHTML($title)) : '') . $content
);
}
}