first commit

master
Jean-Christian Paul Denis 2023-07-23 00:50:27 +02:00
commit 089c23f0f6
Signed by: JcDenis
GPG Key ID: 1B5B8C5B90B6C951
10 changed files with 609 additions and 0 deletions

3
CHANGELOG.md 100644
View File

@ -0,0 +1,3 @@
0.1 - 2023.xx.xx
- require dotclear 2.27
- require php 7.4

46
README.md 100644
View File

@ -0,0 +1,46 @@
# README
## WHAT IS DOTCLEARWATCH ?
"dotclear watch" is a plugin for the open-source
web publishing software called Dotclear.
It tracks Dotclear's installation to get stats about it.
What's being track :
* Dotclear version,
* PHP version,
* Database version,
* List of installed modules (only id and version)
* Number of blogs. (no name or id or whatever else)
If you want to hide some modules, just enter their IDs in a comma separeted list
in aboutConfig global parameters called DotclearWatch->hidden_modules
## REQUIREMENTS
DotclearWatch requires:
* super admin permission to intall it
* Dotclear 2.27
## USAGE
Install DotclearWatch, manualy from a zip package or from
Dotaddict repository. (See Dotclear's documentation to know how do this)
To disable sending stats, just deactivate or uninstall this plugin.
## LINKS
* License : [GNU GPL v2](https://www.gnu.org/licenses/old-licenses/lgpl-2.0.html)
* Source & contributions : [GitHub Page](https://github.com/JcDenis/DotclearWatch)
* Packages & details : [Dotaddict Page](https://plugins.dotaddict.org/dc2/details/DotclearWatch)
* Discussion & Help : [Github issue](https://github.com/JcDenis/DotclearWatch/issues)
## CONTRIBUTORS
* Jean-Christian Denis (author)
You are welcome to contribute to this code.

15
_define.php 100644
View File

@ -0,0 +1,15 @@
<?php
$this->registerModule(
'Dotclear Watch',
'Send report about your Dotclear',
'Jean-Christian Denis and contributors',
'0.1',
[
'requires' => [
['php', '7.4'],
['core', '2.27'],
],
'type' => 'plugin',
]
);

9
icon.svg 100644
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2.75C15.9068 2.75 17.2615 2.75159 18.2892 2.88976C19.2952 3.02503 19.8749 3.27869 20.2981 3.7019C20.7852 4.18904 20.9973 4.56666 21.1147 5.23984C21.2471 5.9986 21.25 7.08092 21.25 9C21.25 9.41422 21.5858 9.75 22 9.75C22.4142 9.75 22.75 9.41422 22.75 9L22.75 8.90369C22.7501 7.1045 22.7501 5.88571 22.5924 4.98199C22.417 3.97665 22.0432 3.32568 21.3588 2.64124C20.6104 1.89288 19.6615 1.56076 18.489 1.40314C17.3498 1.24997 15.8942 1.24998 14.0564 1.25H14C13.5858 1.25 13.25 1.58579 13.25 2C13.25 2.41421 13.5858 2.75 14 2.75Z" fill="#a2cbe9"/>
<path d="M2.00001 14.25C2.41422 14.25 2.75001 14.5858 2.75001 15C2.75001 16.9191 2.75289 18.0014 2.88529 18.7602C3.00275 19.4333 3.21477 19.811 3.70191 20.2981C4.12512 20.7213 4.70476 20.975 5.71085 21.1102C6.73852 21.2484 8.09318 21.25 10 21.25C10.4142 21.25 10.75 21.5858 10.75 22C10.75 22.4142 10.4142 22.75 10 22.75H9.94359C8.10583 22.75 6.6502 22.75 5.51098 22.5969C4.33856 22.4392 3.38961 22.1071 2.64125 21.3588C1.95681 20.6743 1.58304 20.0233 1.40762 19.018C1.24992 18.1143 1.24995 16.8955 1.25 15.0964L1.25001 15C1.25001 14.5858 1.58579 14.25 2.00001 14.25Z" fill="#a2cbe9"/>
<path d="M22 14.25C22.4142 14.25 22.75 14.5858 22.75 15L22.75 15.0963C22.7501 16.8955 22.7501 18.1143 22.5924 19.018C22.417 20.0233 22.0432 20.6743 21.3588 21.3588C20.6104 22.1071 19.6615 22.4392 18.489 22.5969C17.3498 22.75 15.8942 22.75 14.0564 22.75H14C13.5858 22.75 13.25 22.4142 13.25 22C13.25 21.5858 13.5858 21.25 14 21.25C15.9068 21.25 17.2615 21.2484 18.2892 21.1102C19.2952 20.975 19.8749 20.7213 20.2981 20.2981C20.7852 19.811 20.9973 19.4333 21.1147 18.7602C21.2471 18.0014 21.25 16.9191 21.25 15C21.25 14.5858 21.5858 14.25 22 14.25Z" fill="#a2cbe9"/>
<path d="M9.94359 1.25H10C10.4142 1.25 10.75 1.58579 10.75 2C10.75 2.41421 10.4142 2.75 10 2.75C8.09319 2.75 6.73852 2.75159 5.71085 2.88976C4.70476 3.02503 4.12512 3.27869 3.70191 3.7019C3.21477 4.18904 3.00275 4.56666 2.88529 5.23984C2.75289 5.9986 2.75001 7.08092 2.75001 9C2.75001 9.41422 2.41422 9.75 2.00001 9.75C1.58579 9.75 1.25001 9.41422 1.25001 9L1.25 8.90369C1.24995 7.10453 1.24992 5.8857 1.40762 4.98199C1.58304 3.97665 1.95681 3.32568 2.64125 2.64124C3.38961 1.89288 4.33856 1.56076 5.51098 1.40314C6.65019 1.24997 8.10584 1.24998 9.94359 1.25Z" fill="#a2cbe9"/>
<path d="M12 10.75C11.3096 10.75 10.75 11.3096 10.75 12C10.75 12.6904 11.3096 13.25 12 13.25C12.6904 13.25 13.25 12.6904 13.25 12C13.25 11.3096 12.6904 10.75 12 10.75Z" fill="#137bbb"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.89243 14.0598C5.29747 13.3697 5 13.0246 5 12C5 10.9754 5.29748 10.6303 5.89242 9.94021C7.08037 8.56222 9.07268 7 12 7C14.9273 7 16.9196 8.56222 18.1076 9.94021C18.7025 10.6303 19 10.9754 19 12C19 13.0246 18.7025 13.3697 18.1076 14.0598C16.9196 15.4378 14.9273 17 12 17C9.07268 17 7.08038 15.4378 5.89243 14.0598ZM9.25 12C9.25 10.4812 10.4812 9.25 12 9.25C13.5188 9.25 14.75 10.4812 14.75 12C14.75 13.5188 13.5188 14.75 12 14.75C10.4812 14.75 9.25 13.5188 9.25 12Z" fill="#9ac123"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

51
locales/fr/main.po 100644
View File

@ -0,0 +1,51 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: DotclearWatch 0.1\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2023-07-22T22:18:27+00:00\n"
"Last-Translator: Jean-Christain Denis\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Cache directory sucessfully cleared."
msgstr "Répertoire de cache des rapports nettoyé."
msgid "Settings successfully updated."
msgstr "Paramètres mis à jour."
msgid "Report sent."
msgstr "Rapport envoyé."
msgid "Settings are globals. Reports are by blog."
msgstr "Les paramètres sont globaux. Les rapports sont par blog."
msgid "Hidden modules:"
msgstr "Modules cachés :"
msgid "This is the comma separated list of plugins IDs and themes IDs to ignore in report."
msgstr "C'est la liste des modules cachés séparés par une virgule."
msgid "Distant API URL:"
msgstr "URL de l'API distante :"
msgid "This is the URL of the API to send report. Leave empty to reset value."
msgstr "C'est L'URL de l'API où sera envoyer le rapport. Laisser vide pour remettre par défaut."
msgid "Clear reports cache directory"
msgstr "Nettoyer le répertoire de cache des rapports."
msgid "This deletes all blogs reports in cache."
msgstr "Ceci efface tous les rapports des blogs en cache."
msgid "Send report now"
msgstr "Envoyer le rapport maintenant"
msgid "This sent report for current blog even if report exists in cache."
msgstr "Ceci envoie le rapport pou r le blog courant même si un rapport existe en cache."
msgid "Report that will be sent for this blog:"
msgstr "Rapport qui sera envoyé pour ce blog :"

32
src/Backend.php 100644
View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Dotclear\Plugin\DotclearWatch;
use dcCore;
use Dotclear\Core\Process;
class Backend extends Process
{
public static function init(): bool
{
return self::status(My::checkContext(My::BACKEND));
}
public static function process(): bool
{
if (!self::status()) {
return false;
}
//My::addBackendMenuItem();
dcCore::app()->addBehaviors([
'adminDashboardHeaders' => [Utils::class, 'sendReport'],
'adminPageFooterV2' => [Utils::class, 'addMark'],
]);
return true;
}
}

136
src/Config.php 100644
View File

@ -0,0 +1,136 @@
<?php
/**
* @brief DotclearWatch, a plugin for Dotclear 2
*
* @package Dotclear
* @subpackage Plugins
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
declare(strict_types=1);
namespace Dotclear\Plugin\DotclearWatch;
use dcCore;
use Dotclear\Core\Backend\Notices;
use Dotclear\Core\Backend\Page;
use Dotclear\Core\Process;
use Dotclear\Helper\Html\Form\{
Checkbox,
Div,
Input,
Label,
Note,
Para,
Text,
Textarea
};
use Dotclear\Helper\Html\Html;
class Config extends Process
{
private static string $hidden_modules = '';
private static string $distant_api_url = '';
public static function init(): bool
{
return self::status(My::checkContext(My::MANAGE));
}
public static function process(): bool
{
if (!self::status()) {
return false;
}
if (dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax')) {
dcCore::app()->addBehavior('pluginsToolsHeadersV2', fn (bool $plugin): string => Page::jsLoadCodeMirror(dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax_theme')));
}
self::$hidden_modules = (string) My::settings()->getGlobal('hidden_modules');
self::$distant_api_url = (string) My::settings()->getGlobal('distant_api_url');
if (empty($_POST)) {
return true;
}
if (!empty($_POST['clear_cache'])) {
Utils::clearCache();
Notices::AddSuccessNotice(__('Cache directory sucessfully cleared.'));
}
self::$hidden_modules = '';
foreach (explode(',', $_POST['hidden_modules']) as $hidden) {
$hidden = trim($hidden);
if (!empty($hidden)) {
self::$hidden_modules .= trim($hidden) . ',';
}
}
self::$distant_api_url = !empty($_POST['distant_api_url']) && is_string($_POST['distant_api_url']) ? $_POST['distant_api_url'] : Utils::DISTANT_API_URL;
My::settings()->put('hidden_modules', self::$hidden_modules, 'string', 'Hidden modules from report', true, true);
My::settings()->put('distant_api_url', self::$distant_api_url, 'string', 'Distant API report URL', true, true);
Notices::AddSuccessNotice(__('Settings successfully updated.'));
if (!empty($_POST['send_report'])) {
Utils::sendReport(true);
Notices::AddSuccessNotice(__('Report sent.'));
}
dcCore::app()->admin->url->redirect('admin.plugins', ['module' => My::id(), 'conf' => '1']);
return true;
}
public static function render(): void
{
if (!self::status()) {
return;
}
echo
(new Div())->items([
(new Text('p', __('Settings are globals. Reports are by blog.')))->class('message'),
(new Text('pre', sprintf(__('API %s'), Utils::DISTANT_API_VERSION))),
(new Para())->items([
(new Label(__('Hidden modules:')))->for('hidden_modules'),
(new Input('hidden_modules'))->class('maximal')->size(65)->maxlenght(255)->value(self::$hidden_modules),
]),
(new Note())->class('form-note')->text(__('This is the comma separated list of plugins IDs and themes IDs to ignore in report.')),
(new Para())->items([
(new Label(__('Distant API URL:')))->for('distant_api_url'),
(new Input('distant_api_url'))->class('maximal')->size(65)->maxlenght(255)->value(self::$distant_api_url),
]),
(new Note())->class('form-note')->text(__('This is the URL of the API to send report. Leave empty to reset value.')),
(new Para())->items([
(new Checkbox('clear_cache', false))->value(1),
(new Label(__('Clear reports cache directory'), Label::OUTSIDE_LABEL_AFTER))->for('clear_cache')->class('classic'),
]),
(new Note())->class('form-note')->text(__('This deletes all blogs reports in cache.')),
(new Para())->items([
(new Checkbox('send_report', false))->value(1),
(new Label(__('Send report now'), Label::OUTSIDE_LABEL_AFTER))->for('send_report')->class('classic'),
]),
(new Note())->class('form-note')->text(__('This sent report for current blog even if report exists in cache.')),
])->render();
$contents = Utils::getReport();
if (!empty($contents)) {
echo
(new Para())->items([
(new Label(__('Report that will be sent for this blog:')))->for('report_contents'),
(new Textarea('report_contents', Html::escapeHTML($contents)))
->cols(165)
->rows(14)
->readonly(true)
->class('maximal'),
])->render() .
(
!dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax') ? '' :
Page::jsRunCodeMirror(My::id() . 'editor', 'report_contents', 'json', dcCore::app()->auth->user_prefs->get('interface')->get('colorsyntax_theme'))
);
}
}
}

40
src/Install.php 100644
View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Dotclear\Plugin\DotclearWatch;
use Dotclear\Core\Process;
class Install extends Process
{
public static function init(): bool
{
return self::status(My::checkContext(My::INSTALL));
}
public static function process(): bool
{
$s = My::settings();
if (self::status() && $s !== null) {
$s->put(
'hidden_modules',
'DotclearWatch',
'string',
'Hidden modules from report',
false,
true
);
$s->put(
'distant_api_url',
'https://dotclear.watch/report',
'string',
'Distant API report URL',
false,
true
);
}
return self::status();
}
}

17
src/My.php 100644
View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Dotclear\Plugin\DotclearWatch;
use dcCore;
use Dotclear\Module\MyPlugin;
class My extends MyPlugin
{
protected static function checkCustomContext(int $context): ?bool
{
return $context === My::INSTALL ? null :
defined('DC_CONTEXT_ADMIN') && dcCore::app()->auth->isSuperAdmin();
}
}

260
src/Utils.php 100644
View File

@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace Dotclear\Plugin\DotclearWatch;
use dcCore;
use dcModuleDefine;
use dcThemes;
use Dotclear\Helper\Crypt;
use Dotclear\Helper\File\Files;
use Dotclear\Helper\File\Path;
use Dotclear\Helper\Network\HttpClient;
use Exception;
class Utils
{
/** @var int The expiration delay before resend report (one week) */
public const EXPIRED_DELAY = 604800;
/** @var string The default distant API URL */
public const DISTANT_API_URL = 'https://dotclear.watch/api';
/** @var string The distant API version */
public const DISTANT_API_VERSION = '1.0';
/** @var array<int,string> The hiddens modules IDs */
private static array $hiddens = [];
/**
* Add mark to backend menu footer.
*/
public static function addMark(): void
{
if (My::settings()->get('distant_api_url')) {
echo '<p>' . __('/!\ Tracked by dotclear.watch') . '</p>';
}
}
/**
* Get hidden modules.
*
* This does not check if module exists.
*
* @return array<int,string> The hiddens modules
*/
public static function getHiddens(): array
{
if (empty(self::$hiddens)) {
foreach (explode(',', (string) My::settings()->getGlobal('hidden_modules')) as $hidden) {
$hidden = trim($hidden);
if (!empty($hidden)) {
self::$hiddens[] = trim($hidden);
}
}
}
return self::$hiddens;
}
/**
* Get plugins list.
*
* @param bool $strict tak on ly enabled and not hidden plugins
*
* @return array<string,string> The plugins list.
*/
public static function getPlugins(bool $strict = true): array
{
$modules = [];
$hiddens = self::getHiddens();
$defines = dcCore::app()->plugins->getDefines($strict ? ['state' => dcModuleDefine::STATE_ENABLED] : []);
foreach ($defines as $define) {
if ($strict && in_array($define->getId(), $hiddens)) {
continue;
}
$modules[$define->getId()] = $define->get('version');
}
return $modules;
}
/**
* Get themes list.
*
* @param bool $strict tak on ly enabled and not hidden themes
*
* @return array<string,string> The themes list.
*/
public static function getThemes(bool $strict = true): array
{
if (!(dcCore::app()->themes instanceof dcThemes)) {
dcCore::app()->themes = new dcThemes();
dcCore::app()->themes->loadModules(dcCore::app()->blog->themes_path);
}
$modules = [];
$hiddens = self::getHiddens();
$defines = dcCore::app()->themes->getDefines($strict ? ['state' => dcModuleDefine::STATE_ENABLED] : []);
foreach ($defines as $define) {
if ($strict && in_array($define->getId(), $hiddens)) {
continue;
}
$modules[$define->getId()] = $define->get('version');
}
return $modules;
}
/**
* Get report contents.
*
* @return string The report contents as it will be sent
*/
public static function getReport(): string
{
return self::check() ? self::contents() : '';
}
/**
* Clear cache directory.
*/
public static function clearCache(): void
{
if (self::check()) {
self::clear();
}
}
/**
* Send report.
*
* Report will be by blog to keep track of all used themes.
* Plugins stats are by multiblogs and themes stats by blog.
*
* @param bool $force Send report even if delay not expired
*/
public static function sendReport(bool $force = false): void
{
if (!self::check()) {
return;
}
$file = self::file();
if (!$force && !self::expired($file)) {
return;
}
$contents = self::contents();
self::write($file, $contents);
try {
$rsp = HttpClient::quickPost(sprintf(self::url(), 'report'), ['key' => self::key(), 'report' => $contents]);
if ($rsp !== 'ok') {
pdump($rsp);
}
} catch (Exception $e) {
dcCore::app()->error->add(__('Dotclear.watch report failed'));
}
}
private static function check(): bool
{
return defined('DC_MASTER_KEY') && defined('DC_CRYPT_ALGO') && defined('DC_TPL_CACHE') && is_dir(DC_TPL_CACHE) && is_writable(DC_TPL_CACHE);
}
private static function key(): string
{
return Crypt::hmac(DC_MASTER_KEY, My::id() . __DIR__, DC_CRYPT_ALGO);
}
private static function uid(): string
{
return md5(DC_MASTER_KEY . My::id());
}
private static function buid(): string
{
return md5(DC_MASTER_KEY . My::id() . dcCore::app()->blog->uid);
}
private static function url()
{
$api_url = My::settings()->getGlobal('distant_api_url');
return (is_string($api_url) ? $api_url : self::DISTANT_API_URL) . '/' . self::DISTANT_API_VERSION . '/%s/' . self::uid();
}
private static function file(): string
{
$file = self::buid();
return sprintf(
'%s/%s/%s/%s/%s.json',
(string) Path::real(DC_TPL_CACHE),
My::id(),
substr($file, 0, 2),
substr($file, 2, 2),
$file
);
}
private static function clear(): void
{
$path = (string) Path::real(DC_TPL_CACHE) . DIRECTORY_SEPARATOR . My::id();
if (is_dir($path)) {
Files::delTree($path);
}
}
private static function write(string $file, string $contents): void
{
$dir = dirname($file);
if (!is_dir($dir)) {
Files::makeDir($dir, true);
}
file_put_contents($file, $contents);
}
private static function read(string $file): string
{
return is_file($file) && is_readable($file) ? (string) file_get_contents($file) : '';
}
private static function expired(string $file): bool
{
if (!is_file($file) || !is_readable($file) || ($time = filemtime($file)) === false) {
return true;
}
$time = date('U', $time);
if (!is_numeric($time) || (int) $time + self::EXPIRED_DELAY < time()) {
return true;
}
return false;
}
private static function contents(): string
{
// Build json response
return (string) json_encode([
'uid' => self::uid(),
'buid' => self::buid(),
'plugin' => self::getPlugins(), // enabled plugins
'theme' => self::getThemes(), // enabled themes
'server' => [
'blogs_count' => (int) dcCore::app()->getBlogs([], true)->f(0),
'core' => DC_VERSION,
'php' => phpversion(),
'thm' => (string) dcCore::app()->blog->settings->get('system')->get('theme'), // selected theme
],
'database' => [
dcCore::app()->con->driver() => dcCore::app()->con->version(),
],
], JSON_PRETTY_PRINT);
}
}