use namespace

master
Jean-Christian Paul Denis 2023-03-19 21:17:24 +01:00
parent f1d7cb368e
commit dae470074c
Signed by: JcDenis
GPG Key ID: 1B5B8C5B90B6C951
10 changed files with 622 additions and 646 deletions

View File

@ -1,18 +0,0 @@
<?php
/**
* @brief tweakStores, 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);
$admin = implode('\\', ['Dotclear', 'Plugin', basename(__DIR__), 'Admin']);
if ($admin::init()) {
$admin::process();
}

View File

@ -1,19 +0,0 @@
<?php
/**
* @brief tweakStores, 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);
$config = implode('\\', ['Dotclear', 'Plugin', basename(__DIR__), 'Config']);
if ($config::init()) {
$config::process();
$config::render();
}

View File

@ -1,22 +0,0 @@
<?php
/**
* @brief tweakStores, 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);
$prepend = implode('\\', ['Dotclear', 'Plugin', basename(__DIR__), 'Prepend']);
if (!class_exists($prepend)) {
require implode(DIRECTORY_SEPARATOR, [__DIR__, 'inc', 'Prepend.php']);
if ($prepend::init()) {
$prepend::process();
}
}

View File

@ -1,239 +0,0 @@
<?php
/**
* @brief tweakStores, 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\tweakStores;
/* clearbricks ns */
use files;
use text;
use xmlTag;
/* php ns */
use DOMDocument;
use Exception;
class Core
{
/** @var array List of notice messages */
public static $notice = [];
/** @var array List of failed messages */
public static $failed = [];
# taken from lib.moduleslist.php
public static function sanitizeModule(string $id, array $module): array
{
$label = empty($module['label']) ? $id : $module['label'];
$name = __(empty($module['name']) ? $label : $module['name']);
$oname = empty($module['name']) ? $label : $module['name'];
return array_merge(
# Default values
[
'desc' => '',
'author' => '',
'version' => 0,
'current_version' => 0,
'root' => '',
'root_writable' => false,
'permissions' => null,
'parent' => null,
'priority' => 1000,
'standalone_config' => false,
'support' => '',
'section' => '',
'tags' => '',
'details' => '',
'sshot' => '',
'score' => 0,
'type' => null,
'requires' => [],
'settings' => [],
'repository' => '',
'dc_min' => 0,
],
# Module's values
$module,
# Clean up values
[
'id' => $id,
'sid' => self::sanitizeString($id),
'label' => $label,
'name' => $name,
'oname' => $oname,
'sname' => self::sanitizeString($name),
]
);
}
# taken from lib.moduleslist.php
public static function sanitizeString(string $str): string
{
return (string) preg_replace('/[^A-Za-z0-9\@\#+_-]/', '', strtolower($str));
}
public static function parseFilePattern(string $id, array $module, string $file_pattern): string
{
$module = self::sanitizeModule($id, $module);
return text::tidyURL(str_replace(
[
'%type%',
'%id%',
'%version%',
'%author%',
],
[
$module['type'],
$module['id'],
$module['version'],
$module['author'],
],
$file_pattern
));
}
public static function generateXML(string $id, array $module, string $file_pattern): string
{
if (!is_array($module) || empty($module)) {
return '';
}
$module = self::sanitizeModule($id, $module);
$rsp = new xmlTag('module');
self::$notice = [];
self::$failed = [];
# id
if (empty($module['id'])) {
self::$failed[] = 'unknow module';
}
$rsp->id = $module['id'];
# name
if (empty($module['name'])) {
self::$failed[] = 'no module name set in _define.php';
}
$rsp->name($module['oname']);
# version
if (empty($module['version'])) {
self::$failed[] = 'no module version set in _define.php';
}
$rsp->version($module['version']);
# author
if (empty($module['author'])) {
self::$failed[] = 'no module author set in _define.php';
}
$rsp->author($module['author']);
# desc
if (empty($module['desc'])) {
self::$failed[] = 'no module description set in _define.php';
}
$rsp->desc($module['desc']);
# repository
if (empty($module['repository'])) {
self::$failed[] = 'no repository set in _define.php';
}
# file
$file_pattern = self::parseFilePattern($id, $module, $file_pattern);
if (empty($file_pattern)) {
self::$failed[] = 'no zip file pattern set in Tweak Store configuration';
}
$rsp->file($file_pattern);
# da dc_min or requires core
if (!empty($module['requires']) && is_array($module['requires'])) {
foreach ($module['requires'] as $req) {
if (!is_array($req)) {
$req = [$req];
}
if ($req[0] == 'core') {
$module['dc_min'] = $req[1];
break;
}
}
}
if (empty($module['dc_min'])) {
self::$notice[] = 'no minimum dotclear version';
} else {
$rsp->insertNode(new xmlTag('da:dcmin', $module['dc_min']));
}
# details
if (empty($module['details'])) {
self::$notice[] = 'no details URL';
} else {
$rsp->insertNode(new xmlTag('da:details', $module['details']));
}
# section
if (!empty($module['section'])) {
$rsp->insertNode(new xmlTag('da:section', $module['section']));
}
# support
if (empty($module['support'])) {
self::$notice[] = 'no support URL';
} else {
$rsp->insertNode(new xmlTag('da:support', $module['support']));
}
$res = new xmlTag('modules', $rsp);
$res->insertAttr('xmlns:da', 'http://dotaddict.org/da/');
return self::prettyXML($res->toXML());
}
public static function writeXML(string $id, array $module, string $file_pattern): bool
{
self::$failed = [];
if (!$module['root_writable']) {
return false;
}
$content = self::generateXML($id, $module, $file_pattern);
if (!empty(self::$failed)) {
return false;
}
try {
files::putContent($module['root'] . '/dcstore.xml', $content);
} catch (Exception $e) {
self::$failed[] = $e->getMessage();
return false;
}
return true;
}
public static function prettyXML(string $str): string
{
if (class_exists('DOMDocument')) {
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($str);
return (string) $dom->saveXML();
}
return (string) str_replace('><', ">\n<", $str);
}
}

