Compare commits

...

10 Commits

14 changed files with 238 additions and 72 deletions

View File

@ -1,3 +1,15 @@
1.3 - 2023.05.13
- require dotclear 2.26
- fix type hint and nullsafe warnings
1.2 - 2023.04.22
- require dotclear 2.26
- add plugin Uninstaller features
- use latest dotclear namespace
- fix static init
- fix permission
- code doc and review
1.1 - 2023.03.25
- require dotclear 2.26
- use namespace

View File

@ -10,7 +10,7 @@
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
if (!defined('DC_RC_PATH')) {
if (!defined('DC_RC_PATH') || is_null(dcCore::app()->auth)) {
return null;
}
@ -18,11 +18,11 @@ $this->registerModule(
'Http password',
'Manage .htpasswd file to make the blog private',
'Frederic PLE and contributors',
'1.1',
'1.3',
[
'requires' => [['core', '2.26']],
'permissions' => dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_USAGE,
dcCore::app()->auth::PERMISSION_USAGE,
initHttpPassword::PERMISSION,
]),
'type' => 'plugin',

View File

@ -2,10 +2,10 @@
<modules xmlns:da="http://dotaddict.org/da/">
<module id="httpPassword">
<name>Http password</name>
<version>1.1</version>
<version>1.3</version>
<author>Frederic PLE and contributors</author>
<desc>Manage .htpasswd file to make the blog private</desc>
<file>https://github.com/JcDenis/httpPassword/releases/download/v1.1/plugin-httpPassword.zip</file>
<file>https://github.com/JcDenis/httpPassword/releases/download/v1.3/plugin-httpPassword.zip</file>
<da:dcmin>2.26</da:dcmin>
<da:details>http://plugins.dotaddict.org/dc2/details/httpPassword</da:details>
<da:support>https://github.com/JcDenis/httpPassword</da:support>

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

14
icon.svg 100644
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
<g>
<circle fill="#676e78" cx="24" cy="24" r="16"/>
<g>
<path fill="#a2cbe9" d="M45.863,33.859C47.223,30.852,48,27.516,48,24C48,10.742,37.254,0,24,0S0,10.742,0,24
c0,13.25,10.746,24,24,24c3.52,0,6.852-0.781,9.863-2.141L36,48h8v8h8v8h12V52L45.863,33.859z M24,40c-8.836,0-16-7.164-16-16
S15.164,8,24,8s16,7.164,16,16S32.836,40,24,40z"/>
<path fill="#a2cbe9" d="M24,16c-4.422,0-8,3.578-8,8s3.578,8,8,8s8-3.578,8-8S28.422,16,24,16z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 835 B

View File

@ -9,27 +9,29 @@
# DOT NOT MODIFY THIS FILE !
#
l10n::$locales['Manage http password blog protection'] = 'Gérer la protection du blog par mot de passe HTTP';
l10n::$locales['No encryption'] = 'pas de cryptage';
l10n::$locales['Logins history'] = 'Historique des logins';
l10n::$locales['Authorized users'] = 'Utilisateurs autorisés';
l10n::$locales['No write permissions on blogs directories.'] = 'Aucun droit d\'écriture sur le répertoire du blog.';
l10n::$locales['Settings successfully updated.'] = 'Paramètres mis à jour.';
l10n::$locales['Logs successfully cleared.'] = 'Logs effacé.';
l10n::$locales['Logins successfully updated.'] = 'Logins mis à jour.';
l10n::$locales['Select section:'] = 'Sélectionner une section :';
l10n::$locales['Enable http password protection on this blog'] = 'Activer la protection du blog par mot de passe http';
l10n::$locales['Crypt algorithm:'] = 'Algorithme de cryptage :';
l10n::$locales['Some web servers does not surpport plaintext (no) encryption.'] = 'Certains serveurs web ne supportent pas le cryptage "plaintext".';
l10n::$locales['If you change crypt algo, you must edit and resave each users passwords.'] = 'Si vous changer l\'algorithme de cryptage, vous devrez modifier et sauver tous les mots de passes.';
l10n::$locales['Authentication message:'] = 'Message d\'authentification :';
l10n::$locales['Logins history is empty.'] = 'L\'historique des logins est vide.';
l10n::$locales['Clear logs'] = 'Effacer les logs';
l10n::$locales['List of %s last logins.'] = 'Liste des %s derniers logins.';
l10n::$locales['Authorized users list is empty.'] = 'La listes des utilisateurs autorisés est vide.';
l10n::$locales['List of %s authorized users.'] = 'Liste des %s utilisateurs autorisés.';
l10n::$locales['New password'] = 'Nouveau mot de passe';
l10n::$locales['Change password'] = 'Modifier le mot de passe';
l10n::$locales['Add a user'] = 'Ajouter un utilisateur';
l10n::$locales['Login:'] = 'Identifiant :';
l10n::$locales['Manage .htpasswd file to make the blog private'] = 'Gestion du fichier .htpasswd pour rendre le blog privé.';
use Dotclear\Helper\L10n;
L10n::$locales['Manage http password blog protection'] = 'Gérer la protection du blog par mot de passe HTTP';
L10n::$locales['No encryption'] = 'pas de cryptage';
L10n::$locales['Logins history'] = 'Historique des logins';
L10n::$locales['Authorized users'] = 'Utilisateurs autorisés';
L10n::$locales['No write permissions on blogs directories.'] = 'Aucun droit d\'écriture sur le répertoire du blog.';
L10n::$locales['Settings successfully updated.'] = 'Paramètres mis à jour.';
L10n::$locales['Logs successfully cleared.'] = 'Logs effacé.';
L10n::$locales['Logins successfully updated.'] = 'Logins mis à jour.';
L10n::$locales['Select section:'] = 'Sélectionner une section :';
L10n::$locales['Enable http password protection on this blog'] = 'Activer la protection du blog par mot de passe http';
L10n::$locales['Crypt algorithm:'] = 'Algorithme de cryptage :';
L10n::$locales['Some web servers does not surpport plaintext (no) encryption.'] = 'Certains serveurs web ne supportent pas le cryptage "plaintext".';
L10n::$locales['If you change crypt algo, you must edit and resave each users passwords.'] = 'Si vous changer l\'algorithme de cryptage, vous devrez modifier et sauver tous les mots de passes.';
L10n::$locales['Authentication message:'] = 'Message d\'authentification :';
L10n::$locales['Logins history is empty.'] = 'L\'historique des logins est vide.';
L10n::$locales['Clear logs'] = 'Effacer les logs';
L10n::$locales['List of %s last logins.'] = 'Liste des %s derniers logins.';
L10n::$locales['Authorized users list is empty.'] = 'La listes des utilisateurs autorisés est vide.';
L10n::$locales['List of %s authorized users.'] = 'Liste des %s utilisateurs autorisés.';
L10n::$locales['New password'] = 'Nouveau mot de passe';
L10n::$locales['Change password'] = 'Modifier le mot de passe';
L10n::$locales['Add a user'] = 'Ajouter un utilisateur';
L10n::$locales['Login:'] = 'Identifiant :';
L10n::$locales['Manage .htpasswd file to make the blog private'] = 'Gestion du fichier .htpasswd pour rendre le blog privé.';

View File

@ -14,10 +14,10 @@ declare(strict_types=1);
namespace Dotclear\Plugin\httpPassword;
use dcAuth;
use dcAdmin;
use dcCore;
use dcPage;
use dcMenu;
use dcNsProcess;
class Backend extends dcNsProcess
@ -31,20 +31,22 @@ class Backend extends dcNsProcess
public static function process(): bool
{
if (!static::$init) {
if (!static::$init || is_null(dcCore::app()->auth) || is_null(dcCore::app()->blog) || is_null(dcCore::app()->adminurl)) {
return false;
}
// add backend sidebar menu icon
if ((dcCore::app()->menu[dcAdmin::MENU_PLUGINS] instanceof dcMenu)) {
dcCore::app()->menu[dcAdmin::MENU_PLUGINS]->addItem(
My::name(),
dcCore::app()->adminurl->get('admin.plugin.' . My::id()),
dcPage::getPF(My::id() . '/icon.png'),
dcPage::getPF(My::id() . '/icon.svg'),
preg_match('/' . preg_quote(dcCore::app()->adminurl->get('admin.plugin.' . My::id())) . '(&.*)?$/', $_SERVER['REQUEST_URI']),
dcCore::app()->auth->check(dcCore::app()->auth->makePermissions([
dcAuth::PERMISSION_USAGE,
My::PERMISSION,
]), dcCore::app()->blog->id)
);
}
return true;
}

View File

@ -33,7 +33,12 @@ class Frontend extends dcNsProcess
return false;
}
// check password on frontend
dcCore::app()->addBehavior('publicPrependV2', function (): void {
// nullsafe
if (is_null(dcCore::app()->blog)) {
return;
}
$PHP_AUTH_USER = $PHP_AUTH_PW = '';
if (isset($_SERVER['PHP_AUTH_USER']) and isset($_SERVER['PHP_AUTH_PW'])) {
@ -74,7 +79,7 @@ class Frontend extends dcNsProcess
if (!$logs->isEmpty()) {
$ids = [];
while ($logs->fetch()) {
$ids[] = (int) $logs->f('log_id');
$ids[] = is_numeric($logs->f('log_id')) ? (int) $logs->f('log_id') : 0;
}
$logs = dcCore::app()->log->delLogs($ids);
}

View File

@ -22,14 +22,17 @@ class Install extends dcNsProcess
{
public static function init(): bool
{
static::$init = defined('DC_CONTEXT_ADMIN') && dcCore::app()->newVersion(My::id(), dcCore::app()->plugins->moduleInfo(My::id(), 'version'));
if (defined('DC_CONTEXT_ADMIN')) {
$version = dcCore::app()->plugins->moduleInfo(My::id(), 'version');
static::$init = is_string($version) ? dcCore::app()->newVersion(My::id(), $version) : true;
}
return static::$init;
}
public static function process(): bool
{
if (!static::$init) {
if (!static::$init || is_null(dcCore::app()->blog)) {
return false;
}

View File

@ -17,6 +17,7 @@ namespace Dotclear\Plugin\httpPassword;
use dcCore;
use dcNsProcess;
use dcPage;
use Dotclear\Helper\Date;
use Dotclear\Helper\Html\Html;
use Dotclear\Helper\Html\Form\{
Checkbox,
@ -31,7 +32,6 @@ use Dotclear\Helper\Html\Form\{
Submit,
Text
};
use dt;
/**
* Manage contributions list
@ -40,10 +40,13 @@ class Manage extends dcNsProcess
{
public static function init(): bool
{
static::$init = defined('DC_CONTEXT_ADMIN') && dcCore::app()->auth->check(
static::$init = defined('DC_CONTEXT_ADMIN')
&& !is_null(dcCore::app()->auth) && !is_null(dcCore::app()->blog) // nullsafe
&& dcCore::app()->auth->check(
dcCore::app()->auth->makePermissions([
My::PERMISSION,
]), dcCore::app()->blog->id
]),
dcCore::app()->blog->id
);
return static::$init;
@ -51,7 +54,7 @@ class Manage extends dcNsProcess
public static function process(): bool
{
if (!static::$init) {
if (!static::$init || is_null(dcCore::app()->blog) || is_null(dcCore::app()->adminurl)) {
return false;
}
@ -67,6 +70,7 @@ class Manage extends dcNsProcess
return true;
}
// save settings
if ('savesettings' == $action) {
$s = dcCore::app()->blog->settings->get(My::id());
$s->put('active', !empty($_POST['active']));
@ -85,6 +89,7 @@ class Manage extends dcNsProcess
);
}
// delete users logins
if ('savelogins' == $action) {
$logs = dcCore::app()->log->getLogs(['log_table' => My::id()]);
if (!$logs->isEmpty()) {
@ -105,6 +110,7 @@ class Manage extends dcNsProcess
}
}
// save users logins / passwords in frontend passwords file
if ('savepasswords' == $action) {
$passwords = self::getPasswords();
$lines = [];
@ -153,7 +159,7 @@ class Manage extends dcNsProcess
public static function render(): void
{
if (!static::$init) {
if (!static::$init || is_null(dcCore::app()->blog) || is_null(dcCore::app()->adminurl)) {
return;
}
@ -173,7 +179,7 @@ class Manage extends dcNsProcess
]) .
dcPage::notices() .
# Filters select menu list
// Filters select menu list
(new Form('section_menu'))->action(dcCore::app()->adminurl->get('admin.plugin.' . My::id()))->method('get')->fields([
(new Para())->class('anchor-nav')->items([
(new Label(__('Select section:')))->for('part')->class('classic'),
@ -185,6 +191,7 @@ class Manage extends dcNsProcess
'<h3>' . array_search($part, My::sectionCombo()) . '</h3>';
// settigns form
if ('settings' == $part) {
echo
(new Form('section_settings'))->action(dcCore::app()->adminurl->get('admin.plugin.' . My::id(), ['part' => 'settings']))->method('post')->fields([
@ -209,11 +216,12 @@ class Manage extends dcNsProcess
(new Submit(['save']))->value(__('Save')),
(new Hidden(['action'], 'savesettings')),
(new Hidden(['part'], $part)),
(new Text('', dcCore::app()->formNonce())),
dcCore::app()->formNonce(false),
]),
])->render();
}
// delete logins form
if ('logins' == $part) {
$logs = dcCore::app()->log->getLogs(['log_table' => My::id()]);
if ($logs->isEmpty()) {
@ -226,7 +234,7 @@ class Manage extends dcNsProcess
(new Submit(['save']))->value(__('Clear logs')),
(new Hidden(['action'], 'savelogins')),
(new Hidden(['part'], $part)),
(new Text('', dcCore::app()->formNonce())),
dcCore::app()->formNonce(false),
]),
])->render() .
@ -238,10 +246,12 @@ class Manage extends dcNsProcess
'</tr></thead<tbody>';
while ($logs->fetch()) {
$msg = is_string($logs->f('log_msg')) ? $logs->f('log_msg') : '';
$dt = is_string($logs->f('log_dt')) ? $logs->f('log_dt') : '';
echo
'<tr class="line">' .
'<td class="nowrap maximal">' . Html::escapeHTML($logs->f('log_msg')) . '</td>' .
'<td class="nowrap count">' . Html::escapeHTML(dt::dt2str(__('%Y-%m-%d %H:%M'), $logs->f('log_dt'))) . '</td>' .
'<td class="nowrap maximal">' . Html::escapeHTML($msg) . '</td>' .
'<td class="nowrap count">' . Html::escapeHTML(Date::dt2str(__('%Y-%m-%d %H:%M'), $dt)) . '</td>' .
'</tr>';
}
@ -250,6 +260,7 @@ class Manage extends dcNsProcess
}
}
// existing logins/passwords form
if ('passwords' == $part) {
$passwords = self::getPasswords();
@ -290,11 +301,12 @@ class Manage extends dcNsProcess
(new Para())->items([
(new Hidden(['action'], 'savepasswords')),
(new Hidden(['part'], $part)),
(new Text('', dcCore::app()->formNonce())),
dcCore::app()->formNonce(false),
]),
])->render();
}
// new login form
echo
(new Form('section_new'))->action(dcCore::app()->adminurl->get('admin.plugin.' . My::id(), ['part' => $part]))->method('post')->fields([
(new Text('h3', Html::escapeHTML(__('Add a user')))),
@ -312,7 +324,7 @@ class Manage extends dcNsProcess
(new Submit(['add']))->value(__('Save')),
(new Hidden(['action'], 'savepasswords')),
(new Hidden(['part'], $part)),
(new Text('', dcCore::app()->formNonce())),
dcCore::app()->formNonce(false),
]),
])->render();
}
@ -320,6 +332,11 @@ class Manage extends dcNsProcess
dcPage::closeModule();
}
/**
* Get page section.
*
* @return string The section
*/
private static function getSection(): string
{
$part = $_REQUEST['part'] ?? 'settings';
@ -330,6 +347,11 @@ class Manage extends dcNsProcess
return $part;
}
/**
* Get existing passwords from file.
*
* @return array<string,string> The passwords list
*/
private static function getPasswords(): array
{
$passwords = [];

View File

@ -16,6 +16,9 @@ namespace Dotclear\Plugin\httpPassword;
use dcCore;
/**
* This module definitions.
*/
class My
{
/** @var string This plugin permissions */
@ -25,7 +28,7 @@ class My
public const FILE_PASSWORD = '.htpasswd';
/**
* This module id
* This module id.
*/
public static function id(): string
{
@ -33,15 +36,27 @@ class My
}
/**
* This module name
* This module name.
*/
public static function name(): string
{
return __((string) dcCore::app()->plugins->moduleInfo(self::id(), 'name'));
$name = dcCore::app()->plugins->moduleInfo(self::id(), 'name');
return __(is_string($name) ? $name : self::id());
}
/**
* Encryption methods combo
* This module path.
*/
public static function path(): string
{
return dirname(__DIR__);
}
/**
* Encryption methods combo.
*
* @return array<string,string>
*/
public static function cryptCombo(): array
{
@ -57,7 +72,9 @@ class My
}
/**
* Admin section menu
* Admin section menu.
*
* @return array<string,string>
*/
public static function sectionCombo(): array
{

View File

@ -28,10 +28,11 @@ class Prepend extends dcNsProcess
public static function process(): bool
{
if (!static::$init) {
if (!static::$init || is_null(dcCore::app()->auth)) {
return false;
}
// register module permission
dcCore::app()->auth->setPermissionType(
My::PERMISSION,
__('Manage http password blog protection')

72
src/Uninstall.php 100644
View File

@ -0,0 +1,72 @@
<?php
/**
* @brief httpPassword, a plugin for Dotclear 2
*
* @package Dotclear
* @subpackage Plugin
*
* @author Frederic PLE 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\httpPassword;
use dcCore;
use dcNsProcess;
use Dotclear\Plugin\Uninstaller\Uninstaller;
class Uninstall extends dcNsProcess
{
public static function init(): bool
{
static::$init = defined('DC_CONTEXT_ADMIN');
return static::$init;
}
public static function process(): bool
{
if (!static::$init || !dcCore::app()->plugins->moduleExists('Uninstaller')) {
return false;
}
Uninstaller::instance()
->addUserAction(
'settings',
'delete_all',
My::id()
)
->addUserAction(
'plugins',
'delete',
My::id()
)
->addUserAction(
'versions',
'delete',
My::id()
)
->addDirectAction(
'settings',
'delete_all',
My::id()
)
->addDirectAction(
'plugins',
'delete',
My::id()
)
->addDirectAction(
'versions',
'delete',
My::id()
)
;
// no custom action
return false;
}
}

View File

@ -20,9 +20,15 @@ class Utils
{
/**
* Crypt password
*
* @param string $secret The secret
*
* @return string The crypt password (empty on error)
*/
public static function crypt(?string $secret): string
{
$secret = (string) $secret;
switch (self::cryptMethod()) {
case 'plaintext':
$saltlen = -1;
@ -72,43 +78,53 @@ class Utils
$secret = crypt($secret, $salt);
}
return($secret);
return $secret;
}
/**
* Setting: active
*
* @return bool True if module is active
*/
public static function isActive(): bool
{
return (bool) dcCore::app()->blog->settings->get(My::id())->get('active');
return !is_null(dcCore::app()->blog) && (bool) dcCore::app()->blog->settings->get(My::id())->get('active');
}
/**
* Setting: crypt
*
* @return string The crypt method
*/
public static function cryptMethod(): string
{
return (string) dcCore::app()->blog->settings->get(My::id())->get('crypt');
return !is_null(dcCore::app()->blog) && is_string(dcCore::app()->blog->settings->get(My::id())->get('crypt')) ? dcCore::app()->blog->settings->get(My::id())->get('crypt') : '';
}
/**
* Setting: message
*
* @return string The frontend message
*/
public static function httpMessage(): string
{
return (string) dcCore::app()->blog->settings->get(My::id())->get('message');
return !is_null(dcCore::app()->blog) && is_string(dcCore::app()->blog->settings->get(My::id())->get('message')) ? dcCore::app()->blog->settings->get(My::id())->get('message') : '';
}
/**
* Get passwords file path
*
* @return string The passwords file path (empty on error)
*/
public static function passwordFile(): string
{
return dcCore::app()->blog->public_path . DIRECTORY_SEPARATOR . My::FILE_PASSWORD;
return is_null(dcCore::app()->blog) ? '' : dcCore::app()->blog->public_path . DIRECTORY_SEPARATOR . My::FILE_PASSWORD;
}
/**
* Check passwords file
*
* @return bool True if passwords file is writable
*/
public static function isWritable(): bool
{