Merge branch 'next' into develop

This commit is contained in:
Lucas Bartholemy 2023-11-06 19:03:30 +01:00
commit 9323b6840a
71 changed files with 2158 additions and 219 deletions

21
CHANGELOG-DEV.md Normal file
View File

@ -0,0 +1,21 @@
HumHub Changelog
================
1.16.0 (Unreleased)
-------------------
- Fix #6636: Module Manager test
- Enh #6530: Small performance improvements
- Fix #6511: Only test compatible modules in `onMarketplaceAfterFilterModules()`
- Enh #6511: Backup folder path is now return from `removeModule()`
- Fix #6511: `canRemoveModule` no longer throws an Exception
- Enh #6511: Allow an empty filter list to filter all registered modules
- Enh #6511: Allow module paths for `enableModules()`
- Enh #6511: Verify module's event definition
- Enh #6511: Make module's module.json keywords accessible and searchable
- Enh #6511: Add Event tracking capabilities to HumHubDbTestCase
- Enh #6511: Add test for ModuleManager
- Fix #6519: Ensure e-mails would always have a sender address set
- Enh #6512: Show error messages when DB connection configuration is invalid
- Enh #5315: Default stream sort by `created_at` instead of `id`
- Fix #6337: Update `created_at` after first publishing of content record
- Fix #6631: Fix visibility of the method `Controller::getAccessRules()`

View File

@ -4,6 +4,19 @@ Module Migration Guide
See [humhub/documentation::docs/develop/modules-migrate.md](https://github.com/humhub/documentation/blob/master/docs/develop/modules-migrate.md)
for full version.
Version 1.16 (Unreleased)
-------------------------
### Deprecations
- `\humhub\modules\content\components\ContentAddonActiveRecord::canWrite()`
### Type restrictions
- `\humhub\modules\comment\models\Comment` on `canDelete()`
- `\humhub\modules\content\components\ContentAddonActiveRecord` on `canDelete()`, `canRead()`, `canWrite()`, `canEdit()`
- `\humhub\modules\content\models\Content` on `canEdit()`, `canView()`
- `\humhub\modules\file\models\File` on `canRead()`, `canDelete()`
Version 1.15
-------------------------
@ -51,3 +64,4 @@ Version 1.15
- `humhub\widgets\MarkdownField`
- `humhub\widgets\MarkdownFieldModals`
- `humhub\widgets\ModalConfirm`

View File

@ -7,6 +7,8 @@
*/
// comment out the following two lines when deployed to production
use humhub\helpers\DatabaseHelper;
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
@ -22,4 +24,10 @@ $config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/protected/config/web.php')
);
(new humhub\components\Application($config))->run();
try {
(new humhub\components\Application($config))->run();
} catch (\Throwable $ex) {
if (null === DatabaseHelper::handleConnectionErrors($ex)) {
throw $ex;
}
}

View File

@ -9,6 +9,7 @@
namespace humhub\commands;
use humhub\components\Module;
use humhub\helpers\DatabaseHelper;
use Yii;
use yii\console\Exception;
use yii\web\Application;
@ -78,9 +79,17 @@ class MigrateController extends \yii\console\controllers\MigrateController
*/
public function beforeAction($action)
{
// Make sure to define default table storage engine
if (in_array(Yii::$app->db->getDriverName(), ['mysql', 'mysqli'], true)) {
Yii::$app->db->pdo->exec('SET default_storage_engine=' . Yii::$app->params['databaseDefaultStorageEngine']);
// Make sure to define a default table storage engine
$db = Yii::$app->db;
try {
$db->open();
} catch (\Throwable $ex) {
DatabaseHelper::handleConnectionErrors($ex);
}
if (in_array($db->getDriverName(), ['mysql', 'mysqli'], true)) {
$db->pdo->exec('SET default_storage_engine=' . Yii::$app->params['databaseDefaultStorageEngine']);
}
return parent::beforeAction($action);
}

View File

@ -61,4 +61,9 @@ trait ApplicationTrait
{
$this->_homeUrl = $value;
}
public function getMailer(): MailerInterface
{
return parent::getMailer();
}
}

View File

@ -25,6 +25,7 @@ use yii\web\AssetBundle;
*
* @property-read string $name
* @property-read string $description
* @property-read array $keywords
* @property-read bool $isActivated
* @property SettingsManager $settings
* @author luke
@ -57,7 +58,7 @@ class Module extends \yii\base\Module
}
/**
* Returns modules name provided by module.json file
* Returns the module's name provided by module.json file
*
* @return string Name
*/
@ -73,7 +74,7 @@ class Module extends \yii\base\Module
}
/**
* Returns modules description provided by module.json file
* Returns the module's description provided by module.json file
*
* @return string Description
*/
@ -89,7 +90,7 @@ class Module extends \yii\base\Module
}
/**
* Returns modules version number provided by module.json file
* Returns the module's version number provided by module.json file
*
* @return string Version Number
*/
@ -121,6 +122,22 @@ class Module extends \yii\base\Module
return $url;
}
/**
* Returns module's keywords provided by module.json file
*
* @return array List of keywords
*/
public function getKeywords(): array
{
$info = $this->getModuleInfo();
if ($info['keywords']) {
return (array)$info['keywords'];
}
return [];
}
/**
* Returns the url of an asset file and publishes all module assets if
* the file is not published yet.
@ -272,11 +289,16 @@ class Module extends \yii\base\Module
* Delete all Migration Table Entries
*/
$migrations = opendir($migrationPath);
$params = [];
while (false !== ($migration = readdir($migrations))) {
if ($migration == '.' || $migration == '..' || $migration == 'uninstall.php') {
continue;
}
Yii::$app->db->createCommand()->delete('migration', ['version' => str_replace('.php', '', $migration)])->execute();
$command ??= Yii::$app->db->createCommand()->delete('migration', 'version = :version', $params);
$version = str_replace('.php', '', $migration);
$command->bindValue(':version', $version)->execute();
}
}

View File