View File

@ -1,50 +0,0 @@
<?php
/**
* @brief tweakStores, 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\tweakStores;
/* clearbricks ns */
use Clearbricks;
class Prepend
{
private const LIBS = [
'Admin',
'Config',
'Core',
];
protected static $init = false;
public static function init(): bool
{
self::$init = defined('DC_RC_PATH');
return self::$init;
}
public static function process(): ?bool
{
if (!self::$init) {
return false;
}
foreach (self::LIBS as $lib) {
Clearbricks::lib()->autoload([
__NAMESPACE__ . '\\' . $lib => __DIR__ . DIRECTORY_SEPARATOR . $lib . '.php',
]);
}
return true;
}
}

View File

@ -14,270 +14,38 @@ declare(strict_types=1);
namespace Dotclear\Plugin\tweakStores;
/* dotclear ns */
use dcCore;
use dcPage;
use dcNsProcess;
/* clearbricks ns */
use form;
use html;
/* php ns */
use Exception;
class Admin
class Backend extends dcNsProcess
{
private static $pid = '';
protected static $init = false;
public static function init(): bool
{
if (defined('DC_CONTEXT_ADMIN')
&& dcCore::app()->auth->isSuperAdmin()
&& dcCore::app()->blog->settings->get(basename(__NAMESPACE__))->get('active')
) {
dcCore::app()->auth->user_prefs->addWorkspace('interface');
self::$pid = basename(dirname(__DIR__));
self::$init = true;
}
static::$init = My::phpCompliant()
&& defined('DC_CONTEXT_ADMIN')
&& dcCore::app()->auth->isSuperAdmin()
&& dcCore::app()->blog->settings->get(My::id())->get('active');
return self::$init;
return static::$init;
}
public static function process(): ?bool
public static function process(): bool
{
if (!self::$init) {
if (!static::$init) {
return false;
}
if (dcCore::app()->blog->settings->get(self::$pid)->get('packman')) {
// create dcstore.xml file on the fly when plugin packman pack a module
dcCore::app()->addBehavior('packmanBeforeCreatePackage', function (array $module): void {
Core::writeXML($module['id'], $module, dcCore::app()->blog->settings->get(self::$pid)->get('file_pattern'));
});
}
dcCore::app()->addBehaviors([
// addd some js
'pluginsToolsHeadersV2' => [self::class, 'modulesToolsHeaders'],
'themesToolsHeadersV2' => [self::class, 'modulesToolsHeaders'],
// admin plugins page tab
'pluginsToolsTabsV2' => function (): void {
self::modulesToolsTabs(dcCore::app()->plugins->getModules(), explode(',', DC_DISTRIB_PLUGINS), dcCore::app()->adminurl->get('admin.plugins'));
},
// admin themes page tab
'themesToolsTabsV2' => function (): void {
self::modulesToolsTabs(dcCore::app()->themes->getModules(), explode(',', DC_DISTRIB_THEMES), dcCore::app()->adminurl->get('admin.blog.theme'));
},
'pluginsToolsHeadersV2' => [BackendBehaviors::class, 'modulesToolsHeaders'],
'themesToolsHeadersV2' => [BackendBehaviors::class, 'modulesToolsHeaders'],
// admin modules page tab
'pluginsToolsTabsV2' => [BackendBehaviors::class, 'pluginsToolsTabsV2'],
'themesToolsTabsV2' => [BackendBehaviors::class, 'themesToolsTabsV2'],
// add to plugin pacKman
'packmanBeforeCreatePackage' => [BackendBehaviors::class, 'packmanBeforeCreatePackage'],
]);
return true;
}
public static function modulesToolsHeaders(bool $is_plugin): string
{
return
dcPage::jsJson('ts_copied', ['alert' => __('Copied to clipboard')]) .
dcPage::jsModuleLoad(self::$pid . '/js/admin.js') .
(
!dcCore::app()->auth->user_prefs->interface->colorsyntax ? '' :
dcPage::jsLoadCodeMirror(dcCore::app()->auth->user_prefs->interface->colorsyntax_theme) .
dcPage::jsModuleLoad(self::$pid . '/js/cms.js')
);
}
protected static function modulesToolsTabs(array $modules, array $excludes, string $page_url): void
{
$page_url .= '#' . self::$pid;
$user_ui_colorsyntax = dcCore::app()->auth->user_prefs->interface->colorsyntax;
$user_ui_colorsyntax_theme = dcCore::app()->auth->user_prefs->interface->colorsyntax_theme;
$combo = self::comboModules($modules, $excludes);
$file_pattern = dcCore::app()->blog->settings->get(self::$pid)->get('file_pattern');
# check dcstore repo
$url = '';
if (!empty($_POST['checkxml_id']) && in_array($_POST['checkxml_id'], $combo)) {
if (empty($modules[$_POST['checkxml_id']]['repository'])) {
$url = __('This module has no repository set in its _define.php file.');
} else {
try {
$url = $modules[$_POST['checkxml_id']]['repository'];
if (false === strpos($url, 'dcstore.xml')) {
$url .= '/dcstore.xml';
}
if (function_exists('curl_init')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_REFERER, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$file_content = curl_exec($ch);
curl_close($ch);
} else {
$file_content = file_get_contents($url);
}
} catch (Exception $e) {
$file_content = __('Failed to read third party repository');
}
}
}
# generate xml code
if (!empty($_POST['buildxml_id']) && in_array($_POST['buildxml_id'], $combo)) {
$xml_content = Core::generateXML($_POST['buildxml_id'], $modules[$_POST['buildxml_id']], $file_pattern);
}
# write dcstore.xml file
if (!empty($_POST['write_xml'])) {
if (empty($_POST['your_pwd']) || !dcCore::app()->auth->checkPassword($_POST['your_pwd'])) {
dcCore::app()->error->add(__('Password verification failed'));
} else {
$ret = Core::writeXML($_POST['buildxml_id'], $modules[$_POST['buildxml_id']], $file_pattern);
if (!empty(Core::$failed)) {
dcCore::app()->error->add(implode(' ', Core::$failed));
}
}
}
echo
'<div class="multi-part" id="' . self::$pid . '" title="' . dcCore::app()->plugins->moduleInfo(self::$pid, 'name') . '">' .
'<h3>' . __('Tweak third-party repositories') . '</h3>';
if (!empty($_POST['write_xml'])) {
if (dcCore::app()->error->flag()) {
echo dcCore::app()->error->toHTML();
} else {
echo '<p class="success">' . __('File successfully written') . '</p>';
}
}
if (count($combo) < 2) {
echo
'<div class="info">' . __('There is no module to tweak') . '</div>' .
'</div>';
return;
}
echo
'<form method="post" action="' . $page_url . '" id="checkxml" class="fieldset">' .
'<h4>' . __('Check repository') . '</h4>' .
'<p>' . __('This checks if dcstore.xml file is present on third party repository.') . '</p>' .
'<p class="field"><label for="buildxml_id" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Module to parse:') . '</label> ' .
form::combo('checkxml_id', $combo, empty($_POST['checkxml_id']) ? '-' : html::escapeHTML($_POST['checkxml_id'])) .
'</p>' .
'<p><input type="submit" name="check_xml" value="' . __('Check') . '" />' .
dcCore::app()->formNonce() . '</p>' .
'</form>';
if (!empty($url)) {
echo
'<div class="fieldset">' .
'<h4>' . __('Repositiory contents') . '</h4>' .
'<p>' . $url . '</p>' .
(
empty($file_content) ? '' :
'<pre>' . form::textArea('file_xml', 165, 14, [
'default' => html::escapeHTML(Core::prettyXML($file_content)),
'class' => 'maximal',
'extra_html' => 'readonly="true"',
]) . '</pre>' .
(
!$user_ui_colorsyntax ? '' :
dcPage::jsRunCodeMirror('editor', 'file_xml', 'dotclear', $user_ui_colorsyntax_theme)
)
) .
'</div>';
}
if (empty($file_pattern)) {
echo sprintf(
'<div class="fieldset"><h4>' . __('Generate xml code') . '</h4><p class="info"><a href="%s">%s</a></p></div>',
dcCore::app()->adminurl->get('admin.plugins', ['module' => self::$pid, 'conf' => 1, 'redir' => $page_url]),
__('You must configure zip file pattern to complete xml code automatically.')
);
} else {
echo
'<form method="post" action="' . $page_url . '" id="buildxml" class="fieldset">' .
'<h4>' . __('Generate xml code') . '</h4>' .
'<p>' . __('This helps to generate content of dcstore.xml for seleted module.') . '</p>' .
'<p class="field"><label for="buildxml_id" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Module to parse:') . '</label> ' .
form::combo('buildxml_id', $combo, empty($_POST['buildxml_id']) ? '-' : html::escapeHTML($_POST['buildxml_id'])) .
'</p>' .
'<p><input type="submit" name="build_xml" value="' . __('Generate') . '" />' .
dcCore::app()->formNonce() . '</p>' .
'</form>';
}
if (!empty($_POST['buildxml_id'])) {
echo
'<form method="post" action="' . $page_url . '" id="writexml" class="fieldset">' .
'<h4>' . sprintf(__('Generated code for module: %s'), html::escapeHTML($_POST['buildxml_id'])) . '</h4>';
if (!empty(Core::$failed)) {
echo '<p class="info">' . sprintf(__('Failed to parse XML code: %s'), implode(', ', Core::$failed)) . '</p> ';
}
if (!empty(Core::$notice)) {
echo '<p class="info">' . sprintf(__('Code is not fully filled: %s'), implode(', ', Core::$notice)) . '</p> ';
}
if (!empty($xml_content)) {
if (empty(Core::$failed) && empty(Core::$notice)) {
echo '<p class="info">' . __('Code is complete') . '</p>';
}
echo
'<pre>' . form::textArea('gen_xml', 165, 14, [
'default' => html::escapeHTML(Core::prettyXML($xml_content)),
'class' => 'maximal',
'extra_html' => 'readonly="true"',
]) . '</pre>' .
(
!$user_ui_colorsyntax ? '' :
dcPage::jsRunCodeMirror('editor', 'gen_xml', 'dotclear', $user_ui_colorsyntax_theme)
);
if (empty(Core::$failed)
&& $modules[$_POST['buildxml_id']]['root_writable']
&& dcCore::app()->auth->isSuperAdmin()
) {
echo
'<p class="field"><label for="your_pwd2" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Your password:') . '</label> ' .
form::password(
['your_pwd', 'your_pwd2'],
20,
255,
[
'extra_html' => 'required placeholder="' . __('Password') . '"',
'autocomplete' => 'current-password',
]
) . '</p>' .
'<p><input type="submit" name="write_xml" value="' . __('Save to module directory') . '" /> ' .
'<a class="hidden-if-no-js button" href="#' . self::$pid . '" id="ts_copy_button">' . __('Copy to clipboard') . '</a>' .
form::hidden('buildxml_id', $_POST['buildxml_id']) .
dcCore::app()->formNonce() . '</p>';
}
echo sprintf(
'<p class="info"><a href="%s">%s</a></p>',
dcCore::app()->adminurl->get('admin.plugins', ['module' => self::$pid, 'conf' => 1, 'redir' => $page_url]),
__('You can edit zip file pattern from configuration page.')
);
}
echo
'</form>';
}
echo
'</div>';
}
# create list of module for combo and remove official modules
protected static function comboModules(array $modules, array $excludes): array
{
$combo = [__('Select a module') => '0'];
foreach ($modules as $id => $module) {
if (in_array($id, $excludes)) {
continue;
}
$combo[$module['name'] . ' ' . $module['version']] = $id;
}
return $combo;
}
}

View File

@ -0,0 +1,421 @@
<?php
/**
* @brief tweakStores, 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\tweakStores;
/* dotclear ns */
use dcCore;
use dcModuleDefine;
use dcModules;
use dcPage;
/* clearbricks ns */
use files;
use form;
use html;
use text;
use xmlTag;
/* php ns */
use DOMDocument;
use Exception;
class BackendBehaviors
{
/** @var array List of notice messages */
private static $notice = [];
/** @var array List of failed messages */
private static $failed = [];
public static function packmanBeforeCreatePackage(array $module): void
{
if (!dcCore::app()->blog->settings->get(My::id())->get('packman')) {
return;
}
// move from array to dcModuleDefine object
$modules = $module['type'] == 'theme' ? dcCore::app()->themes : dcCore::app()->plugins;
$define = $modules->getDefine($module['id']);
self::writeXML($define, dcCore::app()->blog->settings->get(My::id())->get('file_pattern'));
}
public static function modulesToolsHeaders(bool $is_plugin): string
{
return
dcPage::jsJson('ts_copied', ['alert' => __('Copied to clipboard')]) .
dcPage::jsModuleLoad(My::id() . '/js/backend.js') .
(
!dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax') ? '' :
dcPage::jsLoadCodeMirror(dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax_theme')) .
dcPage::jsModuleLoad(My::id() . '/js/cms.js')
);
}
public static function pluginsToolsTabsV2(): void
{
self::modulesToolsTabs(dcCore::app()->plugins, explode(',', DC_DISTRIB_PLUGINS), dcCore::app()->adminurl->get('admin.plugins'));
}
public static function themesToolsTabsV2(): void
{
self::modulesToolsTabs(dcCore::app()->themes, explode(',', DC_DISTRIB_THEMES), dcCore::app()->adminurl->get('admin.blog.theme'));
}
private static function modulesToolsTabs(dcModules $modules, array $excludes, string $page_url): void
{
$page_url .= '#' . My::id();
$user_ui_colorsyntax = dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax');
$user_ui_colorsyntax_theme = dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax_theme');
$file_pattern = (new Settings())->file_pattern;
$module = $modules->getDefine($_POST['ts_id'] ?? '-');
$combo = self::comboModules($modules, $excludes);
$form = '<p class="field"><label for="buildxml_id" class="classic required">' .
'<abbr title="' . __('Required field') . '">*</abbr> ' . __('Module to parse:') . '</label> ' .
form::combo('ts_id', $combo, $module->isDefined() ? html::escapeHTML($module->get('id')) : '-') .
'</p>';
# check dcstore repo
$url = '';
if (!empty($_POST['check_xml']) && $module->isDefined()) {
if (empty($module->get('repository'))) {
$url = __('This module has no repository set in its _define.php file.');
} else {
try {
$url = $module->get('repository');
if (false === strpos($url, 'dcstore.xml')) {
$url .= '/dcstore.xml';
}
if (function_exists('curl_init')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_REFERER, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$file_content = curl_exec($ch);
curl_close($ch);
} else {
$file_content = file_get_contents($url);
}
} catch (Exception $e) {
$file_content = __('Failed to read third party repository');
}
}
}
# generate xml code
if (!empty($_POST['build_xml']) && $module->isDefined()) {
$xml_content = self::generateXML($module, $file_pattern);
}
# write dcstore.xml file
if (!empty($_POST['write_xml'])) {
if (empty($_POST['your_pwd']) || !dcCore::app()->auth->checkPassword($_POST['your_pwd'])) {
dcCore::app()->error->add(__('Password verification failed'));
} else {
$ret = self::writeXML($module, $file_pattern);
if (!empty(self::$failed)) {
dcCore::app()->error->add(implode(' ', self::$failed));
}
}
}
echo
'<div class="multi-part" id="' . My::id() . '" title="' . My::name() . '">' .
'<h3>' . __('Tweak third-party repositories') . '</h3>';
if (!empty($_POST['write_xml'])) {
if (dcCore::app()->error->flag()) {
echo dcCore::app()->error->toHTML();
} else {
echo '<p class="success">' . __('File successfully written') . '</p>';
}
}
if (count($combo) < 2) {
echo
'<div class="info">' . __('There is no module to tweak') . '</div>' .
'</div>';
return;
}
echo
'<form method="post" action="' . $page_url . '" id="checkxml" class="fieldset">' .
'<h4>' . __('Check repository') . '</h4>' .
'<p>' . __('This checks if dcstore.xml file is present on third party repository.') . '</p>' .
$form .
'<p><input type="submit" name="check_xml" value="' . __('Check') . '" />' .
dcCore::app()->formNonce() . '</p>' .
'</form>';
if (!empty($url)) {
echo
'<div class="fieldset">' .
'<h4>' . __('Repositiory contents') . '</h4>' .
'<p>' . $url . '</p>' .
(
empty($file_content) ? '' :
'<pre>' . form::textArea('file_xml', 165, 14, [
'default' => html::escapeHTML(self::prettyXML($file_content)),
'class' => 'maximal',
'extra_html' => 'readonly="true"',
]) . '</pre>' .
(
!$user_ui_colorsyntax ? '' :
dcPage::jsRunCodeMirror('editor', 'file_xml', 'dotclear', $user_ui_colorsyntax_theme)
)
) .
'</div>';
}
if (empty($file_pattern)) {
echo sprintf(
'<div class="fieldset"><h4>' . __('Generate xml code') . '</h4><p class="info"><a href="%s">%s</a></p></div>',
dcCore::app()->adminurl->get('admin.plugins', ['module' => My::id(), 'conf' => 1, 'redir' => $page_url]),
__('You must configure zip file pattern to complete xml code automatically.')
);
} else {
echo
'<form method="post" action="' . $page_url . '" id="buildxml" class="fieldset">' .
'<h4>' . __('Generate xml code') . '</h4>' .
'<p>' . __('This helps to generate content of dcstore.xml for seleted module.') . '</p>' .
$form .
'<p><input type="submit" name="build_xml" value="' . __('Generate') . '" />' .
dcCore::app()->formNonce() . '</p>' .
'</form>';
}
if (!empty($_POST['build_xml'])) {
echo
'<form method="post" action="' . $page_url . '" id="writexml" class="fieldset">' .
'<h4>' . sprintf(__('Generated code for module: %s'), html::escapeHTML($module->get('id'))) . '</h4>';
if (!empty(self::$failed)) {
echo '<p class="info">' . sprintf(__('Failed to parse XML code: %s'), implode(', ', self::$failed)) . '</p> ';
}
if (!empty(self::$notice)) {
echo '<p class="info">' . sprintf(__('Code is not fully filled: %s'), implode(', ', self::$notice)) . '</p> ';
}
if (!empty($xml_content)) {
if (empty(self::$failed) && empty(self::$notice)) {
echo '<p class="info">' . __('Code is complete') . '</p>';
}
echo
'<pre>' . form::textArea('gen_xml', 165, 14, [
'default' => html::escapeHTML(self::prettyXML($xml_content)),
'class' => 'maximal',
'extra_html' => 'readonly="true"',
]) . '</pre>' .
(
!$user_ui_colorsyntax ? '' :
dcPage::jsRunCodeMirror('editor', 'gen_xml', 'dotclear', $user_ui_colorsyntax_theme)
);
if (empty(self::$failed)
&& $module->get('root_writable')
&& dcCore::app()->auth->isSuperAdmin()
) {
echo
'<p class="field"><label for="your_pwd2" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Your password:') . '</label> ' .
form::password(
['your_pwd', 'your_pwd2'],
20,
255,
[
'extra_html' => 'required placeholder="' . __('Password') . '"',
'autocomplete' => 'current-password',
]
) . '</p>' .
'<p><input type="submit" name="write_xml" value="' . __('Save to module directory') . '" /> ' .
'<a class="hidden-if-no-js button" href="#' . My::id() . '" id="ts_copy_button">' . __('Copy to clipboard') . '</a>' .
form::hidden('ts_id', $_POST['ts_id']) .
dcCore::app()->formNonce() . '</p>';
}
echo sprintf(
'<p class="info"><a href="%s">%s</a></p>',
dcCore::app()->adminurl->get('admin.plugins', ['module' => My::id(), 'conf' => 1, 'redir' => $page_url]),
__('You can edit zip file pattern from configuration page.')
);
}
echo
'</form>';
}
echo
'</div>';
}
# create list of module for combo and remove official modules
private static function comboModules(dcModules $modules, array $excludes): array
{
$combo = [__('Select a module') => '0'];
foreach ($modules->getDefines() as $module) {
if (in_array($module->get('id'), $excludes)) {
continue;
}
$combo[$module->get('name') . ' ' . $module->get('version')] = $module->get('id');
}
return $combo;
}
private static function parseFilePattern(dcModuleDefine $module, string $file_pattern): string
{
return text::tidyURL(str_replace(
[
'%type%',
'%id%',
'%version%',
'%author%',
],
[
$module->get('type'),
$module->get('id'),
$module->get('version'),
$module->get('author'),
],
$file_pattern
));
}
private static function generateXML(dcModuleDefine $module, string $file_pattern): string
{
$rsp = new xmlTag('module');
self::$notice = [];
self::$failed = [];
# id
if (!$module->isDefined()) {
self::$failed[] = 'unknow module';
}
$rsp->id = $module->get('id');
# name
if (empty($module->get('name'))) {
self::$failed[] = 'no module name set in _define.php';
}
$rsp->name($module->get('name'));
# version
if (empty($module->get('version'))) {
self::$failed[] = 'no module version set in _define.php';
}
$rsp->version($module->get('version'));
# author
if (empty($module->get('author'))) {
self::$failed[] = 'no module author set in _define.php';
}
$rsp->author($module->get('author'));
# desc
if (empty($module->get('desc'))) {
self::$failed[] = 'no module description set in _define.php';
}
$rsp->desc($module->get('desc'));
# repository
if (empty($module->get('repository'))) {
self::$failed[] = 'no repository set in _define.php';
}
# file
$file_pattern = self::parseFilePattern($module, $file_pattern);
if (empty($file_pattern)) {
self::$failed[] = 'no zip file pattern set in Tweak Store configuration';
}
$rsp->file($file_pattern);
# da dc_min or requires core
if (!empty($module->get('requires')) && is_array($module->get('requires'))) {
foreach ($module->get('requires') as $req) {
if (!is_array($req)) {
$req = [$req];
}
if ($req[0] == 'core') {
$module->set('dc_min', $req[1]);
break;
}
}
}
if (empty($module->get('dc_min'))) {
self::$notice[] = 'no minimum dotclear version';
} else {
$rsp->insertNode(new xmlTag('da:dcmin', $module->get('dc_min')));
}
# details
if (empty($module->get('details'))) {
self::$notice[] = 'no details URL';
} else {
$rsp->insertNode(new xmlTag('da:details', $module->get('details')));
}
# section
if (!empty($module->get('section'))) {
$rsp->insertNode(new xmlTag('da:section', $module->get('section')));
}
# support
if (empty($module->get('support'))) {
self::$notice[] = 'no support URL';
} else {
$rsp->insertNode(new xmlTag('da:support', $module->get('support')));
}
$res = new xmlTag('modules', $rsp);
$res->insertAttr('xmlns:da', 'http://dotaddict.org/da/');
return self::prettyXML($res->toXML());
}
private static function writeXML(dcModuleDefine $module, string $file_pattern): bool
{
self::$failed = [];
if (!$module->get('root_writable')) {
return false;
}
$content = self::generateXML($module, $file_pattern);
if (!empty(self::$failed)) {
return false;
}
try {
files::putContent($module->get('root') . DIRECTORY_SEPARATOR . 'dcstore.xml', $content);
} catch (Exception $e) {
self::$failed[] = $e->getMessage();
return false;
}
return true;
}
private static function prettyXML(string $str): string
{
if (class_exists('DOMDocument')) {
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($str);
return (string) $dom->saveXML();
}
return (string) str_replace('><', ">\n<", $str);
}
}