@ -1,15 +1,19 @@
<?php
/**
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/** @noinspection UnknownInspectionInspection */
namespace humhub\components;
use ArrayAccess;
use humhub\components\bootstrap\ModuleAutoLoader;
use humhub\components\console\Application as ConsoleApplication;
use humhub\exceptions\InvalidArgumentTypeException;
use humhub\libs\BaseSettingsManager;
use humhub\models\ModuleEnabled;
use humhub\modules\admin\events\ModulesEvent;
@ -146,8 +150,7 @@ class ModuleManager extends Component
*/
public function register($basePath, $config = null)
{
$filename = $basePath . '/config.php';
if ($config === null && is_file($filename)) {
if ($config === null && is_file($filename = $basePath . '/config.php')) {
$config = include $filename;
}
@ -182,7 +185,7 @@ class ModuleManager extends Component
// Not enabled and no core/installer module
if (!$isCoreModule && !in_array($config['id'], $this->enabledModules)) {
return;
return $config['id'];
}
// Handle Submodules
@ -213,23 +216,106 @@ class ModuleManager extends Component
Yii::$app->setModule($config['id'], $moduleConfig);
// Register Event Handlers
if (isset($config['events'])) {
foreach ($config['events'] as $event) {
$eventClass = $event['class'] ?? $event[0];
$eventName = $event['event'] ?? $event[1];
$eventHandler = $event['callback'] ?? $event[2];
$eventData = $event['data'] ?? $event[3] ?? null;
$eventAppend = filter_var($event['append'] ?? $event[4] ?? true, FILTER_VALIDATE_BOOLEAN);
if (method_exists($eventHandler[0], $eventHandler[1])) {
Event::on($eventClass, $eventName, $eventHandler, $eventData, $eventAppend);
}
}
}
$this->registerEventHandlers($basePath, $config);
// Register Console ControllerMap
if (Yii::$app instanceof ConsoleApplication && !(empty($config['consoleControllerMap']))) {
Yii::$app->controllerMap = ArrayHelper::merge(Yii::$app->controllerMap, $config['consoleControllerMap']);
}
return $config['id'];
}
/**
* @throws InvalidConfigException
*/
protected function registerEventHandlers(string $basePath, array &$config): void
{
$events = $config['events'] ?? null;
$strict = $config['strict'] ?? false;
if (empty($events)) {
return;
}
$error = static function (string $message, bool $throw = false) use (&$config, $basePath) {
$message = sprintf("Configuration at %s has an invalid event configuration: %s", $basePath, $message);
if ($throw) {
throw new InvalidConfigException($message);
}
Yii::warning($message, $config['id']);
};
if (!ArrayHelper::isTraversable($events)) {
$error('events must be traversable', $strict);
return;
}
$getProperty = static function ($event, &$var, string $property, int $index, bool $throw = false) use ($error): bool {
$var = $event[$property] ?? $event[$index] ?? null;
if (empty($var)) {
$error(sprintf("required property '%s' missing!", $property), $throw);
return false;
}
return true;
};
foreach ($events as $event) {
if (empty($event)) {
continue;
}
if (!is_array($event) && !$event instanceof ArrayAccess) {
$error('event configuration must be an array or implement \ArrayAccess', $strict);
break;
}
if (!$getProperty($event, $eventClass, 'class', 0, $strict)) {
continue;
}
if (!$getProperty($event, $eventName, 'event', 1, $strict)) {
continue;
}
if (!$getProperty($event, $eventHandler, 'callback', 2, $strict)) {
continue;
}
if (!is_array($eventHandler)) {
$error("property 'callback' must be a callable defined in the array-notation denoting a method of a class", $strict);
continue;
}
if (!is_object($eventHandler[0] ?? null) && !class_exists($eventHandler[0] ?? null)) {
$error(sprintf("class '%s' does not exist.", $eventHandler[0] ?? ''), $strict);
continue;
}
if (!method_exists($eventHandler[0], $eventHandler[1])) {
$error(
sprintf(
"class '%s' does not have a method called '%s",
is_object($eventHandler[0]) ? get_class($eventHandler[0]) : $eventHandler[0],
$eventHandler[1]
),
$strict
);
continue;
}
$eventData = $event['data'] ?? $event[3] ?? null;
$eventAppend = filter_var($event['append'] ?? $event[4] ?? true, FILTER_VALIDATE_BOOLEAN);
Event::on($eventClass, $eventName, $eventHandler, $eventData, $eventAppend);
}
$events = null;
}
/**
@ -283,17 +369,18 @@ class ModuleManager extends Component
/**
* Filter modules by keyword and by additional filters from module event
*
* @param Module[] $modules
* @param array $filters
* @param Module[]|null $modules
* @param array|ArrayAccess $filters = ['keyword' => 'search term']
*
* @return Module[]
*/
public function filterModules(array $modules, $filters = []): array
public function filterModules(?array $modules, $filters = []): array
{
$filters = array_merge([
'keyword' => null,
], $filters);
if (!$filters instanceof ArrayAccess && !is_array($filters)) {
throw new InvalidArgumentTypeException('$filters', ['array', ArrayAccess::class], $filters);
}
$modules = $this->filterModulesByKeyword($modules, $filters['keyword']);
$modules = $this->filterModulesByKeyword($modules, $filters['keyword'] ?? null);
$modulesEvent = new ModulesEvent(['modules' => $modules]);
$this->trigger(static::EVENT_AFTER_FILTER_MODULES, $modulesEvent);
@ -304,12 +391,14 @@ class ModuleManager extends Component
/**
* Filter modules by keyword
*
* @param Module[] $modules
* @param Module[]|null $modules list of modules, defaulting to installed non-core modules
* @param null|string $keyword
* @return Module[]
*/
public function filterModulesByKeyword(array $modules, $keyword = null): array
public function filterModulesByKeyword(?array $modules, $keyword = null): array
{
$modules ??= $this->getModules();
if ($keyword === null) {
$keyword = Yii::$app->request->get('keyword', '');
}
@ -321,11 +410,16 @@ class ModuleManager extends Component
foreach ($modules as $id => $module) {
/* @var Module $module */
$searchFields = [$id];
if (isset($module->name)) {
$searchFields[] = $module->name;
if ($searchField = $module->getName()) {
$searchFields[] = $searchField;
}
if (isset($module->description)) {
$searchFields[] = $module->description;
if ($searchField = $module->getDescription()) {
$searchFields[] = $searchField;
}
if ($searchField = $module->getKeywords()) {
array_push($searchFields, ...$searchField);
}
$keywordFound = false;
@ -396,6 +490,10 @@ class ModuleManager extends Component
*/
public function getModule($id, $throwOnMissingModule = true)
{
if ($id instanceof Module) {
return $id;
}
// Enabled Module
if (Yii::$app->hasModule($id)) {
return Yii::$app->getModule($id, true);
@ -407,6 +505,10 @@ class ModuleManager extends Component
return Yii::createObject($class, [$id, Yii::$app]);
}
if (is_dir($id) && is_file($id . '/config.php')) {
return $this->getModule($this->register($id));
}
if ($throwOnMissingModule) {
throw new Exception('Could not find/load requested module: ' . $id);
}
@ -423,15 +525,16 @@ class ModuleManager extends Component
}
/**
* Checks the module can removed
* Checks if the module can be removed
*
* @param string $moduleId
* @return bool
* @throws Exception
*/
public function canRemoveModule($moduleId)
*
* @noinspection PhpDocMissingThrowsInspection
* */
public function canRemoveModule($moduleId): bool
{
$module = $this->getModule($moduleId);
/** @noinspection PhpUnhandledExceptionInspection */
$module = $this->getModule($moduleId, false);
if ($module === null) {
return false;
@ -460,11 +563,11 @@ class ModuleManager extends Component
* @throws Exception
* @throws \yii\base\ErrorException
*/
public function removeModule($moduleId, $disableBeforeRemove = true)
public function removeModule($moduleId, $disableBeforeRemove = true): ?string
{
$module = $this->getModule($moduleId);
if ($module == null) {
if ($module === null) {
throw new Exception('Could not load module to remove!');
}
@ -487,10 +590,13 @@ class ModuleManager extends Component
FileHelper::copyDirectory($moduleBasePath, $backupFolderName);
FileHelper::removeDirectory($moduleBasePath);
} else {
$backupFolderName = null;
//TODO: Delete directory
}
$this->flushCache();
return $backupFolderName;
}
/**
@ -517,7 +623,7 @@ class ModuleManager extends Component
public function enableModules($modules = [])
{
foreach ($modules as $module) {
$module = ($module instanceof Module) ? $module : $this->getModule($module);
$module = $this->getModule($module);
if ($module != null) {
$module->enable();
}
@ -537,11 +643,11 @@ class ModuleManager extends Component
$this->trigger(static::EVENT_BEFORE_MODULE_DISABLE, new ModuleEvent(['module' => $module]));
$moduleEnabled = ModuleEnabled::findOne(['module_id' => $module->id]);
if ($moduleEnabled != null) {
if ($moduleEnabled !== null) {
$moduleEnabled->delete();
}
if (($key = array_search($module->id, $this->enabledModules)) !== false) {
if (($key = array_search($module->id, $this->enabledModules, true)) !== false) {
unset($this->enabledModules[$key]);
}
@ -557,8 +663,8 @@ class ModuleManager extends Component
public function disableModules($modules = [])
{
foreach ($modules as $module) {
$module = ($module instanceof Module) ? $module : $this->getModule($module);
if ($module != null) {
$module = $this->getModule($module);
if ($module !== null) {
$module->disable();
}
}

View File

@ -41,7 +41,7 @@ class Application extends \yii\console\Application implements ApplicationInterfa
));
}
if (BaseSettingsManager::isDatabaseInstalled()) {
if (BaseSettingsManager::isDatabaseInstalled(Yii::$app->params['databaseInstalled'] ?? false)) {
$baseUrl = Yii::$app->settings->get('baseUrl');
if (!empty($baseUrl)) {
if (Yii::getAlias('@web', false) === false) {

View File

@ -8,8 +8,10 @@
namespace humhub\components\mail;
use humhub\interfaces\MailerInterface;
use Symfony\Component\Mime\Crypto\SMimeSigner;
use Yii;
use yii\mail\MessageInterface;
/**
* Mailer implements a mailer based on SymfonyMailer.
@ -18,7 +20,7 @@ use Yii;
* @since 1.2
* @author Luke
*/
class Mailer extends \yii\symfonymailer\Mailer
class Mailer extends \yii\symfonymailer\Mailer implements MailerInterface
{
/**
* @inheritdoc
@ -68,13 +70,7 @@ class Mailer extends \yii\symfonymailer\Mailer
{
$message = parent::compose($view, $params);
// Set HumHub default from values
if (empty($message->getFrom())) {
$message->setFrom([Yii::$app->settings->get('mailer.systemEmailAddress') => Yii::$app->settings->get('mailer.systemEmailName')]);
if ($replyTo = Yii::$app->settings->get('mailer.systemEmailReplyTo')) {
$message->setReplyTo($replyTo);
}
}
self::ensureHumHubDefaultFromValues($message);
if ($this->signingCertificatePath !== null && $this->signingPrivateKeyPath !== null) {
if ($this->signer === null) {
@ -92,6 +88,25 @@ class Mailer extends \yii\symfonymailer\Mailer
return $message;
}
/**
* @param MessageInterface $message
*
* @return void
*/
public static function ensureHumHubDefaultFromValues(MessageInterface $message): MessageInterface
{
// Set HumHub default from values
if ($message->getFrom()) {
return $message;
}
$message->setFrom([Yii::$app->settings->get('mailer.systemEmailAddress') => Yii::$app->settings->get('mailer.systemEmailName')]);
if ($replyTo = Yii::$app->settings->get('mailer.systemEmailReplyTo')) {
$message->setReplyTo($replyTo);
}
return $message;
}
/**
* @inheritdoc

View File

@ -0,0 +1,147 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017-2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\helpers;
use Throwable;
use Yii;
/**
* @since 1.15
*/
class DatabaseHelper
{
public static function handleConnectionErrors(
Throwable $ex,
bool $print = true,
bool $die = true,
bool $forcePlainText = false
): ?string {
static $last = false;
if (!$ex instanceof \yii\db\Exception) {
return null;
}
if ($last) {
return null;
}
$last = true;
$trace = debug_backtrace(0);
$trace = end($trace);
if ($trace && $trace['function'] === 'handleException' && $trace['args'][0] instanceof \yii\db\Exception) {
return null;
}
switch ($ex->getCode()) {
case 2002:
$error = 'Hostname not found.';
break;
case 1044:
$error = 'Database not found or not accessible.';
break;
case 1049:
$error = 'Database not found.';
break;
default:
$error = $ex->getMessage();
}
/**
* @see https://www.php.net/manual/en/ref.pdo-odbc.connection.php
* @see https://www.php.net/manual/en/ref.pdo-ibm.connection.php
* @see https://www.php.net/manual/en/ref.pdo-pgsql.connection.php
*/
$dsn = preg_replace(
'@((?<=:|;)(?:user|uid|User ID|pwd|password)=)(.*?)(?=;(?:$|\w+=)|$)@i',
'$1****',
Yii::$app->db->dsn
);
try {
$additionalInfo = json_encode([get_class($ex), ...$ex->errorInfo], JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$additionalInfo = 'N/A';
}
while ($ex->getPrevious()) {
$ex = $ex->getPrevious();
}
$htmlMessage = defined('YII_DEBUG') && YII_DEBUG
? sprintf('
<h1>Invalid database configuration</h1>
<p><strong>%s</strong></p>
<p>The following connection string was used:<br><code>%s</code></p>
<br>
<h2>Technical information</h2>
<p><code>%s</code></p>
<p><pre>%s</pre></p>
', $error, $dsn, $additionalInfo, $ex)
: sprintf('
<h1>Invalid database configuration</h1>
<p><strong>%s</strong></p>
', $error);
$txtMessage = defined('YII_DEBUG') && YII_DEBUG
? sprintf('
Invalid database configuration
==============================
%s
The following connection string was used:
%s
Technical information
---------------------
%s
%s
', $error, $dsn, $additionalInfo, $ex)
: sprintf('
Invalid database configuration
==============================
%s
The following connection string was used:
%s
Technical information
---------------------
%s
', $error, $dsn, $additionalInfo);
if ($print) {
if ($forcePlainText) {
echo $txtMessage;
} elseif (Yii::$app instanceof \yii\console\Application && Yii::$app->controller instanceof \yii\console\Controller) {
Yii::$app->controller->stderr($txtMessage);
} else {
header("HTTP/1.1 500 Internal Server Error");
echo $htmlMessage;
}
}
if (!$die) {
return $txtMessage;
}
die(1);
}
}

View File

@ -19,4 +19,6 @@ interface ApplicationInterface
* @event ActionEvent an event raised on init of application.
*/
public const EVENT_ON_INIT = 'onInit';
public function getMailer(): MailerInterface;
}

View File

@ -0,0 +1,28 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\interfaces;
use humhub\modules\user\models\User;
interface ArchiveableInterface
{
/**
* Checks if the given user can edit/create this element.
*
* @param User|integer $user user instance or user id
*
* @return bool can edit/create this element
* @since 1.15
*/
public function canArchive($user = null): bool;
public function archive(): bool;
public function unarchive(): bool;
}

View File

@ -0,0 +1,19 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\interfaces;
interface DeletableInterface
{
/**
* Checks if given item can be deleted.
*/
public function canDelete($userId = null);
public function delete();
}

View File

@ -0,0 +1,27 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\interfaces;
use humhub\modules\user\models\User;
use Throwable;
interface EditableInterface
{
/**
* Checks if the given user can edit/create this element.
*
* @param User|integer $user user instance or user id
*
* @return bool can edit/create this element
* @since 1.15
*/
public function canEdit($user = null): bool;
}

View File

@ -0,0 +1,13 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\interfaces;
interface MailerInterface extends \yii\mail\MailerInterface
{
}

View File

@ -0,0 +1,23 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\interfaces;
use humhub\modules\user\models\User;
interface ReadableInterface
{
/**
* Checks if given element can be read.
*
* @param string|User $userId
*
* @return bool
*/
public function canRead($userId = ""): bool;
}

View File

@ -0,0 +1,27 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\interfaces;
use humhub\modules\user\models\User;
use Throwable;
interface ViewableInterface
{
/**
* Checks if user can view this element.
*
* @param User|integer $user
*
* @return boolean can view this element
* @throws Throwable
* @since 1.15
*/
public function canView($user = null);
}

View File

@ -10,12 +10,12 @@ namespace humhub\libs;
use humhub\components\SettingActiveRecord;
use humhub\exceptions\InvalidArgumentTypeException;
use humhub\helpers\DatabaseHelper;
use Stringable;
use Yii;
use yii\base\Component;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\db\conditions\LikeCondition;
use yii\db\StaleObjectException;
use yii\helpers\Json;
@ -302,19 +302,20 @@ abstract class BaseSettingsManager extends Component
/**
* Checks if settings table exists or application is not installed yet
*
* @return bool
* @since 1.3
*/
public static function isDatabaseInstalled()
public static function isDatabaseInstalled(bool $dieOnError = false): bool
{
try {
if (in_array('setting', Yii::$app->db->schema->getTableNames())) {
return true;
}
$db = Yii::$app->db;
$db->open();
} catch (\Exception $ex) {
if ($dieOnError) {
DatabaseHelper::handleConnectionErrors($ex);
}
return false;
}
return false;
return in_array('setting', $db->schema->getTableNames());
}
}

View File

@ -65,16 +65,13 @@ class ModuleEnabled extends \yii\db\ActiveRecord
public static function getEnabledIds()
{
$enabledModules = Yii::$app->cache->get(self::CACHE_ID_ALL_IDS);
$cache = Yii::$app->cache;
$enabledModules = $cache->get(self::CACHE_ID_ALL_IDS);
if ($enabledModules === false) {
$enabledModules = [];
foreach (\humhub\models\ModuleEnabled::find()->all() as $em) {
$enabledModules[] = $em->module_id;
}
Yii::$app->cache->set(self::CACHE_ID_ALL_IDS, $enabledModules);
$enabledModules = self::find()->select('module_id')->createCommand()->queryColumn();
$cache->set(self::CACHE_ID_ALL_IDS, $enabledModules);
}
return $enabledModules;
}
}

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -47,7 +47,6 @@ class ModuleController extends Controller
*/
protected function getAccessRules()
{
return [
['permissions' => [ManageModules::class]],
['permissions' => [ManageSettings::class], 'actions' => ['index', 'list']]

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -343,7 +343,7 @@ class Comment extends ContentAddonActiveRecord
return $this->message;
}
public function canDelete($userId = '')
public function canDelete($userId = ''): bool
{
if ($userId == '') {

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -9,6 +9,9 @@
namespace humhub\modules\content\components;
use humhub\components\ActiveRecord;
use humhub\interfaces\DeletableInterface;
use humhub\interfaces\EditableInterface;
use humhub\interfaces\ReadableInterface;
use humhub\modules\content\interfaces\ContentOwner;
use humhub\modules\content\models\Content;
use humhub\modules\content\Module;
@ -36,9 +39,8 @@ use yii\base\Exception;
* @package humhub.components
* @since 0.5
*/
class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner
class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner, ReadableInterface, EditableInterface, DeletableInterface
{
/**
* @var boolean also update underlying contents last update stream sorting
*/
@ -136,9 +138,10 @@ class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner
* Checks if the given / or current user can delete this content.
* Currently only the creator can remove.
*
* @param null $userId
* @return boolean
*/
public function canDelete()
public function canDelete($userId = null): bool
{
if ($this->created_by == Yii::$app->user->id) {
return true;
@ -150,11 +153,12 @@ class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner
/**
* Check if current user can read this object
*
* @param string $userId
* @return boolean
*/
public function canRead()
public function canRead($userId = ""): bool
{
return $this->content->canView();
return $this->content->canView($userId);
}
/**
@ -162,24 +166,27 @@ class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner
*
* @return boolean
* @deprecated since 1.4
* @see static::canEdit()
*/
public function canWrite()
public function canWrite($userId = "")
{
return $this->canEdit();
return $this->canEdit($userId);
}
/**
* Checks if this record can be edited
*
* @param User|null $user the user
* @param User|int|null $user the user
* @return boolean
* @since 1.4
*/
public function canEdit(User $user = null)
public function canEdit($user = null): bool
{
if ($user === null && Yii::$app->user->isGuest) {
return false;
} elseif ($user === null) {
}
if ($user === null) {
/** @var User $user */
try {
$user = Yii::$app->user->getIdentity();
@ -189,7 +196,11 @@ class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner
}
}
if ($this->created_by == $user->id) {
if (!$user instanceof User && !($user = User::findOne(['id' => $user]))) {
return false;
}
if ($this->created_by === $user->id) {
return true;
}

View File

@ -0,0 +1,43 @@
<?php
use yii\db\Migration;
/**
* Class m210928_162609_stream_sort_idx
*/
class m210928_162609_stream_sort_idx extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createIndex('idx_stream_created', 'content', 'created_at', false);
$this->createIndex('idx_stream_updated', 'content', 'stream_sort_date', false);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
echo "m210928_162609_stream_sort_idx cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m210928_162609_stream_sort_idx cannot be reverted.\n";
return false;
}
*/
}

View File

@ -12,8 +12,11 @@ use humhub\components\ActiveRecord;
use humhub\components\behaviors\GUID;
use humhub\components\behaviors\PolymorphicRelation;
use humhub\components\Module;
use humhub\modules\activity\models\Activity;
use humhub\interfaces\ArchiveableInterface;
use humhub\interfaces\EditableInterface;
use humhub\interfaces\ViewableInterface;
use humhub\modules\admin\permissions\ManageUsers;
use humhub\modules\content\activities\ContentCreated as ActivitiesContentCreated;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\components\ContentContainerModule;
@ -22,21 +25,22 @@ use humhub\modules\content\events\ContentStateEvent;
use humhub\modules\content\interfaces\ContentOwner;
use humhub\modules\content\interfaces\SoftDeletable;
use humhub\modules\content\live\NewContent;
use humhub\modules\content\notifications\ContentCreated as NotificationsContentCreated;
use humhub\modules\content\permissions\CreatePrivateContent;
use humhub\modules\content\permissions\CreatePublicContent;
use humhub\modules\content\permissions\ManageContent;
use humhub\modules\content\services\ContentStateService;
use humhub\modules\content\services\ContentTagService;
use humhub\modules\notification\models\Notification;
use humhub\modules\post\models\Post;
use humhub\modules\search\libs\SearchHelper;
use humhub\modules\space\models\Space;
use humhub\modules\user\components\PermissionManager;
use humhub\modules\user\helpers\AuthHelper;
use humhub\modules\user\models\User;
use Throwable;
use Yii;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\db\ActiveQuery;
use yii\db\IntegrityException;
use yii\helpers\Url;
@ -79,19 +83,24 @@ use yii\helpers\Url;
* @property string $updated_at
* @property integer $updated_by
* @property ContentContainer $contentContainer
* @property-read mixed $contentName
* @property-read mixed $content
* @property-read ActiveQuery $tagRelations
* @property-read ContentActiveRecord $model
* @property-read mixed $contentDescription
* @property-read StateServiceInterface $stateService
* @property ContentContainerActiveRecord $container
* @mixin PolymorphicRelation
* @mixin GUID
* @since 0.5
*/
class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletable
class Content extends ActiveRecord implements Movable, ContentOwner, ArchiveableInterface, EditableInterface, ViewableInterface, SoftDeletable
{
/**
* The default stream channel.
* @since 1.6
*/
const STREAM_CHANNEL_DEFAULT = 'default';
public const STREAM_CHANNEL_DEFAULT = 'default';
/**
* A array of user objects which should informed about this new content.
@ -103,25 +112,25 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
/**
* @var int The private visibility mode (e.g. for space member content or user profile posts for friends)
*/
const VISIBILITY_PRIVATE = 0;
public const VISIBILITY_PRIVATE = 0;
/**
* @var int Public visibility mode, e.g. content which are visibile for followers
*/
const VISIBILITY_PUBLIC = 1;
public const VISIBILITY_PUBLIC = 1;
/**
* @var int Owner visibility mode, only visible for contentContainer + content owner
*/
const VISIBILITY_OWNER = 2;
public const VISIBILITY_OWNER = 2;
/**
* Content States - By default, only content with the "Published" state is returned.
*/
const STATE_PUBLISHED = 1;
const STATE_DRAFT = 10;
const STATE_SCHEDULED = 20;
const STATE_DELETED = 100;
public const STATE_PUBLISHED = 1;
public const STATE_DRAFT = 10;
public const STATE_SCHEDULED = 20;
public const STATE_DELETED = 100;
/**
* @var ContentContainerActiveRecord the Container (e.g. Space or User) where this content belongs to.
@ -137,7 +146,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
/**
* @event Event is used when a Content state is changed.
*/
const EVENT_STATE_CHANGED = 'changedState';
public const EVENT_STATE_CHANGED = 'changedState';
/**
* @inheritdoc
@ -225,7 +234,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
$this->created_by ??= Yii::$app->user->id;
}
$this->stream_sort_date = date('Y-m-d G:i:s');
$this->stream_sort_date = date('Y-m-d H:i:s');
if ($this->created_by == "") {
throw new Exception("Could not save content without created_by!");
@ -241,7 +250,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
{
if (array_key_exists('state', $changedAttributes)) {
// Run process for new content(Send notifications) only after changing state
$this->processNewContent();
$this->processNewContent($insert);
$model = $this->getModel();
if (!$insert && $model instanceof ContentActiveRecord && $this->getStateService()->isPublished()) {
@ -275,7 +284,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
parent::afterSave($insert, $changedAttributes);
}
private function processNewContent()
private function processNewContent($insert)
{
if (!$this->getStateService()->isPublished()) {
// Don't notify about not published Content
@ -287,6 +296,11 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
return;
}
if (!$insert) {
// Update creation datetime after first publishing
$this->updateAttributes(['created_at' => date('Y-m-d H:i:s')]);
}
$record = $this->getModel();
Yii::debug('Process new content: ' . get_class($record) . ' ID: ' . $record->getPrimaryKey(), 'content');
@ -345,12 +359,12 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
);
}
\humhub\modules\content\notifications\ContentCreated::instance()
NotificationsContentCreated::instance()
->from($this->createdBy)
->about($contentSource)
->sendBulk($userQuery);
\humhub\modules\content\activities\ContentCreated::instance()
ActivitiesContentCreated::instance()
->from($this->createdBy)
->about($contentSource)->save();
}
@ -372,7 +386,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
*/
public function afterDelete()
{
// Try delete the underlying object (Post, Question, Task, ...)
// Try to delete the underlying object (Post, Question, Task, ...)
$this->resetPolymorphicRelation();
/** @var ContentActiveRecord $record */
@ -557,19 +571,31 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
* @throws Exception
* @throws \yii\base\InvalidConfigException
*/
public function canArchive(): bool
public function canArchive($user = null): bool
{
// Currently global content can not be archived
if (!$this->getContainer()) {
return $this->canEdit();
}
$appUser = Yii::$app->user;
// No need to archive content on an archived container, content is marked as archived already
if ($this->getContainer()->isArchived()) {
if ($appUser->isGuest) {
return false;
}
return $this->getContainer()->permissionManager->can(new ManageContent());
if ($user === null) {
$user = $appUser->getIdentity();
} elseif (!($user instanceof User)) {
$user = User::findOne(['id' => $user]);
}
// Currently global content can not be archived
if (!$container = $this->getContainer()) {
return $this->canEdit($user);
}
// No need to archive content on an archived container, content is marked as archived already
if ($container->isArchived()) {
return false;
}
return $container->getPermissionManager($user)->can(new ManageContent());
}
/**
@ -589,7 +615,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
/**
* {@inheritdoc}
* @throws \Throwable
* @throws Throwable
*/
public function move(ContentContainerActiveRecord $container = null, $force = false)
{
@ -688,7 +714,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
* @param ContentContainerActiveRecord|null $container
* @return bool determines if the current user is generally permitted to move content on the given container (or the related container if no container was provided)
* @throws IntegrityException
* @throws \Throwable
* @throws Throwable
* @throws \yii\base\InvalidConfigException
*/
public function checkMovePermission(ContentContainerActiveRecord $container = null)
@ -783,7 +809,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
* Relation to ContentContainer model
* Note: this is not a Space or User instance!
*
* @return \yii\db\ActiveQuery
* @return ActiveQuery
* @since 1.1
*/
public function getContentContainer()
@ -794,7 +820,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
/**
* Returns the ContentTagRelation query.
*
* @return \yii\db\ActiveQuery
* @return ActiveQuery
* @since 1.2.2
*/
public function getTagRelations()
@ -805,7 +831,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
/**
* Returns all content related tags ContentTags related to this content.
*
* @return \yii\db\ActiveQuery
* @return ActiveQuery
* @since 1.2.2
*/
public function getTags($tagClass = ContentTag::class)
@ -852,19 +878,21 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
* @return bool can edit/create this content
* @throws Exception
* @throws IntegrityException
* @throws \Throwable
* @throws Throwable
* @throws \yii\base\InvalidConfigException
* @since 1.1
*/
public function canEdit($user = null)
public function canEdit($user = null): bool
{
if (Yii::$app->user->isGuest) {
$appUser = Yii::$app->user;
if ($appUser->isGuest) {
return false;
}
if ($user === null) {
$user = Yii::$app->user->getIdentity();
} else if (!($user instanceof User)) {
$user = $appUser->getIdentity();
} elseif (!($user instanceof User)) {
$user = User::findOne(['id' => $user]);
}
@ -882,17 +910,17 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
// Check additional manage permission for the given container
if ($this->container) {
if ($model->isNewRecord && $model->hasCreatePermission() && $this->container->getPermissionManager($user)->can($model->getCreatePermission())) {
if (($isNewRecord = $model->isNewRecord) && $model->hasCreatePermission() && $this->container->getPermissionManager($user)->can($model->getCreatePermission())) {
return true;
}
if (!$model->isNewRecord && $model->hasManagePermission() && $this->container->getPermissionManager($user)->can($model->getManagePermission())) {
if (!$isNewRecord && $model->hasManagePermission() && $this->container->getPermissionManager($user)->can($model->getManagePermission())) {
return true;
}
}
// Check if underlying models canEdit implementation
// ToDo: Implement this as interface
if (method_exists($model, 'canEdit') && $model->canEdit($user)) {
// ToDo: Send deprecation waring when not implementing EditableInterface
if (($model instanceof EditableInterface || method_exists($model, 'canEdit')) && $model->canEdit($user)) {
return true;
}
@ -947,14 +975,14 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
* @param User|integer $user
* @return boolean can view this content
* @throws Exception
* @throws \Throwable
* @throws Throwable
* @since 1.1
*/
public function canView($user = null)
public function canView($user = null): bool
{
if (!$user && !Yii::$app->user->isGuest) {
$user = Yii::$app->user->getIdentity();
} else if (!$user instanceof User) {
} elseif (!$user instanceof User) {
$user = User::findOne(['id' => $user]);
}
@ -1032,7 +1060,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
*/
public function updateStreamSortTime()
{
$this->updateAttributes(['stream_sort_date' => date('Y-m-d G:i:s')]);
$this->updateAttributes(['stream_sort_date' => date('Y-m-d H:i:s')]);
}
/**
@ -1083,5 +1111,4 @@ class Content extends ActiveRecord implements Movable, ContentOwner, SoftDeletab
{
$this->getStateService()->set($state, $options);
}
}

View File

@ -8,10 +8,9 @@
namespace humhub\modules\content\models;
use humhub\components\ActiveRecord;
use humhub\components\behaviors\PolymorphicRelation;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\space\models\Space;
use yii\db\ActiveRecord;
/**
* This is the model class for table "contentcontainer".

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -145,8 +145,8 @@ class ContentContainerStreamTest extends HumHubDbTestCase
$this->becomeUser('User2');
$deleteId = $this->createPost('Something to delete',['visibility' => Content::VISIBILITY_PRIVATE]);
$post = Post::findOne(['id' => $deleteId]);
$post->content->softDelete();
$content = Content::findOne(['id' => $deleteId]);
$content->softDelete();
$ids = $this->getStreamActionIds($this->space, 3);

View File

@ -20,7 +20,7 @@ use Throwable;
class RichTextPostProcessTest extends HumHubDbTestCase
{
public function _before()
private function addTestOEmbedRecords()
{
(new UrlOembed([
'url' => 'https://www.youtube.com/watch?v=yt1',
@ -31,12 +31,12 @@ class RichTextPostProcessTest extends HumHubDbTestCase
'url' => 'https://www.youtube.com/watch?v=yt2',
'preview' => 'yt2'
]))->save();
parent::_before();
}
public function testProcessSingleOembed()
{
$this->addTestOEmbedRecords();
$post = Post::findOne(['id' => 1]);
$text = '[https://www.youtube.com/watch?v=yt1](oembed:https://www.youtube.com/watch?v=yt1)';
$result = RichText::postProcess($text, $post);
@ -47,6 +47,8 @@ class RichTextPostProcessTest extends HumHubDbTestCase
public function testProcessNoneOembed()
{
$this->addTestOEmbedRecords();
$post = Post::findOne(['id' => 1]);
$text = '[Normal link](https://www.youtube.com/watch?v=yt1)';
$result = RichText::postProcess($text, $post);
@ -55,6 +57,8 @@ class RichTextPostProcessTest extends HumHubDbTestCase
public function testProcessMultipleOembed()
{
$this->addTestOEmbedRecords();
$post = Post::findOne(['id' => 1]);
$text = '[https://www.youtube.com/watch?v=yt1](oembed:https://www.youtube.com/watch?v=yt1)\n\n[https://www.youtube.com/watch?v=yt2](oembed:https://www.youtube.com/watch?v=yt2)';
$result = RichText::postProcess($text, $post);

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -28,6 +28,7 @@ use yii\db\IntegrityException;
use yii\db\StaleObjectException;
use yii\helpers\Url;
use yii\web\UploadedFile;
use humhub\interfaces\ViewableInterface;
/**
* This is the model class for table "file".
@ -85,7 +86,7 @@ use yii\web\UploadedFile;
* @since 0.5
* @noinspection PropertiesInspection
*/
class File extends FileCompat
class File extends FileCompat implements ViewableInterface
{
/**
* @event Event that is triggered after a new file content has been stored.
@ -285,10 +286,15 @@ class File extends FileCompat
*
* If the file is not an instance of HActiveRecordContent or HActiveRecordContentAddon
* the file is readable for all.
*
* @param string|User $userId
*
* @return bool
* @throws IntegrityException
* @throws Throwable
* @throws \yii\base\Exception
*/
public function canRead($userId = "")
public function canRead($userId = ""): bool
{
$object = $this->getPolymorphicRelation();
if ($object instanceof ContentActiveRecord || $object instanceof ContentAddonActiveRecord) {
@ -298,13 +304,26 @@ class File extends FileCompat
return true;
}
public function canView($user = null): bool
{
return $this->canRead($user);
}
/**
* Checks if given file can be deleted.
*
* If the file is not an instance of ContentActiveRecord or ContentAddonActiveRecord
* the file is readable for all unless there is method canEdit or canDelete implemented.
*
* @param null $userId
*
* @return bool
* @throws IntegrityException
* @throws InvalidConfigException
* @throws Throwable
* @throws \yii\base\Exception
*/
public function canDelete($userId = null)
public function canDelete($userId = null): bool
{
$object = $this->getPolymorphicRelation();

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -8,27 +8,28 @@
namespace humhub\modules\installer\commands;
use humhub\helpers\DatabaseHelper;
use humhub\libs\DynamicConfig;
use humhub\libs\UUID;
use humhub\modules\installer\libs\InitialData;
use humhub\modules\user\models\Group;
use humhub\modules\user\models\Password;
use humhub\modules\user\models\User;
use Yii;
use yii\base\Exception;
use yii\console\Controller;
use yii\console\ExitCode;
use yii\helpers\Console;
use yii\base\Exception;
use humhub\modules\user\models\User;
use humhub\modules\user\models\Password;
use humhub\modules\user\models\Group;
use humhub\modules\installer\libs\InitialData;
use humhub\libs\UUID;
use humhub\libs\DynamicConfig;
/**
* Console Install
*
*
* Example usage:
* php yii installer/write-db-config "$HUMHUB_DB_HOST" "$HUMHUB_DB_NAME" "$HUMHUB_DB_USER" "$HUMHUB_DB_PASSWORD"
* php yii installer/install-db
* php yii installer/write-site-config "$HUMHUB_NAME" "$HUMHUB_EMAIL"
* php yii installer/create-admin-account
*
*
*/
class InstallController extends Controller
{
@ -42,9 +43,9 @@ class InstallController extends Controller
return ExitCode::OK;
}
/**
* Tries to open a connection to given db.
* Tries to open a connection to given db.
* On success: Writes given settings to config-file and reloads it.
* On failure: Throws exception
*/
@ -80,12 +81,12 @@ class InstallController extends Controller
$this->stdout("Install DB:\n\n", Console::FG_YELLOW);
$this->stdout(" * Checking Database Connection\n", Console::FG_YELLOW);
if(!$this->checkDBConnection()){
throw new Exception("Could not connect to DB!");
if (true !== $message = $this->checkDBConnection()) {
throw new Exception($message ?? "Could not connect to DB!");
}
$this->stdout(" * Installing Database\n", Console::FG_YELLOW);
Yii::$app->cache->flush();
// Disable max execution time to avoid timeouts during migrations
@ini_set('max_execution_time', 0);
@ -119,7 +120,7 @@ class InstallController extends Controller
$user->profile->firstname = 'Sys';
$user->profile->lastname = 'Admin';
$user->profile->save();
$password = new Password();
$password->user_id = $user->id;
$password->setPassword($admin_pass);
@ -179,6 +180,8 @@ class InstallController extends Controller
/**
* Tries to open global db connection and checks result.
*
* @return true|null|string
*/
private function checkDBConnection()
{
@ -186,10 +189,9 @@ class InstallController extends Controller
// call setActive with true to open connection.
Yii::$app->db->open();
// return the current connection state.
return Yii::$app->db->getIsActive();
return Yii::$app->db->getIsActive() ?: null;
} catch (Exception $e) {
$this->stderr($e->getMessage());
return DatabaseHelper::handleConnectionErrors($e, false, false, true);
}
return false;
}
}

View File

@ -25,9 +25,9 @@ use Yii;
class AdminController extends Controller
{
/**
* @inerhitdoc
* @inheritdoc
*/
public function getAccessRules()
protected function getAccessRules()
{
return [
['permissions' => [ManageSettings::class]],

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -56,8 +56,7 @@ class Events extends BaseObject
}
foreach ($event->modules as $m => $module) {
/* @var ModelModule $module */
if (!$module->getFilterService()->isFiltered()) {
if ($module instanceof ModelModule && !$module->getFilterService()->isFiltered()) {
unset($event->modules[$m]);
}
}

View File

@ -24,7 +24,7 @@ class LicenceController extends Controller
/**
* @inheritdoc
*/
public function getAccessRules()
protected function getAccessRules()
{
return [
['permissions' => ManageModules::class]

View File

@ -68,7 +68,6 @@ class MailTarget extends BaseTarget
], $notification->getViewParams());
$mail = Yii::$app->mailer->compose($this->view, $viewParams)
->setFrom([Yii::$app->settings->get('mailer.systemEmailAddress') => Yii::$app->settings->get('mailer.systemEmailName')])
->setTo($recipient->email)
->setSubject(str_replace("\n", " ", trim($notification->getMailSubject())));
if ($replyTo = Yii::$app->settings->get('mailer.systemEmailReplyTo')) {

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -535,6 +535,9 @@ class StreamQuery extends Model
return;
}
/**
* Setup Sorting
*/
/**
* Setup Sorting
*/
@ -560,13 +563,28 @@ class StreamQuery extends Model
], [':to' => $this->to]);
}
} else {
$this->_query->orderBy('content.id DESC');
$this->_query->orderBy('content.created_at DESC,content.id DESC');
if (!empty($this->from)) {
$this->_query->andWhere("content.id < :from", [':from' => $this->from]);
$this->_query->andWhere(
['or',
"content.created_at < (SELECT created_at FROM content wd WHERE wd.id=:from)",
['and',
"content.created_at = (SELECT created_at FROM content wd WHERE wd.id=:from)",
"content.id < :from"
],
], [':from' => $this->from]);
} elseif (!empty($this->to)) {
$this->_query->andWhere("content.id > :to", [':to' => $this->to]);
$this->_query->andWhere(
['or',
"content.created_at > (SELECT created_at FROM content wd WHERE wd.id=:to)",
['and',
"content.created_at = (SELECT created_at FROM content wd WHERE wd.id=:to)",
"content.id > :to"
],
], [':to' => $this->to]);
}
}
}
/**

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -11,4 +11,5 @@ modules:
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false
transaction: true
cleanup: false

View File

@ -0,0 +1,16 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/** @noinspection MissedFieldInspection */
return [
'id' => basename(__DIR__),
'class' => \humhub\components\Module::class,
'namespace' => "Some\\Name\\Space",
'isCoreModule' => true,
];

View File

@ -0,0 +1,16 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/** @noinspection MissedFieldInspection */
return [
'id' => basename(__DIR__),
'class' => \humhub\components\Module::class,
'namespace' => "Some\\Name\\Space",
'isInstallerModule' => true,
];

View File

@ -0,0 +1,9 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
return [];

View File

@ -0,0 +1,13 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
$config = require dirname(__DIR__) . "/module1/config.php";
$config['id'] = basename(__DIR__);
$config['class'] = \yii\base\Module::class;
return $config;

View File

@ -0,0 +1,15 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace Some\Name\Space\module1;
class Module extends \humhub\components\Module
{
public const ID = 'module1';
public const NAMESPACE = __NAMESPACE__;
}

View File

@ -0,0 +1,27 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/** @noinspection MissedFieldInspection */
require_once __DIR__ . "/Module.php";
return [
'id' => 'module1',
'class' => \Some\Name\Space\module1\Module::class,
'namespace' => "Some\\Name\\Space\\module1",
'events' => [
[
'class' => \humhub\tests\codeception\unit\components\ModuleManagerTest::class,
'event' => 'valid',
'callback' => [
\humhub\tests\codeception\unit\components\ModuleManagerTest::class,
'handleEvent',
],
],
]
];

View File

@ -0,0 +1,25 @@
{
"id": "example",
"version": "1.0",
"name": "My Example Module 1",
"description": "My testing module 1.",
"humhub": {
"minVersion": "1.2"
},
"keywords": ["my", "cool", "one"],
"homepage": "https://www.example.com",
"authors": [
{
"name": "Tom Coder",
"email": "tc@example.com",
"role": "Developer"
},
{
"name": "Sarah Mustermann",
"email": "sm@example.com",
"homepage": "http://example.com",
"role": "Translator"
}
],
"licence": "AGPL-3.0-or-later"
}

View File

@ -0,0 +1,15 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace Some\Name\Space\module2;
class Module extends \humhub\components\Module
{
public const ID = 'module2';
public const NAMESPACE = __NAMESPACE__;
}

View File

@ -0,0 +1,27 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/** @noinspection MissedFieldInspection */
require_once __DIR__ . "/Module.php";
return [
'id' => 'module2',
'class' => \Some\Name\Space\module2\Module::class,
'namespace' => "Some\\Name\\Space\\module2",
'events' => [
[
'class' => \humhub\tests\codeception\unit\components\ModuleManagerTest::class,
'event' => 'valid',
'callback' => [
\humhub\tests\codeception\unit\components\ModuleManagerTest::class,
'handleEvent',
],
],
]
];

View File

@ -0,0 +1,25 @@
{
"id": "example",
"version": "1.0",
"name": "My Example Module 2",
"description": "My testing module 2.",
"humhub": {
"minVersion": "1.2"
},
"keywords": ["my", "cool", "two"],
"homepage": "https://www.example.com",
"authors": [
{
"name": "Tom Coder",
"email": "tc@example.com",
"role": "Developer"
},
{
"name": "Sarah Mustermann",
"email": "sm@example.com",
"homepage": "http://example.com",
"role": "Translator"
}
],
"licence": "AGPL-3.0-or-later"
}

View File

@ -49,6 +49,8 @@ class HumHubDbTestCase extends Unit
public $time;
public array $firedEvents = [];
protected function setUp(): void
{
if (\Yii::$app !== null) {

View File

@ -34,6 +34,7 @@ use yii\db\ActiveRecord;
use yii\db\Command;
use yii\db\ExpressionInterface;
use yii\db\Query;
use yii\helpers\ArrayHelper;
use yii\helpers\FileHelper;
use yii\log\Dispatcher;
@ -619,15 +620,15 @@ trait HumHubHelperTrait
* @since 1.15
* @see static::assertEvents()
*/
public function handleEvent(Event $event)
public function handleEvent(Event $event, array $eventData = [])
{
$eventData = [
$eventData = ArrayHelper::merge([
'class' => get_class($event),
'event' => $event->name,
'sender' => $event->sender,
'data' => $event->data,
'handled' => $event->handled,
];
], $eventData);
$this->firedEvents[] = $eventData;
}

View File

@ -0,0 +1,25 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace tests\codeception\_support;
use humhub\components\mail\Mailer;
use humhub\interfaces\MailerInterface;
/**
* @since 1.15
*/
class TestMailer extends \Codeception\Lib\Connector\Yii2\TestMailer implements MailerInterface
{
public function compose($view = null, array $params = [])
{
$message = parent::compose($view, $params);
return Mailer::ensureHumHubDefaultFromValues($message);
}
}

View File

@ -3,6 +3,7 @@
/**
* Application configuration shared by all test types
*/
$default = [
'name' => 'HumHub Test',
'language' => 'en-US',
@ -32,6 +33,13 @@ $default = [
'scriptUrl' => '/index-test.php',
],
],
'container' => [
'definitions' => [
\Codeception\Lib\Connector\Yii2\TestMailer::class => [
'class' => \tests\codeception\_support\TestMailer::class,
]
]
],
'modules' => [
'user' => [
'passwordStrength' => [

View File

@ -0,0 +1,29 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\tests\codeception\unit\components;
use humhub\components\ModuleManager;
class ModuleManagerMock extends ModuleManager
{
public function &myModules(): array
{
return $this->modules;
}
public function &myEnabledModules(): array
{
return $this->enabledModules;
}
public function &myCoreModules(): array
{
return $this->coreModules;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -385,7 +385,7 @@ class SettingsManagerTest extends HumHubDbTestCase
// changing the value behind the scenes
$value2 = 'third value';
$this->dbUpdate($table, ['value' => $value2], ['name' => $setting, 'module_id' => $module]);
self::dbUpdate($table, ['value' => $value2], ['name' => $setting, 'module_id' => $module]);
$this->assertRecordValue($value2, 'value', $table, ['name' => $setting, 'module_id' => $module]);
// getting the value now should still show tho "old" value

View File

@ -17,30 +17,30 @@ use Yii;
class ModuleAutoLoaderTest extends Unit
{
/** @var array list of expected core modules */
const EXPECTED_CORE_MODULES = [
'activity',
'admin',
'comment',
'content',
'dashboard',
'file',
'friendship',
'installer',
'ldap',
'like',
'live',
'marketplace',
'notification',
'post',
'queue',
'search',
'space',
'stream',
'topic',
'tour',
'ui',
'user',
'web'
public const EXPECTED_CORE_MODULES = [
'humhub\modules\activity\Module' => 'activity',
'humhub\modules\admin\Module' => 'admin',
'humhub\modules\comment\Module' => 'comment',
'humhub\modules\content\Module' => 'content',
'humhub\modules\dashboard\Module' => 'dashboard',
'humhub\modules\file\Module' => 'file',
'humhub\modules\friendship\Module' => 'friendship',
'humhub\modules\installer\Module' => 'installer',
'humhub\modules\ldap\Module' => 'ldap',
'humhub\modules\like\Module' => 'like',
'humhub\modules\live\Module' => 'live',
'humhub\modules\marketplace\Module' => 'marketplace',
'humhub\modules\notification\Module' => 'notification',
'humhub\modules\post\Module' => 'post',
'humhub\modules\queue\Module' => 'queue',
'humhub\modules\search\Module' => 'search',
'humhub\modules\space\Module' => 'space',
'humhub\modules\stream\Module' => 'stream',
'humhub\modules\topic\Module' => 'topic',
'humhub\modules\tour\Module' => 'tour',
'humhub\modules\ui\Module' => 'ui',
'humhub\modules\user\Module' => 'user',
'humhub\modules\web\Module' => 'web',
];
/**

View File

@ -8,6 +8,8 @@
* @license http://www.yiiframework.com/license/
*/
use humhub\helpers\DatabaseHelper;
defined('YII_DEBUG') or define('YII_DEBUG', true);
// fcgi doesn't have STDIN and STDOUT defined by default
@ -25,6 +27,11 @@ $config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/config/console.php')
);
$application = new humhub\components\console\Application($config);
$exitCode = $application->run();
exit($exitCode);
try {
$exitCode = (new humhub\components\console\Application($config))->run();
exit($exitCode);
} catch (\Throwable $ex) {
if (null === DatabaseHelper::handleConnectionErrors($ex)) {
throw $ex;
}
}