View File

@ -16,89 +16,98 @@ namespace Dotclear\Plugin\tweakStores;
/* dotclear ns */
use dcCore;
use dcNsProcess;
use dcPage;
/* clearbricks ns */
use form;
use http;
use Dotclear\Helper\Html\Form\{
Checkbox,
Div,
Fieldset,
Input,
Label,
Legend,
Note,
Para
};
/* php ns */
use Exception;
class Config
class Config extends dcNsProcess
{
private static $pid = '';
protected static $init = false;
public static function init(): bool
{
if (defined('DC_CONTEXT_ADMIN') && defined('DC_CONTEXT_MODULE')) {
dcPage::checkSuper();
self::$pid = basename(dirname(__DIR__));
self::$init = true;
}
static::$init = My::phpCompliant()
&& defined('DC_CONTEXT_ADMIN')
&& defined('DC_CONTEXT_MODULE')
&& dcCore::app()->auth->isSuperAdmin();
return self::$init;
return static::$init;
}
public static function process(): ?bool
public static function process(): bool
{
if (!self::$init) {
if (!static::$init) {
return false;
}
if (empty($_POST['save'])) {
return null;
return true;
}
$s = new Settings();
try {
$s = dcCore::app()->blog->settings->get(self::$pid);
$s->put('active', !empty($_POST['s_active']));
$s->put('packman', !empty($_POST['s_packman']));
$s->put('file_pattern', $_POST['s_file_pattern']);
foreach ($s->listSettings() as $key) {
$s->writeSetting($key, $_POST['ts_' . $key] ?? '');
}
dcPage::addSuccessNotice(
__('Configuration successfully updated')
);
http::redirect(
dcCore::app()->admin->__get('list')->getURL('module=' . self::$pid . '&conf=1&redir=' . dcCore::app()->admin->__get('list')->getRedir())
dcCore::app()->adminurl->redirect(
'admin.plugins',
['module' => My::id(), 'conf' => 1, 'redir' => dcCore::app()->admin->__get('list')->getRedir()]
);
return true;
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());
}
return null;
return true;
}
public static function render(): void
{
$s = dcCore::app()->blog->settings->get(self::$pid);
if (!static::$init) {
return;
}
echo '
<div class="fieldset">
<h4>' . dcCore::app()->plugins->moduleInfo(self::$pid, 'name') . '</h4>
$s = new Settings();
<p><label class="classic" for="s_active">' .
form::checkbox('s_active', 1, (bool) $s->get('active')) . ' ' .
__('Enable plugin') . '</label></p>
<p class="form-note">' . __('If enabled, new tab "Tweak stores" allows your to perfom actions relative to third-party repositories.') . '</p>
<p><label class="classic" for="s_packman">' .
form::checkbox('s_packman', 1, (bool) $s->get('packman')) . ' ' .
__('Enable packman behaviors') . '</label></p>
<p class="form-note">' . __('If enabled, plugin pacKman will (re)generate on the fly dcstore.xml file at root directory of the module.') . '</p>
<p><label class="classic" for="s_file_pattern">' . __('Predictable URL to zip file on the external repository') .
form::field('s_file_pattern', 65, 255, (string) $s->get('file_pattern'), 'maximal') . '
</label></p>
<p class="form-note">' .
__('You can use widcard like %author%, %type%, %id%, %version%.') . '<br /> ' .
__('For example on github https://github.com/MyGitName/%id%/releases/download/v%version%/%type%-%id%.zip') . '<br />' .
__('Note: on github, you must create a release and join to it the module zip file.') . '
</p>
</div>';
echo (new Div())->items([
(new Fieldset())->class('fieldset')->legend(new Legend(__('Interface')))->fields([
// s_active
(new Para())->items([
(new Checkbox('ts_active', $s->active))->value(1),
(new Label(__('Enable plugin'), Label::OUTSIDE_LABEL_AFTER))->for('ts_active')->class('classic'),
]),
(new Note())->text(__('If enabled, new tab "Tweak stores" allows your to perfom actions relative to third-party repositories.'))->class('form-note'),
// s_file_pattern
(new Para())->items([
(new Label(__('Predictable URL to zip file on the external repository')))->for('ts_file_pattern'),
(new Input('ts_file_pattern'))->size(65)->maxlenght(255)->value($s->file_pattern),
]),
(new Note())->text(__('You can use widcard like %author%, %type%, %id%, %version%.'))->class('form-note'),
(new Note())->text(__('For example on github https://github.com/MyGitName/%id%/releases/download/v%version%/%type%-%id%.zip'))->class('form-note'),
(new Note())->text(__('Note: on github, you must create a release and join to it the module zip file.'))->class('form-note'),
]),
(new Fieldset())->class('fieldset')->legend(new Legend(__('Behaviors')))->fields([
// s_packman
(new Para())->items([
(new Checkbox('ts_packman', $s->packman))->value(1),
(new Label(__('Enable packman behaviors'), Label::OUTSIDE_LABEL_AFTER))->for('ts_packman')->class('classic'),
]),
(new Note())->text(__('If enabled, plugin pacKman will (re)generate on the fly dcstore.xml file at root directory of the module.'))->class('form-note'),
]),
])->render();
}
}

50
src/My.php 100644
View File

@ -0,0 +1,50 @@
<?php
/**
* @brief tweakStores, 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\tweakStores;
use dcCore;
/**
* Plugin definitions
*/
class My
{
/** @var string Required php version */
public const PHP_MIN = '8.1';
/**
* 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, '>=');
}
}

76
src/Settings.php 100644
View File

@ -0,0 +1,76 @@
<?php
/**
* @brief tweakStores, 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\tweakStores;
use dcCore;
class Settings
{
// Enable this plugin
public readonly bool $active;
// Enable plugin pacKman behavior
public readonly bool $packman;
// Predictable dcstore url
public readonly string $file_pattern;
/**
* Constructor set up plugin settings
*/
public function __construct()
{
$s = dcCore::app()->blog->settings->get(My::id());
$this->active = (bool) ($s->get('active') ?? false);
$this->packman = (bool) ($s->get('packman') ?? false);
$this->file_pattern = (string) ($s->get('file_pattern') ?? '');
}
public function getSetting(string $key): mixed
{
return $this->{$key} ?? null;
}
/**
* Overwrite a plugin settings (in db)
*
* @param string $key The setting ID
* @param mixed $value The setting value
*
* @return bool True on success
*/
public function writeSetting(string $key, mixed $value): bool
{
if (property_exists($this, $key) && settype($value, gettype($this->{$key})) === true) {
dcCore::app()->blog->settings->get(My::id())->drop($key);
dcCore::app()->blog->settings->get(My::id())->put($key, $value, gettype($this->{$key}), '', true, true);
return true;
}
return false;
}
/**
* List defined settings keys
*
* @return array The settings keys
*/
public function listSettings(): array
{
return array_keys(get_class_vars(Settings::class));
}
}