Compare commits

...

48 Commits

Author SHA1 Message Date
Yuriy Bakhtin
24052f9fff
Fix access to options on space invite form (#7492) 2025-04-11 08:50:31 +02:00
Lucas Bartholemy
e927b4256f Update Composer dependencies 2025-04-10 17:20:33 +02:00
Yuriy Bakhtin
daf7ac12d2
Catch errors in external file handlers (#7486)
Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
2025-04-10 13:39:41 +02:00
Yuriy Bakhtin
a483f791a7
Fix plural wording (#7490) 2025-04-10 13:38:36 +02:00
Yuriy Bakhtin
cc4b81a30c
Fix tests (#7488)
* Fix tests

* Fix tests

* Fix artifact uploading from modules

* Fix tests

* Upload logs on failure test

* Fix empty content model error from test log

* Update command `content-search/rebuild` on tests

* Upload DB dump on failure test

* Add more info on error profile field deleting

* Revert timeout in tests

* Fix user image widget without provided user

* Upload DB dump for each failed acceptance test

* Add missed profile for test user #8

* Upload app log for each failed acceptance test

* Downgrade Selenium

* Update php-test.yml

* Fix tests for profile of the user #8

---------

Co-authored-by: Lucas Bartholemy <lucas@bartholemy.com>
Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
2025-04-10 11:47:30 +02:00
Yuriy Bakhtin
9a21276770
Fix comments list when comment is active from another parent comment (#7487)
* Fix comments list when comment is active from another parent comment

* Update CHANGELOG.md
2025-04-08 19:12:30 +02:00
Lucas Bartholemy
6b04a6a83e
Update common.php 2025-04-07 12:15:58 +02:00
Yuriy Bakhtin
e4360f144c
Use password type on the installation DB config form (#7484) 2025-04-07 11:15:00 +02:00
Lucas Bartholemy
7e1a9aad94 Prepare 1.17.2 Release 2025-04-07 10:17:26 +02:00
Yuriy Bakhtin
edabe6036c
Fix the post submit button title after back from draft mode (#7482) 2025-04-04 18:57:51 +02:00
github-actions[bot]
9124eff27f
Autocommit PHP CS Fixer (#7480)
Co-authored-by: GitHub Action <action@github.com>
2025-04-03 12:07:50 +02:00
Marc Farré
0c56f2c7fa
Enh/add share intend feature (#7455)
* Enh: Add the possibility to create a content from a modal for the Mobile app Share Intend feature, in the Profile, a Space, or a new Conversation

* Fix Space Search URL

* Small emprovements

* Hide the content types menu (allow posting only)

* Hide "Schedule" feature

* display the richtext toolbar directly (no need to click in the editor)

* Reuse the ShareService::searchSpaces() from the Share Between module and move it to core, in a ContentCreationService

* Merge master branch

* Move Controller and Form to new ShareIntend classes

* Refactor ShareIntend

* https://github.com/humhub/app-internal/issues/76#issuecomment-2734995419

* Remove useless UserPickerField::getItemText()

* Add Cancel button

* After content creation, if the dashboard stream is not displayed on the current page, redirect to the content container, to make sure the user sees the new content

* Fix for non-pretty URLs

* https://github.com/humhub/app-internal/issues/76#issuecomment-2749597714

* Change wording

* Create ShareIntendTargetForm::canPostInOwnProfile() to avoid duplicate

* Fix Space Picker

* Improve Share Intend Modal ID

* Improve WallCreateContentFormFooter param handling

* Allow null for WallCreateContentForm in Footer

* Fixed wrong variable assignment

* Check if the files exists, and if user is the owner of the files && Fix submit URL

* Remove SearchContainerController and force defining the url when using ContentContainerPicker

* Rename ContentContainerPicker to ContentContainerPickerField

* Allow Create Content form in plain and modal at the same time

* Fix schedule and content form message ID for modal

* Fix: Remove added Dashboard stream ID and use Class instead

---------

Co-authored-by: Lucas Bartholemy <lucas@bartholemy.com>
2025-04-03 12:02:21 +02:00
Yuriy Bakhtin
09b59a8bbc
Don't require approval for user created from administration (#7479)
* Don't require approval for user created from administration

* Don't require approval for user created from administration
2025-03-31 14:15:51 +02:00
github-actions[bot]
98b0dce221
Autocommit PHP CS Fixer (#7478)
Co-authored-by: GitHub Action <action@github.com>
2025-03-26 16:33:47 +01:00
Lucas Bartholemy
3b6ec3d127
Refactor Registration Form Options (#7477)
* Refactor Registration Form Options

* Update CHANGELOG.md

* Update MIGRATE-DEV.md
2025-03-26 16:32:14 +01:00
Yuriy Bakhtin
6a612c93f5
Fix missing fields when creating a new user from admin (#7473) 2025-03-22 10:47:30 +01:00
Marc Farré
6f98ac37b1
Fix: Formatted Arabic numbers displays 0 instead of the number (#7465)
* Fix: Formatted Arabic numbers displays 0 instead of the number

* Add PR ID in CHANGELOG

* Round before converting to current language

* Fix: Formatted Arabic numbers displays 0 instead of the number

* Fix: Formatted Arabic numbers displays 0 instead of the number

* Fix: Formatted Arabic numbers displays 0 instead of the number

---------

Co-authored-by: Yuriy Bakhtin <yurybakh@gmail.com>
Co-authored-by: Yuriy Bakhtin <yurabakhtin@users.noreply.github.com>
2025-03-21 19:48:38 +01:00
Yuriy Bakhtin
164105b9a6
Fix advanced searching by space filter (#7471) 2025-03-21 17:30:30 +01:00
Yuriy Bakhtin
a8968c6fef
Fix searching with not full latin words (#7464) 2025-03-18 09:38:12 +01:00
Yuriy Bakhtin
c8be19776f
Don't reset additional registration form elements on init default elements (#7461) 2025-03-13 13:43:22 +01:00
Yuriy Bakhtin
163d948e6b
Fix changing of space visibility (#7460) 2025-03-11 10:49:59 +01:00
Lucas Bartholemy
a8ca3746ae Increase Version 2025-03-06 16:14:33 +01:00
Lucas Bartholemy
365705433b Prepare 1.17.1 Release 2025-03-06 15:46:22 +01:00
Gevorg Mansuryan
bfdc4a913a
Master Tests broken #575 (#7453)
* Master Tests broken #575

* Master Tests broken #575
2025-03-03 17:05:08 +01:00
Yuriy Bakhtin
03d21e4bd0
Update code to manage users from external modules (#7452) 2025-03-03 11:29:07 +01:00
Yuriy Bakhtin
fe53f44547
Update code to manage users from external modules (#7445)
* Update code to manage users from external modules

* Check POST data on load HForm

* Check POST data on load HForm

* Revert init AuthClientUserService
2025-02-27 19:26:28 +01:00
Marc Farré
8f9d04389a
Fix: Hide "Remember me" option on mobile app (#7441)
* Fix: Hide "Remember me" option on mobile app - https://github.com/humhub/app/issues/281

* Add PR ID to CHANGELOG
2025-02-24 15:26:37 +01:00
Gevorg Mansuryan
c904807b0a
Add beforeInitCallback humhub.ui.picker (#7437)
* Add beforeInitCallback `humhub.ui.picker`

* Add beforeInitCallback humhub.ui.picker
2025-02-22 09:25:01 +01:00
Yuriy Bakhtin
e5bb6a246a
Fix picker autofocus (#7428)
Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
2025-02-18 16:32:38 +01:00
Yuriy Bakhtin
99969d2175
New event on get registration groups (#7424) 2025-02-18 15:57:45 +01:00
Lucas Bartholemy
2885844519 Fixed Group Back Link in Module Context 2025-02-08 18:03:37 +01:00
Yuriy Bakhtin
ad0b0c8b86
Fix profile field encoding (#7420)
* Fix profile field encoding

* Update MIGRATE-DEV.md

* Fix profile field encoding

---------

Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
2025-02-06 19:57:29 +01:00
Yuriy Bakhtin
ae3502fe82
Reset modal window after close (#7419) 2025-02-06 11:31:28 +01:00
Gevorg Mansuryan
38025387c7
Enh/collapsed comments (#7382)
* Collapsed comments #508

* Collapsed comments

* collapsed comments

* Fix sorting on load previous comments

---------

Co-authored-by: Yuriy Bakhtin <yurybakh@gmail.com>
2025-01-31 14:39:58 +01:00
Marc Farré
e6d72bd182
Fix: Always allow admins to edit/delete content in the other Profile stream (#7409) 2025-01-22 10:56:59 +01:00
Lucas Bartholemy
645651cd60
Use realpath() to handle symlinked dirs (#7404)
* Use realpath() to handle symlinked dirs

* Updated Changelog
2025-01-20 17:49:43 +01:00
Gevorg Mansuryan
e30c00b523
The "Default user profile visibility" is not displayed any more… (#7400)
* 1.17: The "Default user profile visibility" is not displayed any more #7397

* 1.17: The "Default user profile visibility" is not displayed any more #7397

* 1.17: The "Default user profile visibility" is not displayed any more #7397

* 1.17: The "Default user profile visibility" is not displayed any more #7397

* 1.17: The "Default user profile visibility" is not displayed any more #7397
2025-01-20 14:55:46 +01:00
Usama Ayaz
02400a8eae
Correct typo in comments (#7402) 2025-01-19 23:06:27 +01:00
Yuriy Bakhtin
76a70079a2
Fix profile stream for guests (#7396) 2025-01-17 13:37:09 +01:00
Yuriy Bakhtin
c2737c384c
Show language chooser for guest on the sidebar footer (#7384)
* Show language chooser for guest on the sidebar footer

* Add language chooser to main footer navigation

* Remove static/js/humhub-app.js.tmp that was added by error

---------

Co-authored-by: Marc Farré <contact@marc.fun>
2025-01-15 16:05:51 +01:00
Yuriy Bakhtin
056e0d4654
Save current language for email invitation and show language chooser on registration form (#7381)
Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
2025-01-15 10:47:49 +01:00
Yuriy Bakhtin
e31c77d603
Improve SelfTest for Base URL (#7383)
* Improve SelfTest for Base URL

* Update CHANGELOG.md

---------

Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
2025-01-14 19:33:41 +01:00
Lucas Bartholemy
c238611a65 Fix #7377: Configured Redis cache key prefix was overwritten by the default value 2025-01-13 18:57:11 +01:00
github-actions[bot]
eecf111971
Autocommit PHP CS Fixer (#7379)
Co-authored-by: GitHub Action <action@github.com>
2025-01-13 16:49:31 +01:00
github-actions[bot]
eca400bbae
Autocommit PHP CS Fixer (#7378)
Co-authored-by: GitHub Action <action@github.com>
2025-01-13 16:49:08 +01:00
Lucas Bartholemy
6163852d8a Reload Settings after Each Request 2025-01-13 15:56:09 +01:00
Lucas Bartholemy
376deed743 Updated CHANGELOG 2025-01-13 15:49:26 +01:00
Lucas Bartholemy
80b24f90c6 Disable Cache during Installer 2025-01-13 15:47:54 +01:00
103 changed files with 1895 additions and 980 deletions

View File

@ -1,6 +1,7 @@
name: PHP Codeception Tests
on:
workflow_dispatch:
push:
branches:
- master
@ -56,7 +57,7 @@ jobs:
steps:
- name: Start Selenium
run: |
docker run --detach --net=host --shm-size="2g" selenium/standalone-chrome
docker run --detach --net=host --shm-size="2g" selenium/standalone-chrome:108.0-20250123
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -119,7 +120,7 @@ jobs:
run: php protected/humhub/tests/codeception/bin/yii installer/auto
- name: Rebuild search index
run: php protected/humhub/tests/codeception/bin/yii search/rebuild
run: php protected/humhub/tests/codeception/bin/yii content-search/rebuild
- name: Run test server
run: php --server 127.0.0.1:8080 index-test.php &>/tmp/phpserver.log &
@ -138,4 +139,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: codeception-output
path: protected/humhub/tests/codeception/_output/*
path: |
protected/humhub/tests/codeception/_output/*
protected/humhub/modules/*/tests/codeception/_output/*
protected/runtime/logs/*

View File

@ -1,6 +1,43 @@
HumHub Changelog
================
1.17.3 (Unreleased)
----------------------
- Fix #7484: Use password type on the installation DB config form
- Fix #7486: Catch errors in external file handlers
- Fix #7487: Fix comments list when comment is active from another parent comment
- Enh #7492: Fix access to options on space invite form
1.17.2 (April 7, 2025)
----------------------
- Fix #7457: Fix changing of space visibility
- Fix #7464: Fix searching with not full latin words
- Fix #7465: Formatted Arabic numbers displays 0 instead of the number
- Fix #7471: Fix advanced searching by space filter
- Fix #7472: Fix missing fields when creating a new user from admin
- Fix #7477: Refactor Registration Form Options
- Enh #7455: Add Share Intend feature for the Mobile app
- Fix #7482: Fix the post submit button title after back from draft mode
1.17.1 (March 6, 2025)
----------------------
- Fix #7377: Configured Redis cache key prefix was overwritten by the default value
- Fix #7375: Use default language for email invitation and show language chooser on registration form
- Enh #7383: Improve SelfTest for Base URL
- Enh #5426: Show language chooser for guest on the sidebar footer
- Fix #7395: Fix profile stream for guests
- Fix #7400: Fixed `Default user profile visibility` field visibility in the user settings
- Fix #7404: Marketplace - Allow symlinked `@app/modules` directory
- Fix: Always allow admins to edit/delete content in the other Profile stream
- Fix #7414: Fix profile field encoding
- Fix #7419: Reset modal window after close
- Fix #7428: Fix picker autofocus
- Enh #7424: New event on get registration groups
- Enh #7437: Add beforeInitCallback `humhub.ui.picker`
- Fix #7441: Hide "Remember me" option on mobile app
- Enh #7408: Update code to manage users from external modules
- Fix #7453: Fix registration form submit
1.17.0 (January 13, 2025)
-------------------------
@ -10,6 +47,7 @@ Info: The minimum PHP version is now `PHP 8.1`!
- Fix #7365: `DeviceDetectorHelper::isMobile()` and `DeviceDetectorHelper::isTablet()` when no user agent
- Fix #7376: `humhub\helpers\ArrayHelper::flatten()` not compatible Yii base ArrayHelper
- Enh #7382: Allow initial collapsed comments bu setting `\humhub\modules\comment\Module::$commentsPreviewMax` to 0
1.17.0-beta.4 (December 24, 2024)
---------------------------------

View File

@ -1,14 +1,24 @@
Module Migration Guide
======================
Version 1.17.2
---------------
Version 1.17 (Unreleased)
### Behaviour change
- Method signature changed - `humhub\modules\user\models\fieldtype\BaseType::getUserValue(User $user, bool $raw = true, bool $encode = true): ?string`
- Constructor changed - `humhub\modules\user\models\forms\Registration` and properties (`$enablePasswordForm`, `$enableMustChangePassword`, `$enableEmailField`) are now private
Version 1.17
-------------------------
### Behaviour change
- Forms in modal box no longer have focus automatically on the first field. [The `autofocus` attribute](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/autofocus) is now required on the field. More info: [#7136](https://github.com/humhub/humhub/issues/7136)
- The new "Manage All Content" Group Permission allows managing all content (view, edit, move, archive, pin, etc.) even if the user is not a super administrator. It is disabled by default. It can be enabled via the configuration file, using the `\humhub\modules\admin\Module::$enableManageAllContentPermission` option.
- System admins are allowed, in all cases (even when `enableManageAllContentPermission` is disabled), to edit and delete content in other Profile streams.
- Users allowed to "Manage Users" can no longer move all content: they need to be allowed to "Manage All Content".
### New

View File

@ -31,6 +31,7 @@
"laminas/laminas-zendframework-bridge": "^1.4",
"matthewbdaly/zendsearch": "^0.0.3",
"mistic100/randomcolor": "^1.0",
"mobiledetect/mobiledetectlib": "4.8.09",
"npm-asset/animate.css": "^4.0",
"npm-asset/bluebird": "^3.3.5",
"npm-asset/blueimp-file-upload": "^9.24",
@ -87,8 +88,7 @@
"yiisoft/yii2-jui": "^2.0.0",
"yiisoft/yii2-queue": "^2.3.0",
"yiisoft/yii2-redis": "^2.0.0",
"yiisoft/yii2-symfonymailer": "^2.0",
"mobiledetect/mobiledetectlib": "4.8.09"
"yiisoft/yii2-symfonymailer": "^2.0"
},
"replace": {
},

1052
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -56,11 +56,15 @@ class HForm extends \yii\base\Component
*/
public $markedAsSubmitted = false;
public function __construct($definition = [], $primaryModel = null)
public function __construct($definition = [], $primaryModel = null, array $config = [])
{
$this->definition = $definition;
$this->primaryModel = $primaryModel;
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
$this->trigger(static::EVENT_AFTER_INIT);
}
@ -69,11 +73,12 @@ class HForm extends \yii\base\Component
{
if (Yii::$app->request->method == 'POST') {
if ($buttonName == "" || isset($_POST[$buttonName])) {
$allowedPostData = $this->getAllowedPostData();
foreach ($this->models as $model) {
$model->load(Yii::$app->request->post());
$model->load($allowedPostData);
}
if ($this->primaryModel !== null) {
$this->primaryModel->load(Yii::$app->request->post());
$this->primaryModel->load($allowedPostData);
}
return true;
}
@ -84,6 +89,32 @@ class HForm extends \yii\base\Component
return false;
}
protected function getAllowedPostData(): array
{
$post = Yii::$app->request->post();
foreach ($this->models as $modelName => $model) {
$className = substr(strrchr(get_class($model), '\\'), 1);
if (!isset($post[$className])) {
continue;
}
if (!isset($this->definition['elements'][$modelName])) {
// Remove post data of the object if no definition
unset($post[$className]);
}
if (isset($this->definition['elements'][$modelName]['elements'])) {
foreach ($this->definition['elements'][$modelName]['elements'] as $elementName => $element) {
if (!empty($element['readonly']) && isset($post[$className][$elementName])) {
// Remove a readonly field from the POST data
unset($post[$className][$elementName]);
}
}
}
}
return $post;
}
public function validate()
{
$hasErrors = false;

View File

@ -581,8 +581,9 @@ class ModuleManager extends Component
$marketplaceModule = Yii::$app->getModule('marketplace');
if ($marketplaceModule !== null) {
// Normalize paths before comparing in order to fix issues like Windows path separators `\`
// Realpath is required when the modules path is a symlinked
$modulePath = FileHelper::normalizePath($module->getBasePath());
$aliasPath = FileHelper::normalizePath(Yii::getAlias($marketplaceModule->modulesPath));
$aliasPath = FileHelper::normalizePath(realpath(Yii::getAlias($marketplaceModule->modulesPath)));
if (strpos($modulePath, $aliasPath) !== false) {
return true;
}

View File

@ -89,8 +89,13 @@ class Formatter extends \yii\i18n\Formatter
*/
public function asShortInteger($value, $options = [], $textOptions = [])
{
list($params, $position) = $this->formatNumber($value, null, 2, 1000, $options, $textOptions);
$params['nFormatted'] = floor((float)$params['nFormatted']);
list($params, $position) = $this->formatNumber($value, 0, 2, 1000, $options, $textOptions);
if ($position < 3 && mb_strlen($params['nFormatted']) === 4) {
// Convert 1000K to 1M or 1000M to 1B
$params['nFormatted'] = mb_substr($params['nFormatted'], 0, 1);
$position++;
}
switch ($position) {
case 0:

View File

@ -42,7 +42,7 @@ $logTargetConfig = [
$config = [
'name' => 'HumHub',
'version' => '1.17.0',
'version' => '1.17.3-master',
'minRecommendedPhpVersion' => '8.1',
'minSupportedPhpVersion' => '8.1',
'basePath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR,

View File

@ -9,6 +9,7 @@
namespace humhub\libs;
use humhub\widgets\Button;
use yii\base\Event;
use yii\grid\Column;
use yii\helpers\Url;
use humhub\libs\Html;
@ -20,6 +21,8 @@ use humhub\libs\Html;
*/
class ActionColumn extends Column
{
public const EVENT_AFTER_INIT_ACTIONS = 'afterInitActions';
/**
* @var string the ID attribute of the model, to generate action URLs.
*/
@ -85,6 +88,8 @@ class ActionColumn extends Column
return call_user_func($this->actions, $model, $key, $index, $this);
}
Event::trigger($this, self::EVENT_AFTER_INIT_ACTIONS);
return $this->actions;
}

View File

@ -92,21 +92,22 @@ class DynamicConfig extends BaseObject
// Add Caching
$cacheClass = Yii::$app->settings->get('cacheClass');
$cacheKeyPrefix = empty($config['components']['cache']['keyPrefix']) ? Yii::$app->id : $config['components']['cache']['keyPrefix'];
if (in_array($cacheClass, ['yii\caching\DummyCache', 'yii\caching\FileCache'])) {
$config['components']['cache'] = [
'class' => $cacheClass,
'keyPrefix' => Yii::$app->id,
'keyPrefix' => $cacheKeyPrefix,
];
} elseif ($cacheClass == 'yii\caching\ApcCache' && (function_exists('apcu_add') || function_exists('apc_add'))) {
$config['components']['cache'] = [
'class' => $cacheClass,
'keyPrefix' => Yii::$app->id,
'keyPrefix' => $cacheKeyPrefix,
'useApcu' => (function_exists('apcu_add')),
];
} elseif ($cacheClass === \yii\redis\Cache::class) {
$config['components']['cache'] = [
'class' => \yii\redis\Cache::class,
'keyPrefix' => Yii::$app->id,
'keyPrefix' => $cacheKeyPrefix,
];
}

View File

@ -15,6 +15,7 @@ use humhub\modules\marketplace\Module;
use humhub\services\MigrationService;
use Yii;
use yii\helpers\UnsetArrayValue;
use yii\helpers\Url;
/**
* SelfTest is a helper class which checks all dependencies of the application.
@ -430,16 +431,7 @@ class SelfTest
}
$title = Yii::t('AdminModule.information', 'Settings') . ' - ' . Yii::t('AdminModule.information', 'Base URL');
$scheme = Yii::$app->request->getIsSecureConnection() ? 'https' : 'http';
$port = Yii::$app->request->getServerPort();
$currentBaseUrl = $scheme . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST'])
. (
($scheme === 'https' && $port == 443) ||
($scheme === 'http' && $port == 80)
? ''
: ':' . $port
)
. ($_SERVER['BASE'] ?? '');
$currentBaseUrl = Url::base(true);
if ($currentBaseUrl === Yii::$app->settings->get('baseUrl')) {
$checks[] = [
'title' => $title,

View File

@ -132,7 +132,7 @@ class GroupController extends Controller
$group = Group::findOne(['id' => Yii::$app->request->get('id')]);
$this->checkGroupAccess($group);
$searchModel = new UserSearch();
$searchModel = Yii::createObject(UserSearch::class);
$searchModel->query = $group->getUsers();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('members', [

View File

@ -83,7 +83,7 @@ class UserController extends Controller
*/
public function actionList()
{
$searchModel = new UserSearch();
$searchModel = Yii::createObject(UserSearch::class);
$searchModel->status = User::STATUS_ENABLED;
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$showPendingRegistrations = Invite::find()->where(Invite::filterSource())->exists() &&
@ -261,10 +261,8 @@ class UserController extends Controller
public function actionAdd()
{
$registration = new Registration();
$registration->enableEmailField = true;
$registration = new Registration(enableEmailField: true, enableMustChangePassword: true);
$registration->enableUserApproval = false;
$registration->enableMustChangePassword = true;
if ($registration->submitted('save') && $registration->validate() && $registration->register()) {
return $this->redirect(['edit', 'id' => $registration->getUser()->id]);
@ -390,7 +388,7 @@ class UserController extends Controller
*/
public function actionExport($format)
{
$searchModel = new UserSearch();
$searchModel = Yii::createObject(UserSearch::class);
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$exporter = new SpreadsheetExport([

View File

@ -24,82 +24,35 @@ $this->registerJsConfig('admin.space', [
$userModule = Yii::$app->getModule('user');
$this->beginContent('@admin/views/authentication/_authenticationLayout.php');
?>
echo Html::beginTag('div', ['class' => 'panel-body']);
<div class="panel-body">
<?php $form = ActiveForm::begin(['id' => 'authentication-settings-form', 'acknowledge' => true]) ?>
<?= $form->errorSummary($model) ?>
$form = ActiveForm::begin(['id' => 'authentication-settings-form', 'acknowledge' => true]);
<?= $form->field($model, 'allowGuestAccess')->checkbox() ?>
<?= $form->field($model, 'internalAllowAnonymousRegistration')->checkbox() ?>
<?= $form->field($model, 'internalUsersCanInviteByEmail')->checkbox() ?>
<?= $form->field($model, 'internalUsersCanInviteByLink')->checkbox() ?>
<?= $form->field($model, 'internalRequireApprovalAfterRegistration')->checkbox() ?>
<?= $form->field($model, 'showRegistrationUserGroup')->checkbox() ?>
<?= $form->field($model, 'blockUsers')->checkbox() ?>
<?= $form->field($model, 'hideOnlineStatus')->checkbox() ?>
<?= $form->field($model, 'allowUserTopics')->checkbox(['data' => ['action-change' => 'admin.space.restrictTopicCreation']]) ?>
<?= $form->field($model, 'defaultUserIdleTimeoutSec')->textInput(['readonly' => $userModule->settings->isFixed('auth.defaultUserIdleTimeoutSec')]) ?>
<p class="help-block"><?= Yii::t('AdminModule.user', 'The default user idle timeout is used when user session is idle for a certain time. The user is automatically logged out after this time.') ?></p>
<?= $form->field($model, 'defaultUserProfileVisibility')->dropDownList(User::getVisibilityOptions(false), ['readonly' => (!Yii::$app->getModule('user')->settings->get('auth.allowGuestAccess'))]) ?>
<p class="help-block"><?= Yii::t('AdminModule.user', 'Only applicable when limited access for non-authenticated users is enabled. Only affects new users.') ?></p>
echo $form->errorSummary($model);
<?php if (Yii::$app->getModule('user')->settings->get('auth.needApproval')) : ?>
<?= $form->field($model, 'registrationSendMessageMailContent')->widget(RichTextField::class, ['exclude' => ['oembed', 'upload']]) ?>
<?= $form->field($model, 'registrationApprovalMailContent')->widget(RichTextField::class, ['exclude' => ['oembed', 'upload']]) ?>
<?= $form->field($model, 'registrationDenialMailContent')->widget(RichTextField::class, ['exclude' => ['oembed', 'upload']]) ?>
<p class="help-block"><?= Yii::t('AdminModule.user', 'Do not change placeholders like {displayName} if you want them to be automatically filled by the system. To reset the email content fields with the system default, leave them empty.') ?></p>
<?php endif; ?>
echo $form->field($model, 'allowGuestAccess')->checkbox();
echo $form->field($model, 'internalAllowAnonymousRegistration')->checkbox();
echo $form->field($model, 'internalUsersCanInviteByEmail')->checkbox();
echo $form->field($model, 'internalUsersCanInviteByLink')->checkbox();
echo $form->field($model, 'internalRequireApprovalAfterRegistration')->checkbox();
echo $form->field($model, 'showRegistrationUserGroup')->checkbox();
echo $form->field($model, 'blockUsers')->checkbox();
echo $form->field($model, 'hideOnlineStatus')->checkbox();
echo $form->field($model, 'allowUserTopics')->checkbox(['data' => ['action-change' => 'admin.space.restrictTopicCreation']]);
echo $form->field($model, 'defaultUserIdleTimeoutSec')
->textInput(['readonly' => $userModule->settings->isFixed('auth.defaultUserIdleTimeoutSec')]);
echo Html::tag(
'p',
Yii::t(
'AdminModule.user',
'The default user idle timeout is used when user session is idle for a certain time. The user is automatically logged out after this time.'
),
['class' => 'help-block']
);
$form->field($model, 'defaultUserProfileVisibility')
->dropDownList(
User::getVisibilityOptions(false),
['readonly' => (!Yii::$app->getModule('user')->settings->get('auth.allowGuestAccess'))]
);
echo Html::tag(
'p',
Yii::t(
'AdminModule.user',
'Only applicable when limited access for non-authenticated users is enabled. Only affects new users.'
),
['class' => 'help-block']
);
if (Yii::$app->getModule('user')->settings->get('auth.needApproval')) {
echo $form->field($model, 'registrationSendMessageMailContent')->widget(
RichTextField::class,
['exclude' => ['oembed', 'upload']]
);
echo $form->field($model, 'registrationApprovalMailContent')->widget(
RichTextField::class,
['exclude' => ['oembed', 'upload']]
);
echo $form->field($model, 'registrationDenialMailContent')->widget(
RichTextField::class,
['exclude' => ['oembed', 'upload']]
);
echo Html::tag(
'p',
Yii::t(
'AdminModule.user',
'Do not change placeholders like {displayName} if you want them to be automatically filled by the system. To reset the email content fields with the system default, leave them empty.',
),
['class' => 'help-block']
);
}
echo Html::tag('hr');
echo Html::submitButton(
Yii::t('AdminModule.user', 'Save'),
['class' => 'btn btn-primary', 'data-ui-loader' => ""]
);
ActiveForm::end();
echo Html::endTag('div');
$this->endContent();
<hr/>
<?= Html::submitButton(Yii::t('AdminModule.user', 'Save'), ['class' => 'btn btn-primary', 'data-ui-loader' => '']) ?>
<?php ActiveForm::end() ?>
</div>
<?php $this->endContent() ?>

View File

@ -10,7 +10,7 @@ use humhub\widgets\Button;
?>
<div class="panel-body">
<div class="pull-right">
<?= Button::back(['index'], Yii::t('AdminModule.base', 'Back to overview')) ?>
<?= Button::back(['/admin/group/index'], Yii::t('AdminModule.base', 'Back to overview')) ?>
</div>
<?php if (!$group->isNewRecord) : ?>

View File

@ -302,13 +302,13 @@ class Comment extends ContentAddonActiveRecord
->limit($pageSize);
if ($type === ShowMore::TYPE_NEXT) {
$query->orderBy(['created_at' => SORT_ASC]);
$query->orderBy(['created_at' => SORT_ASC, 'id' => SORT_ASC]);
if ($commentId) {
$query->andWhere(['>', 'id', $commentId]);
}
$comments = $query->all();
} else {
$query->orderBy(['created_at' => SORT_DESC]);
$query->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC]);
if ($commentId) {
$query->andWhere(['<', 'id', $commentId]);
}

View File

@ -308,6 +308,10 @@ humhub.module('comment', function (module, require, $) {
if (!visible && !window.comments_collapsed) {
target.find('.humhub-ui-richtext').trigger('focus');
}
if (!visible && window.comments_collapsed && !target.find('.comment>.media').length) {
target.find('[data-action-click="comment.showMore"]').trigger('click');
}
}
var toggleCommentHandler = function (evt) {

View File

@ -65,9 +65,7 @@ class Comments extends Widget
{
$objectModel = PolymorphicRelation::getObjectModel($this->object);
$objectId = $this->object->getPrimaryKey();
$streamQuery = Yii::$app->request->getQueryParam('StreamQuery');
$currentCommentId = empty($streamQuery['commentId']) ? null : $streamQuery['commentId'];
$currentCommentId = $this->getCurrentCommentId();
// Count all Comments
$commentCount = CommentModel::GetCommentCount($objectModel, $objectId);
@ -101,4 +99,27 @@ class Comments extends Widget
{
return $this->isFullViewMode() ? $this->module->commentsBlockLoadSizeViewMode : $this->module->commentsBlockLoadSize;
}
protected function getCurrentCommentId(): ?int
{
$streamQuery = Yii::$app->request->getQueryParam('StreamQuery');
if (empty($streamQuery['commentId'])) {
return null;
}
$currentCommentId = (int) $streamQuery['commentId'];
$currentComment = Yii::$app->runtimeCache->getOrSet('getCurrentComment' . $currentCommentId, function () use ($currentCommentId) {
return CommentModel::findOne(['id' => $currentCommentId]);
});
if (!$currentComment ||
$currentComment->object_id !== $this->object?->id ||
$currentComment->object_model !== get_class($this->object)) {
// The current comment is from another parent object
return null;
}
return $currentCommentId;
}
}

View File

@ -1,13 +1,13 @@
<?php
use humhub\commands\CronController;
use humhub\modules\content\Events;
use humhub\commands\IntegrityController;
use humhub\modules\content\Events;
use humhub\modules\content\models\Content;
use humhub\modules\content\Module;
use humhub\modules\content\widgets\WallEntryAddons;
use humhub\modules\user\models\User;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\User;
return [
'id' => 'content',

View File

@ -24,6 +24,7 @@ use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\db\IntegrityException;
use yii\web\ForbiddenHttpException;
use yii\web\HttpException;
use yii\web\NotFoundHttpException;
use yii\web\Response;
@ -421,4 +422,16 @@ class ContentController extends Controller
'disableInputs' => $disableInputs,
]);
}
/**
* Triggered after content creation
*/
public function actionRedirectToContentContainer($contentId)
{
$content = Content::findOne(['id' => $contentId]);
if ($content === null) {
throw new HttpException(404, 'Content not found!');
}
return $this->redirect($content->container->getUrl());
}
}

View File

@ -0,0 +1,103 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2025 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\controllers;
use humhub\components\behaviors\AccessControl;
use humhub\components\Controller;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\models\ContentContainer;
use humhub\modules\content\models\forms\ShareIntendTargetForm;
use humhub\modules\space\helpers\CreateContentPermissionHelper;
use humhub\modules\user\widgets\UserPicker;
use Yii;
use yii\web\HttpException;
/**
* Allows sharing files from the mobile app
*
* @since 1.17.2
*/
abstract class ShareIntendController extends Controller
{
abstract public function actionCreate();
abstract protected function getCreatePermissionClass(): string;
private const SESSION_KEY_TARGET_GUID = 'shareIntendTargetGuid';
public ?ContentContainerActiveRecord $shareTarget = null;
public function behaviors()
{
return [
'acl' => [
'class' => AccessControl::class,
],
];
}
public function beforeAction($action)
{
if (!parent::beforeAction($action)) {
return false;
}
\humhub\modules\file\controllers\ShareIntendController::checkShareFileGuids();
if (!in_array($action->id, ['index', 'container-search-json'])) {
$this->initShareTarget();
}
return true;
}
private function initShareTarget(): void
{
$shareTargetGuid = Yii::$app->session->get(self::SESSION_KEY_TARGET_GUID);
$this->shareTarget = ContentContainer::findRecord($shareTargetGuid);
if ($this->shareTarget === null) {
throw new HttpException('500', 'No target to share found!');
}
}
public function actionIndex()
{
Yii::$app->session->remove(self::SESSION_KEY_TARGET_GUID);
$model = new ShareIntendTargetForm();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
Yii::$app->session->set(self::SESSION_KEY_TARGET_GUID, $model->targetContainerGuid);
$this->initShareTarget();
return $this->actionCreate();
}
return $this->renderAjax('@content/views/share-intend/index', [
'model' => $model,
'fileList' => \humhub\modules\file\controllers\ShareIntendController::getShareFileGuids(),
]);
}
public function actionContainerSearchJson()
{
$containers = CreateContentPermissionHelper::findSpaces(
$this->getCreatePermissionClass(),
Yii::$app->request->get('keyword'),
Yii::$app->user->identity,
);
if (ShareIntendTargetForm::canPostInOwnProfile()) {
$currentUser = UserPicker::createJSONUserInfo(Yii::$app->user->identity);
$currentUser['text'] = Yii::t('base', 'My Profile');
array_unshift($containers, $currentUser);
}
return $this->asJson($containers);
}
}

View File

@ -212,7 +212,7 @@ class Content extends ActiveRecord implements Movable, ContentOwner, Archiveable
/**
* @since 1.3
*/
public function getModel(): ContentActiveRecord
public function getModel(): ?ContentActiveRecord
{
return $this->getPolymorphicRelation();
}
@ -915,11 +915,20 @@ class Content extends ActiveRecord implements Movable, ContentOwner, Archiveable
$user = User::findOne(['id' => $user]);
}
// Only owner can edit his content
// Owner can edit his content
if ($user !== null && $this->created_by === $user->id) {
return true;
}
// System Admins can edit User content for moderation purposes
if (
$this->id // The content exists
&& $this->container instanceof User
&& $user->isSystemAdmin()
) {
return true;
}
if ($user?->canManageAllContent()) {
return true;
}

View File

@ -0,0 +1,48 @@
<?php
namespace humhub\modules\content\models\forms;
use humhub\modules\post\models\Post;
use humhub\modules\user\Module;
use Yii;
use yii\base\Model;
use yii\helpers\Url;
class ShareIntendTargetForm extends Model
{
public $targetContainerGuid;
/**
* @inheritdoc
*/
public function rules(): array
{
return [
[['targetContainerGuid'], 'required'],
];
}
public function attributeHints(): array
{
return [
'targetContainerGuid' => static::canPostInOwnProfile() ?
Yii::t('ContentModule.base', 'Select target Space/Profile.') :
Yii::t('ContentModule.base', 'Select target Space.'),
];
}
public function getContainerSearchUrl(): string
{
return Url::to(['container-search-json']);
}
public static function canPostInOwnProfile(): bool
{
/** @var Module $userModule */
$userModule = Yii::$app->getModule('user');
return
!$userModule->profileDisableStream // The profile stream is enabled
&& !Yii::$app->user->isGuest
&& (new Post(Yii::$app->user->identity))->content->canEdit(); // Can post in own profile
}
}

View File

@ -5,6 +5,7 @@
humhub.module('content.form', function (module, require, $) {
var CREATE_FORM_ROOT_SELECTOR = '#contentFormBody';
var CREATE_FORM_ROOT_SELECTOR_MODAL = CREATE_FORM_ROOT_SELECTOR + 'Modal';
var object = require('util').object;
var client = require('client');
@ -22,26 +23,32 @@ humhub.module('content.form', function (module, require, $) {
object.inherits(CreateForm, Widget);
CreateForm.prototype.init = function () {
this.$.hide();
this.menu = this.$.parent().prev('#contentFormMenu');
// Hide options by default
$('.contentForm_options').hide();
this.isModal = this.$.is(CREATE_FORM_ROOT_SELECTOR_MODAL)
if (!this.isModal) {
this.$.hide();
this.menu = this.$.parent().prev('#contentFormMenu'); // #contentFormMenuModal doesn't exist
// Hide options by default
this.$.find('.contentForm_options').hide();
}
this.setDefaultVisibility();
this.$.fadeIn('fast');
this.showMenu();
if (!module.config['disabled']) {
$('#contentFormBody').on('click.humhub:content:form dragover.humhub:content:form', function (evt) {
this.$.on('click.humhub:content:form dragover.humhub:content:form', function (evt) {
// Prevent fading in for topic remove button clicks
if ($(evt.target).closest('.topic-remove-label').length) {
return;
}
$('.contentForm_options').fadeIn();
});
if (!this.isModal) {
this.$.find('.contentForm_options').fadeIn();
}
}.bind(this));
} else {
$('#contentFormBody').find('.humhub-ui-richtext').trigger('disable');
this.$.find('.humhub-ui-richtext').trigger('disable');
}
};
@ -65,11 +72,19 @@ humhub.module('content.form', function (module, require, $) {
event.trigger('humhub:content:beforeSubmit', this);
client.submit(evt).then(function (response) {
that.$.find(".preferences, .fileinput-button").show();
$('.contentForm_options .preferences, .fileinput-button').show();
that.$.find('.contentForm_options .preferences, .fileinput-button').show();
if (!response.errors) {
event.trigger('humhub:content:newEntry', response.output, this);
event.trigger('humhub:content:afterSubmit', response.output, this);
that.resetForm();
if ($('#share-intend-modal').length) {
$("#globalModal").modal("hide");
// If the dashboard stream is not displayed on the current page, redirect to the content container, to make sure the user sees the new content
if (response.url && !$('.dashboard-wall-stream').length) {
client.pjax.redirect(module.config.redirectToContentContainerUrl.replace('the-content-id', response.data.id));
}
} else {
that.resetForm();
}
} else {
that.handleError(response);
}
@ -82,12 +97,15 @@ humhub.module('content.form', function (module, require, $) {
/**
* Todo: this is post form only, this needs to be added to post module perhaps by calling $form.trigger('humhub:form:clear');
*
* As the form for share intend is in a modal, we don't need to reset it
*
* @returns {undefined}
*/
CreateForm.prototype.resetForm = function () {
// Reset Form (Empty State)
$('.contentForm_options').hide();
var $contentForm = $('.contentForm');
this.$.find('.contentForm_options').hide();
var $contentForm = this.$.find('.contentForm');
$contentForm.filter(':text').val('');
$contentForm.filter('textarea').val('').trigger('autosize.resize');
$contentForm.attr('checked', false);
@ -98,30 +116,29 @@ humhub.module('content.form', function (module, require, $) {
this.resetFileUpload();
this.resetState();
$('#public').attr('checked', false);
$('#contentFormBody').find('.humhub-ui-richtext').trigger('clear');
this.$.find('.humhub-ui-richtext').trigger('clear');
};
CreateForm.prototype.resetSettingInputs = function () {
$('#notifyUserContainer').hide();
Widget.instance('#notifyUserInput').reset();
$('#postTopicContainer').hide();
this.$.find('.notifyUserContainer').hide();
Widget.instance('#notifyUserInput' + (this.isModal ? 'Modal' : '')).reset();
$('#postTopicContainer' + (this.isModal ? 'Modal' : '')).hide();
var topicPicker = Widget.instance('#postTopicInput');
var topicPicker = Widget.instance('#postTopicInput' + (this.isModal ? 'Modal' : ''));
if (topicPicker) {
topicPicker.reset();
}
};
CreateForm.prototype.resetFilePreview = function () {
var preview = Widget.instance($('#contentFormFiles_preview'));
var preview = Widget.instance($('#contentFormFiles_preview' + (this.isModal ? 'Modal' : '')));
if (preview) {
preview.reset();
}
};
CreateForm.prototype.resetFileUpload = function () {
var upload = Widget.instance($('#contentForm_message-file-upload'));
var upload = Widget.instance($('#contentFormFiles_progress' + (this.isModal ? 'Modal' : '')));
if (upload) {
upload.reset();
}
@ -146,7 +163,7 @@ humhub.module('content.form', function (module, require, $) {
};
CreateForm.prototype.changeVisibility = function () {
if (!$('#contentForm_visibility').prop('checked')) {
if (!this.$.find('.contentForm_visibility').prop('checked')) {
this.setPublicVisibility();
} else {
this.setPrivateVisibility();
@ -162,26 +179,26 @@ humhub.module('content.form', function (module, require, $) {
};
CreateForm.prototype.setPublicVisibility = function () {
$('#contentForm_visibility').prop("checked", true);
$('#contentForm_visibility_entry').html('<i class="fa fa-lock"></i>' + module.text(['makePrivate']));
$('.label-public').removeClass('hidden');
this.$.find('.contentForm_visibility').prop("checked", true);
this.$.find('.contentForm_visibility_entry').html('<i class="fa fa-lock"></i>' + module.text(['makePrivate']));
this.$.find('.label-public').removeClass('hidden');
};
CreateForm.prototype.setPrivateVisibility = function () {
$('#contentForm_visibility').prop("checked", false);
$('#contentForm_visibility_entry').html('<i class="fa fa-unlock"></i>' + module.text(['makePublic']));
$('.label-public').addClass('hidden');
this.$.find('.contentForm_visibility').prop("checked", false);
this.$.find('.contentForm_visibility_entry').html('<i class="fa fa-unlock"></i>' + module.text(['makePublic']));
this.$.find('.label-public').addClass('hidden');
};
CreateForm.prototype.notifyUser = function () {
$('#notifyUserContainer').show();
Widget.instance('#notifyUserInput').focus();
this.$.find('.notifyUserContainer').show();
Widget.instance('#notifyUserInput' + (this.isModal ? 'Modal' : '')).focus();
};
CreateForm.prototype.setTopics = function () {
$('#postTopicContainer').show();
$('#postTopicContainer' + (this.isModal ? 'Modal' : '')).show();
var topicPicker = Widget.instance('#postTopicInput');
var topicPicker = Widget.instance('#postTopicInput' + (this.isModal ? 'Modal' : ''));
if (topicPicker) {
topicPicker.focus();
}
@ -190,7 +207,7 @@ humhub.module('content.form', function (module, require, $) {
CreateForm.prototype.changeState = function (state, title, buttonTitle) {
const stateInput = this.$.find('input[name=state]');
let stateLabel = this.$.find('.label-content-state');
const button = this.$.find('#post_submit_button');
const button = $('#post_submit_button' + (this.isModal ? '_modal' : ''));
if (!stateLabel.length) {
stateLabel = $('<span>').addClass('label label-warning label-content-state');
@ -217,27 +234,34 @@ humhub.module('content.form', function (module, require, $) {
stateLabel.show().html(title);
button.html(buttonTitle);
this.$.find('.preferences [data-action-click=notifyUser]').parent().hide();
this.$.find('#notifyUserContainer').hide();
this.$.find('.notifyUserContainer').hide();
}
CreateForm.prototype.resetState = function () {
const stateInput = this.$.find('input[name=state]');
const button = this.$.find('#post_submit_button');
const button = $('#post_submit_button' + (this.isModal ? '_modal' : ''));
const initial = stateInput.data('initial');
if (initial !== undefined) {
stateInput.val(initial.state);
button.data('htmlOld', initial.buttonTitle).removeAttr('style');
loader.reset(button);
if (loader.is(button)) {
button.data('htmlOld', initial.buttonTitle).removeAttr('style');
loader.reset(button);
} else {
button.html(initial.buttonTitle);
}
}
this.$.find('input[name^=scheduled]').remove();
this.$.find('.label-content-state').hide();
this.$.find('.preferences [data-action-click=notifyUser]').parent().show();
const notifyUserContainer = this.$.find('#notifyUserContainer');
const notifyUserContainer = this.$.find('.notifyUserContainer');
if (notifyUserContainer.find('ul .select2-selection__clear').length) {
notifyUserContainer.show();
}
}
/**
* Schedule is not available for share intend because it is already in a modal
*/
CreateForm.prototype.scheduleOptions = function (evt) {
const that = this;
const modalGlobal = modal.global.$;
@ -275,6 +299,9 @@ humhub.module('content.form', function (module, require, $) {
});
}
/**
* Schedule is not available for share intend because it is already in a modal
*/
CreateForm.prototype.setScheduleOption = function (name, value) {
let input = this.$.find('input[name=' + name + ']');
@ -290,10 +317,16 @@ humhub.module('content.form', function (module, require, $) {
input.val(value);
}
/**
* Schedule is not available for share intend because it is already in a modal
*/
CreateForm.prototype.resetScheduleOption = function (name) {
this.setScheduleOption(name);
}
/**
* CreateFormMenu is not available for share intend
*/
const CreateFormMenu = Widget.extend();
CreateFormMenu.prototype.init = function () {
@ -343,6 +376,13 @@ humhub.module('content.form', function (module, require, $) {
}
};
var initModal = function () {
var $rootModal = $(CREATE_FORM_ROOT_SELECTOR_MODAL);
if ($rootModal.length) {
instance = Widget.instance($rootModal);
}
};
var unload = function () {
instance = undefined;
}
@ -352,6 +392,7 @@ humhub.module('content.form', function (module, require, $) {
CreateFormMenu: CreateFormMenu,
instance: instance,
init: init,
initModal: initModal,
initOnPjaxLoad: true,
unload: unload
});

View File

@ -165,7 +165,7 @@ class MysqlDriver extends AbstractDriver
$term = preg_replace('/-+(\*?)$/', '$1', $term);
// Wrap a keyword in quotes to avoid error with the special chars in the sql MATCH-AGAINST expression
return preg_match('#[^\p{L}\d\*\'`\-\_]#', $term) ? '"' . $term . '"' : $term;
return preg_match('#[^\p{L}\d\*\'`\-\_]#u', $term) ? '"' . $term . '"' : $term;
}
protected function addQueryFilterVisibility(ActiveQuery $query): ActiveQuery

View File

@ -0,0 +1,44 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2025 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\modules\content\models\forms\ShareIntendTargetForm;
use humhub\modules\content\widgets\ContentContainerPickerField;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\modules\ui\view\components\View;
use humhub\widgets\ModalButton;
use humhub\widgets\ModalDialog;
/**
* @var $this View
* @var $model ShareIntendTargetForm
* @var $fileList string[]
*/
?>
<?php ModalDialog::begin([
'header' => Yii::t('ContentModule.base', 'Share'),
]) ?>
<?php $form = ActiveForm::begin() ?>
<div class="modal-body">
<?= $form->field($model, 'targetContainerGuid')->widget(ContentContainerPickerField::class, [
'maxSelection' => 1,
'minInput' => 0,
'focus' => true,
'url' => $model->getContainerSearchUrl(),
'options' => ['data-action-change' => 'ui.modal.submit'],
])->label(false) ?>
</div>
<div class="modal-footer">
<?= ModalButton::defaultType(Yii::t('base', 'Back'))
->load(['/file/share-intend', 'fileList' => $fileList]) ?>
</div>
<?php ActiveForm::end() ?>
<?php ModalDialog::end() ?>

View File

@ -0,0 +1,64 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2017 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*
*/
namespace humhub\modules\content\widgets;
use humhub\modules\content\models\ContentContainer;
use humhub\modules\ui\form\widgets\BasePicker;
/**
* Mutliselect input field for selecting Space guids or current user Profile
*
* This widget has no default route, so the `url` param is required.
*
* @since 1.17.2
*/
class ContentContainerPickerField extends BasePicker
{
/**
* @inheritdoc
* The 'guid' value is default for UserPickerField
*/
public $itemKey = 'guid';
/**
* @inheritdoc
*/
public $itemClass = ContentContainer::class;
/**
* @inheritdoc
*/
protected function getAttributes()
{
return array_merge(parent::getAttributes(), [
'data-tags' => 'false',
]);
}
/**
* @inheritdoc
*/
protected function getItemText($item)
{
return $this->itemClass === ContentContainer::class ?
$item->getPolymorphicRelation()->displayName :
$item->displayName;
}
/**
* @inheritdoc
*/
protected function getItemImage($item)
{
return $this->itemClass === ContentContainer::class ?
$item->getPolymorphicRelation()->getProfileImage()->getUrl() :
$item->getProfileImage()->getUrl();
}
}

View File

@ -105,7 +105,7 @@ class SearchFilters extends DirectoryFilters
'sortOrder' => 500,
]);*/
$this->addFilter('space', [
$this->addFilter('contentContainer', [
'title' => Yii::t('ContentModule.search', 'Space'),
'type' => 'widget',
'widget' => SpacePickerField::class,

View File

@ -38,6 +38,16 @@ abstract class WallCreateContentForm extends Widget
*/
public $contentContainer;
/**
* Pre-uploaded File GUIDs to be attached to the new content
*/
public array $fileList = [];
/**
* The widget is executed in a modal window
*/
public bool $isModal = false;
/**
* @inheritdoc
*/

View File

@ -8,10 +8,10 @@
namespace humhub\modules\content\widgets;
use humhub\libs\Sort;
use humhub\modules\content\widgets\stream\WallStreamEntryWidget;
use humhub\components\Widget;
use humhub\libs\Sort;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\widgets\stream\WallStreamEntryWidget;
use yii\web\HttpException;
/**

View File

@ -24,7 +24,12 @@ use yii\web\HttpException;
class WallCreateContentFormFooter extends Widget
{
/**
* @var string form submit route/url (required)
* @var WallCreateContentForm null (required)
*/
public ?WallCreateContentForm $wallCreateContentForm = null;
/**
* @var string form submit route/url (automatically set if `wallCreateContentForm` is provided)
*/
public $submitUrl;
@ -51,6 +56,10 @@ class WallCreateContentFormFooter extends Widget
throw new HttpException(500, 'No Content Container given!');
}
if ($this->wallCreateContentForm !== null) {
$this->submitUrl = $this->wallCreateContentForm->submitUrl;
}
parent::init();
}
@ -61,6 +70,8 @@ class WallCreateContentFormFooter extends Widget
{
return $this->render('@humhub/modules/content/widgets/views/wallCreateContentFormFooter', [
'contentContainer' => $this->contentContainer,
'fileList' => $this->wallCreateContentForm?->fileList ?? [],
'isModal' => $this->wallCreateContentForm?->isModal ?? false,
'submitUrl' => $this->contentContainer->createUrl($this->submitUrl),
'submitButtonText' => $this->submitButtonText,
'canSwitchVisibility' => $this->contentContainer->visibility !== Space::VISIBILITY_NONE && $this->contentContainer->can(CreatePublicContent::class),

View File

@ -1,11 +1,12 @@
<?php
use humhub\modules\content\assets\ContentFormAsset;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\widgets\WallCreateContentForm;
use humhub\modules\content\widgets\WallCreateContentMenu;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\modules\content\assets\ContentFormAsset;
use humhub\modules\space\models\Space;
use humhub\modules\ui\form\widgets\ActiveForm;
use yii\helpers\Url;
/* @var $wallCreateContentForm WallCreateContentForm */
/* @var $defaultVisibility int */
@ -19,14 +20,15 @@ $this->registerJsConfig('content.form', [
'text' => [
'makePrivate' => Yii::t('ContentModule.base', 'Change to "Private"'),
'makePublic' => Yii::t('ContentModule.base', 'Change to "Public"'),
'info.archived' => Yii::t('ContentModule.base', 'This space is archived.')
]
'info.archived' => Yii::t('ContentModule.base', 'This space is archived.'),
],
'redirectToContentContainerUrl' => Url::to(['/content/content/redirect-to-content-container', 'contentId' => 'the-content-id']),
]);
?>
<?php if (WallCreateContentMenu::canCreateEntry($contentContainer, 'form')) : ?>
<div class="panel panel-default clearfix">
<div class="panel-body" id="contentFormBody" style="display:none;"
<div class="panel-body" id="contentFormBody<?= $wallCreateContentForm->isModal ? 'Modal' : '' ?>" class="content-form-body" style="display:none;"
data-action-component="content.form.CreateForm">
<?php $form = ActiveForm::begin(['acknowledge' => true]); ?>

View File

@ -5,9 +5,9 @@
* @license https://www.humhub.com/licences
*/
use humhub\modules\content\assets\ContentFormAsset;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\widgets\WallCreateContentMenu;
use humhub\modules\content\assets\ContentFormAsset;
/* @var $contentContainer ContentContainerActiveRecord */
/* @var $formClass string */
@ -19,4 +19,4 @@ ContentFormAsset::register($this);
<?php if ($formClass) : ?>
<?= $formClass::widget(['contentContainer' => $contentContainer]) ?>
<?php endif; ?>
<?php endif; ?>

View File

@ -5,6 +5,7 @@
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\file\handler\BaseFileHandler;
@ -17,20 +18,21 @@ use humhub\modules\ui\icon\widgets\Icon;
use humhub\modules\user\widgets\UserPickerField;
use humhub\widgets\Button;
use humhub\widgets\Link;
use yii\helpers\Html;
/* @var $submitUrl string */
/* @var $submitButtonText string */
/* @var $fileHandlers BaseFileHandler[] */
/* @var $canSwitchVisibility bool */
/* @var $contentContainer ContentContainerActiveRecord */
/* @var $fileList array */
/* @var $isModal bool */
/* @var $pickerUrl string */
/* @var $scheduleUrl string */
?>
<div id="notifyUserContainer" class="form-group" style="margin-top:15px;display:none">
<div class="notifyUserContainer form-group" style="margin-top:15px;display:none">
<?= UserPickerField::widget([
'id' => 'notifyUserInput',
'id' => 'notifyUserInput' . ($isModal ? 'Modal' : ''),
'url' => $pickerUrl,
'formName' => 'notifyUserInput',
'maxSelection' => 10,
@ -39,34 +41,35 @@ use yii\helpers\Html;
]) ?>
</div>
<div id="postTopicContainer" class="form-group" style="margin-top:15px;display:none">
<div id="postTopicContainer<?= $isModal ? 'Modal' : '' ?>" class="form-group" style="margin-top:15px;display:none">
<?= TopicPicker::widget([
'id' => 'postTopicInput',
'id' => 'postTopicInput' . ($isModal ? 'Modal' : ''),
'name' => 'postTopicInput',
'contentContainer' => $contentContainer
]); ?>
'contentContainer' => $contentContainer,
]) ?>
</div>
<?= Html::hiddenInput('containerGuid', $contentContainer->guid); ?>
<?= Html::hiddenInput('containerClass', get_class($contentContainer)); ?>
<?= Html::hiddenInput('containerGuid', $contentContainer->guid) ?>
<?= Html::hiddenInput('containerClass', get_class($contentContainer)) ?>
<div class="contentForm_options">
<hr>
<div class="btn_container">
<?= Button::info($submitButtonText)->action('submit', $submitUrl)->id('post_submit_button')->submit() ?>
<?= Button::info($submitButtonText)->action('submit', $submitUrl)->id('post_submit_button' . ($isModal ? '_modal' : ''))->submit() ?>
<?php $uploadButton = UploadButton::widget([
'id' => 'contentFormFiles',
'id' => 'contentFormFiles' . ($isModal ? 'Modal' : ''),
'tooltip' => Yii::t('ContentModule.base', 'Attach Files'),
'progress' => '#contentFormFiles_progress',
'preview' => '#contentFormFiles_preview',
'dropZone' => '#contentFormBody',
'max' => Yii::$app->getModule('content')->maxAttachedFiles
'progress' => '#contentFormFiles_progress' . ($isModal ? 'Modal' : ''),
'preview' => '#contentFormFiles_preview' . ($isModal ? 'Modal' : ''),
'dropZone' => '#contentFormBody' . ($isModal ? 'Modal' : ''),
'max' => Yii::$app->getModule('content')->maxAttachedFiles,
'fileList' => $fileList,
]); ?>
<?= FileHandlerButtonDropdown::widget(['primaryButton' => $uploadButton, 'handlers' => $fileHandlers, 'cssButtonClass' => 'btn-default']); ?>
<!-- public checkbox -->
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true']); ?>
<?= Html::checkbox('visibility', '', ['class' => 'contentForm_visibility contentForm hidden', 'aria-hidden' => 'true']); ?>
<!-- state data -->
<?= Html::hiddenInput('state', Content::STATE_PUBLISHED) ?>
@ -95,7 +98,7 @@ use yii\helpers\Html;
<?php if ($canSwitchVisibility): ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Change to "Public"'), 'changeVisibility')
->id('contentForm_visibility_entry')->icon('unlock') ?>
->cssClass('contentForm_visibility_entry')->icon('unlock') ?>
</li>
<?php endif; ?>
<li>
@ -104,19 +107,28 @@ use yii\helpers\Html;
->options([
'data-state' => Content::STATE_DRAFT,
'data-state-title' => Yii::t('ContentModule.base', 'Draft'),
'data-button-title' => Yii::t('ContentModule.base', 'Save as draft')
'data-button-title' => Yii::t('ContentModule.base', 'Save as draft'),
]) ?>
</li>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Schedule publication'), 'scheduleOptions', $scheduleUrl)
->icon('clock-o') ?>
</li>
<?php if (!$isModal): ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Schedule publication'), 'scheduleOptions', $scheduleUrl)
->icon('clock-o') ?>
</li>
<?php endif; ?>
</ul>
</li>
</ul>
</div>
</div>
<?= UploadProgress::widget(['id' => 'contentFormFiles_progress']) ?>
<?= FilePreview::widget(['id' => 'contentFormFiles_preview', 'edit' => true, 'options' => ['style' => 'margin-top:10px;']]); ?>
<?= UploadProgress::widget([
'id' => 'contentFormFiles_progress' . ($isModal ? 'Modal' : ''),
]) ?>
<?= FilePreview::widget([
'id' => 'contentFormFiles_preview' . ($isModal ? 'Modal' : ''),
'edit' => true,
'items' => $fileList,
'options' => ['style' => 'margin-top:10px;'],
]) ?>
</div><!-- /contentForm_Options -->

View File

@ -2,11 +2,11 @@
namespace humhub\modules\dashboard\widgets;
use humhub\modules\post\widgets\Form;
use Yii;
use humhub\modules\stream\widgets\StreamViewer;
use humhub\components\Widget;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\post\widgets\Form;
use humhub\modules\stream\widgets\StreamViewer;
use Yii;
class DashboardContent extends Widget
{
@ -33,6 +33,7 @@ class DashboardContent extends Widget
}
echo StreamViewer::widget([
'options' => ['class' => 'dashboard-wall-stream'],
'streamAction' => '//dashboard/dashboard/stream',
'showFilters' => (bool)Yii::$app->getModule('dashboard')->settings->get('showProfilePostForm'),
'messageStreamEmpty' => $messageStreamEmpty,

View File

@ -0,0 +1,96 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2025 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\file\controllers;
use humhub\components\behaviors\AccessControl;
use humhub\components\Controller;
use humhub\modules\file\models\File;
use Yii;
use yii\web\HttpException;
use yii\web\NotFoundHttpException;
/**
* Modules can be added as an additional target by
* registering an event on `ShareIntendController::EVENT_INIT`
* and adding the following method to the Events class:
*
* ```
* public static function onShareIntendControllerInit($event)
* {
* $event->sender->shareTargets[] = [
* 'title' => 'Your Module',
* 'route' => '/your-module/share-intend/index',
* ];
* }
* ```
*
* The module must have the ShareIntendController and view,
* similar to the Post module.
* The controller must extend \humhub\modules\content\controllers\ShareIntendController
*/
final class ShareIntendController extends Controller
{
public $shareTargets = [];
public function behaviors()
{
return [
'acl' => [
'class' => AccessControl::class,
],
];
}
public function init()
{
$this->shareTargets[] = [
'title' => Yii::t('FileModule.base', 'Share as Post'),
'route' => '/post/share-intend',
];
parent::init();
}
public function actionIndex()
{
$fileList = Yii::$app->request->get('fileList');
if (!$fileList) {
throw new NotFoundHttpException('No files to share found!');
}
// Check if the files exists, and if user is the owner of the files
if (File::find()->where(['guid' => $fileList])->andWhere(['created_by' => Yii::$app->user->id])->count() !== count($fileList)) {
throw new NotFoundHttpException('Files not uploaded correctly!');
}
if (count($this->shareTargets) === 0) {
throw new NotFoundHttpException('No sharing targets found!');
}
Yii::$app->session->set('shareIntendFiles', $fileList);
return $this->renderAjax('index', [
'shareTargets' => $this->shareTargets,
]);
}
public static function checkShareFileGuids(): void
{
$fileGuids = Yii::$app->session->get('shareIntendFiles');
if (empty($fileGuids)) {
throw new HttpException('500', 'No files to share found!');
}
}
public static function getShareFileGuids(): ?array
{
return Yii::$app->session->get('shareIntendFiles');
}
}

View File

@ -57,7 +57,11 @@ class FileHandlerCollection extends Component
{
parent::init();
$this->trigger(self::EVENT_INIT);
try {
$this->trigger(self::EVENT_INIT);
} catch (\Exception $ex) {
Yii::error('Could not init file handler. Error: ' . $ex->getMessage(), 'file');
}
// Register default handlers
if ($this->type === self::TYPE_CREATE) {

View File

@ -17,8 +17,8 @@ return array (
'Invalid Mime-Type' => 'Неправильный MIME-тип файла',
'Last update by:' => 'Последнее обновление:',
'Size:' => 'Размер:',
'Sorry, you can only upload up to {n,plural,=1{# file} other{# files}} at once.' => 'К сожалению, вы можете загрузить только до {n, plural, =1{# файла} many{# файлов}} одновременно.',
'Sorry, you can only upload up to {n,plural,=1{# file} other{# files}} at once.' => 'К сожалению, вы можете загрузить только до {n,plural,=1{# файла} other{# файлов}} одновременно.',
'The uploaded image is not a squared.' => 'Загруженное изображение не квадратное.',
'This upload field only allows a maximum of {n,plural,=1{# file} other{# files}}.' => 'Это поле загрузки допускает максимум {n,plural,=1{# файл} few{# файла} many{# файлов}}.',
'This upload field only allows a maximum of {n,plural,=1{# file} other{# files}}.' => 'Это поле загрузки допускает максимум {n,plural,=1{# файл} other{# файлов}}.',
'Upload files' => 'Загрузить файлы',
);

View File

@ -0,0 +1,32 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2025 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\modules\ui\view\components\View;
use humhub\widgets\ModalButton;
use humhub\widgets\ModalDialog;
use yii\helpers\Url;
/**
* @var $this View
* @var $shareTargets array
*/
?>
<?php ModalDialog::begin(['header' => Yii::t('FileModule.base', 'Share')]) ?>
<div class="modal-body">
<?php foreach ($shareTargets as $target): ?>
<a class="btn btn-primary btn-block" data-action-click="ui.modal.load"
data-action-url="<?= Url::to([$target['route']]) ?>">
<?= $target['title'] ?>
</a>
<?php endforeach; ?>
</div>
<div class="modal-footer">
<?= ModalButton::cancel() ?>
</div>
<?php ModalDialog::end() ?>

View File

@ -159,6 +159,11 @@ class UploadInput extends JsWidget
*/
public $attach = true;
/**
* Pre-uploaded File GUIDs to be attached to the new content
*/
public array $fileList = [];
public function init()
{
parent::init();
@ -183,6 +188,9 @@ class UploadInput extends JsWidget
$result .= Html::hiddenInput($this->submitName, $file->guid);
}
}
foreach ((array)$this->fileList as $file) {
$result .= Html::hiddenInput($this->submitName, $file);
}
return $result;
}

View File

@ -33,6 +33,7 @@ use humhub\modules\user\models\Password;
use humhub\modules\user\models\User;
use Yii;
use yii\base\InvalidConfigException;
use yii\caching\DummyCache;
use yii\web\HttpException;
/**
@ -75,10 +76,6 @@ class ConfigController extends Controller
public function beforeAction($action)
{
if (parent::beforeAction($action)) {
// Flush Caches
Yii::$app->cache->flush();
// Database Connection seems not to work
if (!$this->module->checkDBConnection()) {
$this->redirect(['/installer/setup']);
@ -104,7 +101,6 @@ class ConfigController extends Controller
*/
public function actionIndex()
{
if (Yii::$app->settings->get('name') == "") {
Yii::$app->settings->set('name', "HumHub");
}
@ -204,11 +200,23 @@ class ConfigController extends Controller
}
if ($form->load(Yii::$app->request->post()) && $form->validate()) {
Yii::$app->getModule('user')->settings->set('auth.needApproval', $form->internalRequireApprovalAfterRegistration);
Yii::$app->getModule('user')->settings->set('auth.anonymousRegistration', $form->internalAllowAnonymousRegistration);
Yii::$app->getModule('user')->settings->set(
'auth.needApproval',
$form->internalRequireApprovalAfterRegistration,
);
Yii::$app->getModule('user')->settings->set(
'auth.anonymousRegistration',
$form->internalAllowAnonymousRegistration,
);
Yii::$app->getModule('user')->settings->set('auth.allowGuestAccess', $form->allowGuestAccess);
Yii::$app->getModule('user')->settings->set('auth.internalUsersCanInviteByEmail', $form->canInviteExternalUsersByEmail);
Yii::$app->getModule('user')->settings->set('auth.internalUsersCanInviteByLink', $form->canInviteExternalUsersByLink);
Yii::$app->getModule('user')->settings->set(
'auth.internalUsersCanInviteByEmail',
$form->canInviteExternalUsersByEmail,
);
Yii::$app->getModule('user')->settings->set(
'auth.internalUsersCanInviteByLink',
$form->canInviteExternalUsersByLink,
);
Yii::$app->getModule('friendship')->settings->set('enable', $form->enableFriendshipModule);
return $this->redirect(Yii::$app->getModule('installer')->getNextConfigStepUrl());
}
@ -230,7 +238,10 @@ class ConfigController extends Controller
$modules = $marketplaceModule->onlineModuleManager->getModules(false);
foreach ($modules as $i => $module) {
if (!isset($module['useCases']) || strpos($module['useCases'], Yii::$app->settings->get('useCase')) === false) {
if (!isset($module['useCases']) || strpos(
$module['useCases'],
Yii::$app->settings->get('useCase'),
) === false) {
unset($modules[$i]);
}
}
@ -279,7 +290,6 @@ class ConfigController extends Controller
Yii::$app->getModule('installer')->settings->set('sampleData', $form->sampleData);
if (Yii::$app->getModule('installer')->settings->get('sampleData') == 1) {
// Add sample image to admin
$admin = User::find()->where(['id' => 1])->one();
$adminImage = new ProfileImage($admin->guid);
@ -364,7 +374,10 @@ class ConfigController extends Controller
// Create a sample post
$post = new Post();
$post->message = Yii::t("InstallerModule.base", "We're looking for great slogans of famous brands. Maybe you can come up with some samples?");
$post->message = Yii::t(
"InstallerModule.base",
"We're looking for great slogans of famous brands. Maybe you can come up with some samples?",
);
$post->content->container = $space;
$post->content->visibility = Content::VISIBILITY_PRIVATE;
$post->save();
@ -382,7 +395,10 @@ class ConfigController extends Controller
Yii::$app->user->switchIdentity($userModel2);
$comment2 = new Comment();
$comment2->message = Yii::t("InstallerModule.base", "Calvin Klein Between love and madness lies obsession.");
$comment2->message = Yii::t(
"InstallerModule.base",
"Calvin Klein Between love and madness lies obsession.",
);
$comment2->object_model = Post::class;
$comment2->object_id = $post->getPrimaryKey();
$comment2->save();
@ -416,7 +432,6 @@ class ConfigController extends Controller
*/
public function actionAdmin()
{
// Admin account already created
if (User::find()->count() > 0) {
return $this->redirect(Yii::$app->getModule('installer')->getNextConfigStepUrl());
@ -486,7 +501,6 @@ class ConfigController extends Controller
$form->models['Profile'] = $profileModel;
if ($form->submitted('save') && $form->validate()) {
$form->models['User']->status = User::STATUS_ENABLED;
$form->models['User']->language = '';
$form->models['User']->tagsField = ['Administration', 'Support', 'HumHub'];

View File

@ -21,7 +21,7 @@ use humhub\modules\user\models\ProfileField;
use humhub\modules\user\models\ProfileFieldCategory;
use Yii;
use yii\base\Exception;
use yii\helpers\BaseUrl;
use yii\helpers\Url;
/**
* InitialData
@ -37,7 +37,7 @@ class InitialData
return;
}
Yii::$app->settings->set('baseUrl', BaseUrl::base(true));
Yii::$app->settings->set('baseUrl', Url::base(true));
Yii::$app->settings->set('paginationSize', 10);
Yii::$app->settings->set('displayNameFormat', '{profile.firstname} {profile.lastname}');
Yii::$app->settings->set('horImageScrollOnMobile', true);

View File

@ -24,7 +24,7 @@ use humhub\modules\ui\form\widgets\ActiveForm;
<hr/>
<?= $form->field($model, 'username') ?>
<hr/>
<?= $form->field($model, 'password') ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<hr/>
<?= $form->field($model, 'database') ?>
<?= $form->field($model, 'create')->checkbox() ?>

View File

@ -0,0 +1,28 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2025 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\post\controllers;
use humhub\modules\post\permissions\CreatePost;
final class ShareIntendController extends \humhub\modules\content\controllers\ShareIntendController
{
public function actionCreate()
{
return $this->renderAjax('create', [
'shareTarget' => $this->shareTarget,
'fileList' => \humhub\modules\file\controllers\ShareIntendController::getShareFileGuids(),
]);
}
protected function getCreatePermissionClass(): string
{
return CreatePost::class;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2025 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\post\widgets\Form;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\modules\ui\view\components\View;
use humhub\widgets\ModalButton;
use humhub\widgets\ModalDialog;
use yii\helpers\StringHelper;
/**
* @var $this View
* @var $fileList array
* @var $shareTarget ContentContainerActiveRecord
*/
?>
<?php ModalDialog::begin([
'id' => 'share-intend-modal',
'header' => Yii::t('FileModule.base', 'Share in {targetDisplayName}', [
'targetDisplayName' => $shareTarget->guid === Yii::$app->user->identity->guid ?
Yii::t('base', 'My Profile') :
Html::encode(StringHelper::truncate($shareTarget->displayName, 10)),
]),
]) ?>
<?php $form = ActiveForm::begin() ?>
<div class="modal-body">
<div id="space-content-create-form" data-stream-create-content="stream.wall.WallStream">
<?= Form::widget([
'contentContainer' => $shareTarget,
'fileList' => $fileList,
'isModal' => true,
]) ?>
</div>
</div>
<div class="modal-footer">
<?= ModalButton::defaultType(Yii::t('base', 'Back'))
->load(['/post/share-intend']) ?>
</div>
<?php ActiveForm::end() ?>
<?php ModalDialog::end() ?>
<script <?= Html::nonce() ?>>
$(function () {
humhub.modules.content.form.initModal();
$('#share-intend-modal').find('input[type=text], textarea, .ProseMirror').eq(0).trigger('click').focus();
});
</script>

View File

@ -45,8 +45,8 @@ class Form extends WallCreateContentForm
return array_merge([
'post' => $post,
'wallCreateContentForm' => $this,
'mentioningUrl' => $canCreatePostInSpace ? Url::to([$this->mentioningUrl, 'id' => $this->contentContainer->id]) : null,
'submitUrl' => $this->submitUrl,
], $additionalParams);
}

View File

@ -1,20 +1,21 @@
<?php
use humhub\modules\content\widgets\richtext\RichTextField;
use humhub\modules\content\widgets\WallCreateContentForm;
use humhub\modules\content\widgets\WallCreateContentFormFooter;
use humhub\modules\post\models\Post;
use humhub\modules\ui\form\widgets\ActiveForm;
/* @var WallCreateContentForm $wallCreateContentForm */
/* @var string $mentioningUrl */
/* @var ActiveForm $form */
/* @var Post $post */
/* @var string $submitUrl */
?>
<?= $form->field($post, 'message')->widget(RichTextField::class, [
'id' => 'contentForm_message',
'id' => 'contentForm_message' . ($wallCreateContentForm->isModal ? 'Modal' : ''),
'form' => $form,
'layout' => RichTextField::LAYOUT_INLINE,
'layout' => $wallCreateContentForm->isModal ? RichTextField::LAYOUT_BLOCK : RichTextField::LAYOUT_INLINE,
'pluginOptions' => ['maxHeight' => '300px'],
'placeholder' => Yii::t("PostModule.base", "What's on your mind?"),
'name' => 'message',
@ -25,5 +26,5 @@ use humhub\modules\ui\form\widgets\ActiveForm;
<?= WallCreateContentFormFooter::widget([
'contentContainer' => $post->content->container,
'submitUrl' => $submitUrl,
'wallCreateContentForm' => $wallCreateContentForm,
]) ?>

View File

@ -8,8 +8,8 @@
namespace humhub\modules\space\controllers;
use humhub\components\Controller;
use humhub\components\behaviors\AccessControl;
use humhub\components\Controller;
use humhub\modules\content\widgets\ContainerTagPicker;
use humhub\modules\space\models\Space;
use humhub\modules\space\widgets\Chooser;
@ -43,7 +43,7 @@ class BrowseController extends Controller
/**
* Returns a workspace list by json
*
* It can be filtered by by keyword.
* It can be filtered by keyword.
*/
public function actionSearchJson()
{

View File

@ -0,0 +1,96 @@
<?php
namespace humhub\modules\space\helpers;
use humhub\libs\BasePermission;
use humhub\modules\content\models\ContentContainerDefaultPermission;
use humhub\modules\space\models\Space;
use humhub\modules\space\widgets\Chooser;
use Yii;
use yii\db\Expression;
use yii\web\IdentityInterface;
final class CreateContentPermissionHelper
{
/**
* Returns a list of Spaces where the user has a special permission (e.g. CreatePost).
*
* @param string $permissionClass
* @param string|null $keyword
* @param IdentityInterface|null $user
* @return array
*/
public static function findSpaces(
string $permissionClass,
?string $keyword = null,
?IdentityInterface $user = null,
): array {
$user = $user ?? Yii::$app->user->identity;
$spaces = Space::find()
->visible($user)
->filterBlockedSpaces($user)
->andWhere(['space.status' => Space::STATUS_ENABLED]);
if ($keyword) {
$spaces->search($keyword);
}
if (!$user->isSystemAdmin()) {
// Check the User can create a Post in the searched Spaces
$spaces->leftJoin('space_membership', 'space_membership.space_id = space.id')
->leftJoin(
'contentcontainer_permission',
'contentcontainer_permission.contentcontainer_id = space.contentcontainer_id
AND contentcontainer_permission.group_id = space_membership.group_id
AND contentcontainer_permission.permission_id = :permission_id',
)
->andWhere(['space_membership.user_id' => $user->id])
->andWhere(['OR',
// Allowed by default
['AND',
['IN', 'space_membership.group_id', self::getDefaultAllowedGroups($permissionClass)],
['IS', 'contentcontainer_permission.permission_id', new Expression('NULL')],
],
// Set to allow
['contentcontainer_permission.state' => $permissionClass::STATE_ALLOW],
])
->addParams(['permission_id' => $permissionClass]);
}
$result = [];
foreach ($spaces->all() as $space) {
$result[] = Chooser::getSpaceResult($space);
}
return $result;
}
private static function getDefaultAllowedGroups(string $permissionClass): array
{
$defaultAllowedGroups = (new $permissionClass())->defaultAllowedGroups;
/* @var ContentContainerDefaultPermission[] $defaultPermissions */
$defaultPermissions = ContentContainerDefaultPermission::find()
->where(['contentcontainer_class' => Space::class])
->andWhere(['permission_id' => $permissionClass])
->all();
foreach ($defaultPermissions as $defaultPermission) {
switch ($defaultPermission->state) {
case BasePermission::STATE_ALLOW:
if (!in_array($defaultPermission->group_id, $defaultAllowedGroups)) {
$defaultAllowedGroups[] = $defaultPermission->group_id;
}
break;
case BasePermission::STATE_DENY:
if (($i = array_search($defaultPermission->group_id, $defaultAllowedGroups)) !== false) {
unset($defaultAllowedGroups[$i]);
}
break;
}
}
return $defaultAllowedGroups;
}
}

View File

@ -299,7 +299,7 @@ class Membership extends ActiveRecord
}
/**
* Returns an ActiveQuery selcting all memberships for the given $user.
* Returns an ActiveQuery selecting all memberships for the given $user.
*
* @param User $user
* @param int $membershipStatus the status of the Space by default self::STATUS_MEMBER.

View File

@ -2,6 +2,7 @@
namespace humhub\modules\space\models\forms;
use humhub\modules\admin\permissions\ManageSpaces;
use humhub\modules\admin\permissions\ManageUsers;
use humhub\modules\space\jobs\AddUsersToSpaceJob;
use humhub\modules\space\models\Membership;
@ -127,8 +128,10 @@ class InviteForm extends Model
$this->inviteExternalByEmail();
$this->space->auto_add_new_members = $this->addDefaultSpace ? 1 : null;
$this->space->save();
if (Yii::$app->user->can(ManageSpaces::class)) {
$this->space->auto_add_new_members = $this->addDefaultSpace ? 1 : null;
$this->space->save();
}
return true;
}
@ -168,7 +171,7 @@ class InviteForm extends Model
'forceMembership' => $this->withoutInvite,
'spaceId' => $this->space->id,
'userIds' => $this->getInviteIds(),
'allUsers' => $this->allRegisteredUsers,
'allUsers' => $this->allRegisteredUsers && Yii::$app->user->can(ManageUsers::class),
]));
}

View File

@ -44,7 +44,7 @@ use humhub\libs\Html;
'action-confirm-header' => Yii::t('SpaceModule.base', 'Change visibility'),
'confirm-text' => Yii::t('SpaceModule.base', 'Warning: If you change the visibility settings of a Space from public to private, all content within that Space, including posts, comments, attachments etc. will also be set to private. This means that non-members will no longer be able to see, access, or interact with any of the content within that Space.'),
],
]
],
); ?>
<?= DataSaved::widget(); ?>
@ -52,13 +52,3 @@ use humhub\libs\Html;
<?php ActiveForm::end(); ?>
</div>
</div>
<script <?= Html::nonce() ?>>
$('#space-visibility').on('change', function () {
if (this.value == 0) {
$('#space-join_policy, #space-default_content_visibility').val('0').prop('disabled', true);
} else {
$('#space-join_policy, #space-default_content_visibility').val('0').prop('disabled', false);
}
});
</script>

View File

@ -56,12 +56,18 @@ humhub.module('space', function (module, require, $) {
};
var changeVisibilityOption = function (event) {
const submitButton = event.$trigger.closest('form').find(':submit');
const form = event.$trigger.closest('form');
const submitButton = form.find(':submit');
const fields = form.find('select[name*=join_policy], select[name*=default_content_visibility]');
if (event.$trigger.val() == 0) {
submitButton.attr('data-action-confirm', submitButton.data('confirm-text'))
if (event.$trigger.val() === '0') {
// Private visibility
submitButton.attr('data-action-confirm', submitButton.data('confirm-text'));
fields.val(0).prop('disabled', true);
} else {
submitButton.removeAttr('data-action-confirm')
// Public or guest visibility
submitButton.removeAttr('data-action-confirm');
fields.prop('disabled', false);
}
};

View File

@ -2,6 +2,7 @@
namespace humhub\modules\space\widgets;
use humhub\modules\admin\permissions\ManageSpaces;
use humhub\modules\admin\permissions\ManageUsers;
use humhub\modules\space\models\forms\InviteForm;
use Yii;
@ -39,7 +40,9 @@ class InviteModal extends Widget
'model' => $this->model,
'attribute' => $this->attribute,
'searchUrl' => $this->searchUrl,
'canSelectAllRegisteredUsers' => Yii::$app->user->can(ManageUsers::class),
'canAddWithoutInvite' => Yii::$app->user->can(ManageUsers::class) || Yii::$app->getModule('space')->membersCanAddWithoutInvite === true,
'canAddAsDefaultSpace' => Yii::$app->user->can(ManageSpaces::class),
]);
}
}

View File

@ -2,10 +2,9 @@
namespace humhub\modules\space\widgets;
use humhub\modules\content\widgets\ContentContainerPickerField;
use humhub\modules\space\models\Space;
use humhub\modules\ui\form\widgets\BasePicker;
use Yii;
use yii\helpers\Html;
/**
* Mutliselect input field for selecting space guids.
@ -14,30 +13,19 @@ use yii\helpers\Html;
* @since 1.2
* @author buddha
*/
class SpacePickerField extends BasePicker
class SpacePickerField extends ContentContainerPickerField
{
/**
* @inheritdoc
* Min guids string value of Space model equal 2
*/
public $minInput = 2;
public $itemClass = Space::class;
/**
* @inheritdoc
*/
public $defaultRoute = '/space/browse/search-json';
public $itemClass = Space::class;
public $itemKey = 'guid';
/**
* @inheritdoc
* Min guids string value
*/
protected function getAttributes()
{
return array_merge(parent::getAttributes(), [
'data-tags' => 'false',
]);
}
public $minInput = 2;
/**
* @inheritdoc
@ -56,21 +44,4 @@ class SpacePickerField extends BasePicker
return $result;
}
/**
* @inheritdoc
*/
protected function getItemText($item)
{
return $item->getDisplayName();
}
/**
* @inheritdoc
*/
protected function getItemImage($item)
{
return Image::widget(['space' => $item, 'width' => 24]);
}
}

View File

@ -2,7 +2,9 @@
/* @var $this View */
/* @var $canInviteByEmail bool */
/* @var $canInviteByLink bool */
/* @var $canSelectAllRegisteredUsers bool */
/* @var $canAddWithoutInvite bool */
/* @var $canAddAsDefaultSpace bool */
/* @var $submitText string */
/* @var $submitAction string */
/* @var $model InviteForm */
@ -77,16 +79,20 @@ $form = ActiveForm::begin([
<?= $form->field($model, 'invite')
->widget(UserPickerField::class, ['disabledItems' => [Yii::$app->user->guid], 'url' => $searchUrl, 'focus' => true, 'id' => 'space-invite-user-picker']); ?>
<br/>
<?= $form->field($model, 'allRegisteredUsers')->checkbox() ?>
<?php if ($canSelectAllRegisteredUsers) : ?>
<br/>
<?= $form->field($model, 'allRegisteredUsers')->checkbox() ?>
<?php endif; ?>
<?php if ($canAddWithoutInvite) : ?>
<br/>
<?= $form->field($model, 'withoutInvite')->checkbox() ?>
<?php endif; ?>
<br/>
<?= $form->field($model, 'addDefaultSpace')->checkbox() ?>
<?php if ($canAddAsDefaultSpace) : ?>
<br/>
<?= $form->field($model, 'addDefaultSpace')->checkbox() ?>
<?php endif; ?>
</div>
<?php if ($canInviteByEmail) : ?>

View File

@ -80,14 +80,14 @@ abstract class BasePicker extends JsInputWidget
public $maxSelection = 50;
/**
* Minimum character input before triggering search query.
* Minimum characters input before triggering search query.
*
* @var int
*/
public $minInput = 3;
/**
* Minimum character input before triggering search query.
* Maximum characters input before triggering search query.
*
* @var int
*/

View File

@ -88,6 +88,7 @@ class InviteController extends Controller
$userInvite->email = $email;
$userInvite->source = Invite::SOURCE_INVITE;
$userInvite->user_originator_id = Yii::$app->user->getIdentity()->id;
$userInvite->language = Yii::$app->settings->get('defaultLanguage');
$existingInvite = Invite::findOne(['email' => $email]);
if ($existingInvite !== null) {

View File

@ -92,7 +92,7 @@ class RegistrationController extends Controller
$inviteRegistrationService->populateRegistration($registration);
} elseif (Yii::$app->session->has('authClient')) {
$authClient = Yii::$app->session->get('authClient');
$this->handleAuthClientRegistration($authClient, $registration);
$registration = $this->createRegistrationByAuthClient($authClient);
} else {
Yii::warning('Registration failed: No token (query) or authclient (session) found!', 'user');
Yii::$app->session->setFlash('error', 'Registration failed.');
@ -173,7 +173,7 @@ class RegistrationController extends Controller
* @param Registration $registration
* @throws Exception
*/
protected function handleAuthClientRegistration(ClientInterface $authClient, Registration $registration)
protected function createRegistrationByAuthClient(ClientInterface $authClient): Registration
{
$attributes = $authClient->getUserAttributes();
@ -181,7 +181,8 @@ class RegistrationController extends Controller
throw new Exception("No user id given by authclient!");
}
$registration->enablePasswordForm = false;
$registration = new Registration(enablePasswordForm: false);
if ($authClient instanceof ApprovalBypass) {
$registration->enableUserApproval = false;
}
@ -191,5 +192,7 @@ class RegistrationController extends Controller
$registration->getUser()->setAttributes($attributes, false);
$registration->getProfile()->setAttributes($attributes, false);
return $registration;
}
}

View File

@ -9,14 +9,17 @@
namespace humhub\modules\user\models;
use humhub\components\ActiveRecord;
use humhub\libs\ParameterEvent;
use humhub\modules\admin\notifications\ExcludeGroupNotification;
use humhub\modules\admin\notifications\IncludeGroupNotification;
use humhub\modules\admin\permissions\ManageGroups;
use humhub\modules\space\models\Space;
use humhub\modules\user\components\ActiveQueryUser;
use humhub\modules\user\models\forms\Registration;
use humhub\modules\user\Module;
use Throwable;
use Yii;
use yii\base\Event;
use yii\base\InvalidConfigException;
use yii\db\ActiveQuery;
use yii\db\Expression;
@ -50,6 +53,7 @@ use yii\helpers\Url;
*/
class Group extends ActiveRecord
{
public const EVENT_GET_REGISTRATION_GROUPS = 'getRegistrationGroups';
public const SCENARIO_EDIT = 'edit';
/**
@ -460,26 +464,32 @@ class Group extends ActiveRecord
/**
* Returns groups which are available in user registration
*
* @param User|null $user
* @return Group[] the groups which can be selected in registration
*/
public static function getRegistrationGroups()
public static function getRegistrationGroups(?User $user = null)
{
if (Yii::$app->getModule('user')->settings->get('auth.showRegistrationUserGroup')) {
$groups = self::find()
->where(['show_at_registration' => 1, 'is_admin_group' => 0])
->orderBy('name ASC')
->all();
if (count($groups) > 0) {
return $groups;
}
if (empty($groups)) {
$groups = [];
if ($defaultGroup = Yii::$app->getModule('user')->getDefaultGroup()) {
$groups[] = $defaultGroup;
}
}
$groups = [];
if ($defaultGroup = Yii::$app->getModule('user')->getDefaultGroup()) {
$groups[] = $defaultGroup;
}
$evt = new ParameterEvent([
'user' => $user,
'groups' => $groups,
]);
ParameterEvent::trigger(static::class, static::EVENT_GET_REGISTRATION_GROUPS, $evt);
return $groups;
return $evt->parameters['groups'];
}
/**

View File

@ -126,7 +126,7 @@ class GroupUser extends ActiveRecord
{
if ($this->scenario == self::SCENARIO_REGISTRATION) {
if ($this->group_id != '') {
$registrationGroups = Group::getRegistrationGroups();
$registrationGroups = Group::getRegistrationGroups($this->user);
foreach ($registrationGroups as $group) {
if ($this->group_id == $group->id) {
return;

View File

@ -413,9 +413,10 @@ class Profile extends ActiveRecord
*
* @param string $field
* @param bool $raw
* @param bool $encode
* @return string|null
*/
public function getFieldValue(string $field, bool $raw = false): ?string
public function getFieldValue(string $field, bool $raw = false, bool $encode = true): ?string
{
if (!$this->hasAttribute($field) || !$this->user) {
return null;
@ -423,6 +424,6 @@ class Profile extends ActiveRecord
$profileField = ProfileField::findOne(['internal_name' => $field]);
return $profileField?->getUserValue($this->user, $raw);
return $profileField?->getUserValue($this->user, $raw, $encode);
}
}

View File

@ -309,11 +309,11 @@ class ProfileField extends ActiveRecord
*
* @param User $user
* @param bool $raw
* @return string
* @return string|null
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
return $this->fieldType->getUserValue($user, $raw);
return $this->fieldType->getUserValue($user, $raw, $encode);
}
/**

View File

@ -699,7 +699,7 @@ class User extends ContentContainerActiveRecord implements IdentityInterface
}
if ($this->profile !== null) {
return $this->profile->getFieldValue(Yii::$app->settings->get('displayNameSubFormat', '')) ?? '';
return $this->profile->getFieldValue(Yii::$app->settings->get('displayNameSubFormat', ''), false, false) ?? '';
}
return '';

View File

@ -315,7 +315,7 @@ class BaseType extends Model
$query = $db->getQueryBuilder()->dropColumn(Profile::tableName(), $this->profileField->internal_name);
$db->createCommand($query)->execute();
} else {
Yii::error('Could not delete profile column - not exists!');
Yii::error('Could not delete profile column "' . $columnName . '" - not exists!');
}
}
@ -352,17 +352,14 @@ class BaseType extends Model
*
* @param User $user
* @param bool $raw
* @return string
* @param bool $encode
* @return string|null
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->{$this->profileField->internal_name} ?? '';
if ($raw) {
return $user->profile->$internalName;
} else {
return Html::encode($user->profile->$internalName);
}
return $encode ? Html::encode($value) : $value;
}
/**

View File

@ -34,9 +34,9 @@ abstract class BaseTypeVirtual extends BaseType
/**
* @inheritdoc
*/
final public function getUserValue(User $user, $raw = true): ?string
final public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
return $this->getVirtualUserValue($user, $raw);
return $this->getVirtualUserValue($user, $raw, $encode);
}
/**
@ -71,13 +71,14 @@ abstract class BaseTypeVirtual extends BaseType
}
/**
* Returns the readonly virutal value for the given User
* Returns the readonly virtual value for the given User
*
* @param User $user
* @param bool $raw
* @return mixed
* @param bool $encode
* @return string
*/
abstract protected function getVirtualUserValue($user, $raw = true);
abstract protected function getVirtualUserValue(User $user, bool $raw = true, bool $encode = true): string;
/**
* @inheritDoc

View File

@ -177,7 +177,7 @@ class Birthday extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$birthdayDate = \DateTime::createFromFormat(

View File

@ -8,6 +8,7 @@
namespace humhub\modules\user\models\fieldtype;
use humhub\libs\Html;
use humhub\modules\user\models\Profile;
use humhub\modules\user\models\User;
use Yii;
@ -108,16 +109,19 @@ class Checkbox extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->$internalName;
if (!$raw && !empty($value)) {
$labels = $this->getLabels();
return $labels[$internalName];
if (empty($value)) {
return '';
}
return $value;
if (!$raw) {
$value = $this->getLabels()[$internalName] ?? '';
}
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -164,14 +164,13 @@ class CheckboxList extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$internalNameOther = $internalName . '_other_selection';
$value = $user->profile->$internalName;
if (!$raw && $value !== null) {
$options = $this->getSelectItems();
$translatedValues = [];
if (is_string($value)) {
@ -179,15 +178,14 @@ class CheckboxList extends BaseType
}
foreach ($value as $v) {
if ($v === 'other' && !empty($user->profile->$internalNameOther)) {
$translatedValues[] = Html::encode($user->profile->$internalNameOther);
$translatedValues[] = $user->profile->$internalNameOther;
} elseif (isset($options[$v])) {
$translatedValues[] = Html::encode(Yii::t($this->profileField->getTranslationCategory(), $options[$v]));
$translatedValues[] = Yii::t($this->profileField->getTranslationCategory(), $options[$v]);
}
}
return implode(', ', $translatedValues);
$value = implode(', ', $translatedValues);
}
return $value;
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -89,16 +89,20 @@ class CountrySelect extends Select
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->$internalName;
$value = $user->profile->$internalName ?? '';
if (!$raw) {
return Html::encode(Iso3166Codes::country($value));
if (empty($value)) {
return '';
}
return $value;
if (!$raw) {
$value = Iso3166Codes::country($value);
}
return $encode ? Html::encode($value) : $value;
}
/**

View File

@ -79,20 +79,25 @@ class Date extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->$internalName ?? '';
$date = \DateTime::createFromFormat(
'Y-m-d',
$user->profile->$internalName ?? '',
$value,
new DateTimeZone(Yii::$app->formatter->timeZone),
);
if ($date === false) {
return "";
return '';
}
return $raw ? Html::encode($user->profile->$internalName) : Yii::$app->formatter->asDate($date, 'long');
if (!$raw) {
$value = Yii::$app->formatter->asDate($date, 'long');
}
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -110,21 +110,25 @@ class DateTime extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->$internalName ?? '';
$date = \DateTime::createFromFormat(
'Y-m-d H:i:s',
$user->profile->$internalName ?? '',
$value,
new DateTimeZone(Yii::$app->formatter->timeZone),
);
if ($date === false) {
return "";
return '';
}
return $raw ? Html::encode($user->profile->$internalName) : Yii::$app->formatter->asDatetime($date, 'long');
if (!$raw) {
$value = Yii::$app->formatter->asDatetime($date, 'long');
}
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -115,19 +115,19 @@ class Select extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): ?string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->$internalName;
$value = $user->profile->$internalName ?? '';
if (!$raw) {
$options = $this->getSelectItems();
if (isset($options[$value])) {
return Html::encode(Yii::t($this->profileField->getTranslationCategory(), $options[$value]));
$value = Yii::t($this->profileField->getTranslationCategory(), $options[$value]);
}
}
return $value;
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -31,13 +31,13 @@ class Template extends BaseType
];
}
public function getUserValue($user, $raw = true): ?string
public function getUserValue($user, bool $raw = true, bool $encode = true): ?string
{
$variables = ArrayHelper::map(
$user->profile->getProfileFields(null, [static::class]),
'internal_name',
function (ProfileField $profileField) use ($user) {
return $profileField->getUserValue($user);
function (ProfileField $profileField) use ($user, $raw, $encode) {
return $profileField->getUserValue($user, $raw, $encode);
},
);

View File

@ -207,17 +207,17 @@ class Text extends BaseType
/**
* @inheritdoc
*/
public function getUserValue(User $user, $raw = true): string
public function getUserValue(User $user, bool $raw = true, bool $encode = true): ?string
{
$internalName = $this->profileField->internal_name;
$value = $user->profile->$internalName;
$value = $user->profile->$internalName ?? '';
if (!$raw && (in_array($this->validator, [self::VALIDATOR_EMAIL, self::VALIDATOR_URL]) || !empty($this->linkPrefix))) {
$linkPrefix = ($this->validator === self::VALIDATOR_EMAIL) ? 'mailto:' : $this->linkPrefix;
return Html::a(Html::encode($value), $linkPrefix . $value);
return Html::a($encode ? Html::encode($value) : $value, $linkPrefix . $value);
}
return Html::encode($value);
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\user\models\fieldtype;
use humhub\libs\Html;
use humhub\modules\user\models\User;
/**
* UserEmail is a virtual profile field
@ -19,18 +20,20 @@ use humhub\libs\Html;
class UserEmail extends BaseTypeVirtual
{
/**
* @inheritDoc
* @inheritdoc
*/
public function getVirtualUserValue($user, $raw = true)
protected function getVirtualUserValue(User $user, bool $raw = true, bool $encode = true): string
{
if (empty($user->email)) {
return '';
}
if ($raw) {
return Html::encode($user->email);
} else {
return Html::a(Html::encode($user->email), 'mailto:' . $user->email);
$value = $encode ? Html::encode($user->email) : $user->email;
if (!$raw) {
return Html::a($value, 'mailto:' . $user->email);
}
return $value;
}
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\user\models\fieldtype;
use humhub\libs\Html;
use humhub\modules\user\models\User;
use Yii;
use yii\base\InvalidConfigException;
@ -21,15 +22,20 @@ use yii\base\InvalidConfigException;
class UserLastLogin extends BaseTypeVirtual
{
/**
* @inheritDoc
* @inheritdoc
* @throws InvalidConfigException
*/
public function getVirtualUserValue($user, $raw = true)
protected function getVirtualUserValue(User $user, bool $raw = true, bool $encode = true): string
{
if (empty($user->last_login)) {
$value = $user->last_login;
if (empty($value)) {
return '-';
}
return Yii::$app->formatter->asDate($user->last_login, 'long');
if (!$raw) {
$value = Yii::$app->formatter->asDate($value, 'long');
}
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\user\models\fieldtype;
use humhub\libs\Html;
use humhub\modules\user\models\User;
use Yii;
/**
@ -20,14 +21,19 @@ use Yii;
class UserMemberSince extends BaseTypeVirtual
{
/**
* @inheritDoc
* @inheritdoc
*/
public function getVirtualUserValue($user, $raw = true)
public function getVirtualUserValue(User $user, bool $raw = true, bool $encode = true): string
{
if (empty($user->created_at)) {
$value = $user->created_at;
if (empty($value)) {
return '';
}
return Yii::$app->formatter->asDate($user->created_at, 'long');
if (!$raw) {
$value = Yii::$app->formatter->asDate($value, 'long');
}
return $encode ? Html::encode($value) : $value;
}
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\user\models\fieldtype;
use humhub\libs\Html;
use humhub\modules\user\models\User;
/**
* UserName is a virtual profile field
@ -21,12 +22,12 @@ class UserName extends BaseTypeVirtual
/**
* @inheritDoc
*/
public function getVirtualUserValue($user, $raw = true)
public function getVirtualUserValue(User $user, bool $raw = true, bool $encode = true): string
{
if (empty($user->username)) {
return '';
}
return Html::encode($user->username);
return $encode ? Html::encode($user->username) : $user->username;
}
}

View File

@ -2,6 +2,7 @@
namespace humhub\modules\user\models\forms;
use humhub\helpers\DeviceDetectorHelper;
use humhub\modules\user\assets\UserAsset;
use humhub\modules\user\authclient\BaseClient;
use humhub\modules\user\authclient\BaseFormAuth;
@ -28,6 +29,11 @@ class Login extends Model
*/
public $rememberMe = false;
/**
* @var bool hide "Remember me" form field in the view
*/
public $hideRememberMe = false;
/**
* @var BaseClient auth client used to authenticate
*/
@ -50,6 +56,7 @@ class Login extends Model
public function init()
{
$this->rememberMe = Yii::$app->getModule('user')->loginRememberMeDefault;
$this->hideRememberMe = DeviceDetectorHelper::isAppRequest();
parent::init();
}

View File

@ -36,17 +36,17 @@ class Registration extends HForm
/**
* @var bool show password creation form
*/
public $enablePasswordForm = true;
private $enablePasswordForm;
/**
* @var bool show checkbox to force to change password on first log in
*/
public $enableMustChangePassword = false;
private $enableMustChangePassword;
/**
* @var bool show e-mail field
*/
public $enableEmailField = false;
private $enableEmailField;
/**
* @var bool|null require user approval by admin after registration.
@ -73,6 +73,21 @@ class Registration extends HForm
*/
private $_profile = null;
public function __construct(
$definition = [],
$primaryModel = null,
array $config = [],
bool $enableEmailField = false,
bool $enablePasswordForm = true,
bool $enableMustChangePassword = false,
) {
$this->enableEmailField = $enableEmailField;
$this->enablePasswordForm = $enablePasswordForm;
$this->enableMustChangePassword = $enableMustChangePassword;
parent::__construct($definition, $primaryModel, $config);
}
/**
* @inheritdoc
*/
@ -84,16 +99,9 @@ class Registration extends HForm
$this->enableUserApproval = false;
}
return parent::init();
}
/**
* @inheritdoc
*/
public function render($form)
{
$this->setFormDefinition();
return parent::render($form);
parent::init();
}
/**
@ -101,14 +109,18 @@ class Registration extends HForm
*/
protected function setFormDefinition()
{
$this->definition = [];
$this->definition['elements'] = [];
if (!isset($this->definition['elements']) || !is_array($this->definition['elements'])) {
$this->definition['elements'] = [];
}
$this->definition['elements']['User'] = $this->getUserFormDefinition();
$this->definition['elements']['GroupUser'] = $this->getGroupFormDefinition();
if ($this->enablePasswordForm) {
$this->definition['elements']['Password'] = $this->getPasswordFormDefinition();
}
$this->definition['elements']['Profile'] = array_merge(['type' => 'form'], $this->getProfile()->getFormDefinition());
$this->definition['elements']['Profile'] = array_merge(
['type' => 'form'],
$this->getProfile()->getFormDefinition(),
);
$this->definition['buttons'] = [
'save' => [
'type' => 'submit',
@ -185,9 +197,11 @@ class Registration extends HForm
protected function getGroupFormDefinition()
{
$groupModels = Group::getRegistrationGroups();
$groupModels = Group::getRegistrationGroups($this->getUser());
$groupFieldType = (Yii::$app->getModule('user')->settings->get('auth.showRegistrationUserGroup') && count($groupModels) > 1)
$groupFieldType = (Yii::$app->getModule('user')->settings->get('auth.showRegistrationUserGroup') && count(
$groupModels,
) > 1)
? 'dropdownlist'
: 'hidden'; // TODO: Completely hide the element instead of current <input type="hidden">
@ -272,7 +286,6 @@ class Registration extends HForm
}
if ($this->models['User']->save()) {
// Save User Profile
$this->models['Profile']->user_id = $this->models['User']->id;
$this->models['Profile']->save();
@ -296,7 +309,10 @@ class Registration extends HForm
if ($authClient !== null) {
(new AuthClientUserService($this->models['User']))->add($authClient);
$authClient->trigger(BaseClient::EVENT_CREATE_USER, new UserEvent(['identity' => $this->models['User']]));
$authClient->trigger(
BaseClient::EVENT_CREATE_USER,
new UserEvent(['identity' => $this->models['User']]),
);
}
$this->trigger(self::EVENT_AFTER_REGISTRATION, new UserEvent(['identity' => $this->models['User']]));
@ -368,7 +384,7 @@ class Registration extends HForm
$this->_groupUser->scenario = GroupUser::SCENARIO_REGISTRATION;
// assign default value for group_id
$registrationGroups = Group::getRegistrationGroups();
$registrationGroups = Group::getRegistrationGroups($this->getUser());
if (count($registrationGroups) == 1) {
$this->_groupUser->group_id = $registrationGroups[0]->id;
}

View File

@ -123,9 +123,7 @@ class AuthClientService
return null;
}
$registration = new Registration();
$registration->enablePasswordForm = false;
$registration->enableEmailField = true;
$registration = new Registration(enableEmailField: true, enablePasswordForm: false);
if ($this->authClient instanceof ApprovalBypass) {
$registration->enableUserApproval = false;

View File

@ -52,7 +52,9 @@ final class InviteRegistrationService
{
$invite = $this->getInvite();
if ($invite !== null) {
Yii::$app->setLanguage($invite->language);
if (Yii::$app->request->post('ChooseLanguage') === null) {
Yii::$app->setLanguage($invite->language);
}
$registration->getUser()->email = $invite->email;
}
}

View File

@ -69,14 +69,10 @@ class IncludeAllContributionsFilter extends ContentContainerStreamFilter
$conditionUserPrivateRestriction = '';
} else {
// User must be a space's member OR Space and Content are public
if ($queryUser) {
$spaceMembership = 'space_membership.status=3 OR ';
} else {
$spaceMembership = '';
}
$spaceMembership = $queryUser ? 'space_membership.status=3 OR ' : '';
$conditionSpaceMembershipRestriction = " AND ($spaceMembership (content.visibility=1 AND space.visibility != 0) )";
// User can view only content of own profile
$conditionUserPrivateRestriction = ' AND content.contentcontainer_id=' . $queryUser->contentcontainer_id;
// User can view a private content only of own profile
$conditionUserPrivateRestriction = $queryUser ? ' AND content.contentcontainer_id=' . $queryUser->contentcontainer_id : '';
}
// Build Access Check based on Space Content Container

View File

@ -25,4 +25,5 @@ return [
['user_id' => 5, 'firstname' => 'Disabled', 'lastname' => 'User'],
['user_id' => 6, 'firstname' => 'UnApproved', 'lastname' => 'User'],
['user_id' => 7, 'firstname' => 'UnApprovedNoGroup', 'lastname' => 'User'],
['user_id' => 8, 'firstname' => 'AdminNotMember', 'lastname' => 'User'],
];

View File

@ -31,7 +31,7 @@ class PeopleFiltersTest extends HumHubDbTestCase
ProfileField::updateAll(['directory_filter' => 1], ['IN', 'internal_name', ['firstname', 'lastname']]);
$peopleFilters = new PeopleFilters();
$this->assertEquals(['Admin', 'Andreas', 'Peter', 'Sara'], $this->getFilterOptions('fields[firstname]', $peopleFilters));
$this->assertEquals(['Admin', 'AdminNotMember', 'Andreas', 'Peter', 'Sara'], $this->getFilterOptions('fields[firstname]', $peopleFilters));
}
public function testReducedFilters()
@ -52,8 +52,8 @@ class PeopleFiltersTest extends HumHubDbTestCase
// Filter by firstname
$peopleQuery = new PeopleQuery(['defaultFilters' => ['fields' => ['firstname' => 'Admin']]]);
$peopleFilters = new PeopleFilters(['query' => $peopleQuery]);
$this->assertEquals(['Admin'], $this->getFilterOptions('fields[firstname]', $peopleFilters));
$this->assertEquals(['AdminLastName'], $this->getFilterOptions('fields[lastname]', $peopleFilters));
$this->assertEquals(['Admin', 'AdminNotMember'], $this->getFilterOptions('fields[firstname]', $peopleFilters));
$this->assertEquals(['AdminLastName', 'User'], $this->getFilterOptions('fields[lastname]', $peopleFilters));
$this->assertEquals(['' => 'Any', 1 => 'Administrator'], $this->getFilterOptions('groupId', $peopleFilters));
// Filter by group

View File

@ -53,7 +53,7 @@ $this->pageTitle = Yii::t('UserModule.auth', 'Login');
<?= $form->field($model, 'password')
->passwordInput(['id' => 'login_password', 'placeholder' => $model->getAttributeLabel('password'), 'aria-label' => $model->getAttributeLabel('password')])
->label(false); ?>
<?= $form->field($model, 'rememberMe')->checkbox(); ?>
<?= $model->hideRememberMe ? '' : $form->field($model, 'rememberMe')->checkbox(); ?>
<hr>
<div class="row">

View File

@ -69,7 +69,7 @@ use yii\widgets\ActiveForm;
<?php $form = ActiveForm::begin(['id' => 'account-login-form-modal', 'enableClientValidation' => false]); ?>
<?= $form->field($model, 'username')->textInput(['id' => 'login_username', 'placeholder' => $model->getAttributeLabel('username')]); ?>
<?= $form->field($model, 'password')->passwordInput(['id' => 'login_password', 'placeholder' => $model->getAttributeLabel('password')]); ?>
<?= $form->field($model, 'rememberMe')->checkbox(); ?>
<?= $model->hideRememberMe ? '' : $form->field($model, 'rememberMe')->checkbox(); ?>
<hr>
<div class="row">
<div class="col-md-4">

View File

@ -42,7 +42,7 @@ $categories = $user->profile->getProfileFieldCategories();
</label>
<div class="col-sm-9 field-value">
<?= ($field->field_type_class === MarkdownEditor::class) ?
RichText::output($field->getUserValue($user, true)) :
RichText::output($field->getUserValue($user, true, false)) :
$field->getUserValue($user, false) ?>
</div>
</div>

View File

@ -4,6 +4,7 @@ use humhub\libs\Html;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\modules\user\models\forms\Registration;
use humhub\modules\user\widgets\AuthChoice;
use humhub\widgets\LanguageChooser;
use humhub\widgets\SiteLogo;
/**
@ -32,12 +33,15 @@ $this->pageTitle = Yii::t('UserModule.auth', 'Create Account');
<?php if ($showRegistrationForm): ?>
<?php $form = ActiveForm::begin(['id' => 'registration-form', 'enableClientValidation' => false]); ?>
<?= Html::hiddenInput('ChooseLanguage[language]', Yii::$app->language) ?>
<?= $hForm->render($form); ?>
<?php ActiveForm::end(); ?>
<?php endif; ?>
</div>
</div>
</div>
<?= LanguageChooser::widget() ?>
</div>
<script <?= Html::nonce() ?>>

View File

@ -36,6 +36,14 @@ class Image extends BaseImage
public bool $showSelfOnlineStatus = false;
/**
* @inheritdoc
*/
public function beforeRun()
{
return parent::beforeRun() && $this->user instanceof User;
}
/**
* @inheritdoc
*/

View File

@ -3,11 +3,11 @@
namespace humhub\modules\user\widgets;
use humhub\libs\BasePermission;
use humhub\modules\user\models\UserFilter;
use Yii;
use yii\base\Widget;
use yii\helpers\Html;
use yii\helpers\Url;
use humhub\modules\user\models\UserFilter;
/**
* UserPickerWidget displays a user picker instead of an input field.
@ -254,7 +254,7 @@ class UserPicker extends Widget
* @param type $permission
* @return type
*/
private static function createJSONUserInfo($user, $permission = null, $priority = null)
public static function createJSONUserInfo($user, $permission = null, $priority = null)
{
$disabled = false;

View File

@ -2,7 +2,7 @@
namespace humhub\modules\user\widgets;
use humhub\modules\ui\form\widgets\BasePicker;
use humhub\modules\content\widgets\ContentContainerPickerField;
use humhub\modules\user\models\User;
use Yii;
use yii\helpers\Url;
@ -13,8 +13,10 @@ use yii\helpers\Url;
* @since 1.2
* @author buddha
*/
class UserPickerField extends BasePicker
class UserPickerField extends ContentContainerPickerField
{
public $itemClass = User::class;
/**
* @inheritdoc
*/
@ -25,24 +27,6 @@ class UserPickerField extends BasePicker
*/
public $jsWidget = 'user.picker.UserPicker';
/**
* @inheritdoc
* The 'guid' value is default for UserPickerField
*/
public $itemKey = 'guid';
/**
* @inheritdoc
*/
public function init()
{
$this->itemClass = User::class;
if (empty($this->itemKey)) {
$this->itemKey = 'guid';
}
parent::init();
}
/**
* @inheritdoc
*/
@ -50,26 +34,16 @@ class UserPickerField extends BasePicker
{
if (!$this->url) {
// provide the space id if the widget is calling from a space
if (Yii::$app->controller->id == 'space') {
if (Yii::$app->controller->id === 'space') {
return Url::to([$this->defaultRoute, 'space_id' => Yii::$app->controller->getSpace()->id]);
} else {
return Url::to([$this->defaultRoute]);
}
return Url::to([$this->defaultRoute]);
}
return parent::getUrl();
}
/**
* @inheritdoc
*/
protected function getAttributes()
{
return array_merge(parent::getAttributes(), [
'data-tags' => 'false',
]);
}
/**
* @inheritdoc
*/
@ -88,8 +62,7 @@ class UserPickerField extends BasePicker
if ($this->placeholder && !$this->placeholderMore) {
$result['placeholder-more'] = $this->placeholder;
} else {
$result['placeholder-more'] = ($this->placeholderMore) ? $this->placeholderMore
: Yii::t('UserModule.chooser', 'Add user');
$result['placeholder-more'] = ($this->placeholderMore) ?: Yii::t('UserModule.chooser', 'Add user');
}
$result['no-result'] = Yii::t('UserModule.chooser', 'No users found for the given query.');
@ -103,20 +76,4 @@ class UserPickerField extends BasePicker
}
return $result;
}
/**
* @inheritdoc
*/
protected function getItemText($item)
{
return $item->displayName;
}
/**
* @inheritdoc
*/
protected function getItemImage($item)
{
return $item->getProfileImage()->getUrl();
}
}

View File

@ -3,6 +3,7 @@
namespace tests\codeception\_support;
use Codeception\Module;
use Codeception\TestInterface;
use Yii;
/**
@ -40,4 +41,35 @@ class WebHelper extends Module
Yii::$app->moduleManager->enableModules($cfg['humhub_modules']);
}
}
/**
* @inheritdoc
*/
public function _failed(TestInterface $test, $fail)
{
parent::_failed($test, $fail);
$filePath = codecept_output_dir() . str_replace(['\\', '/', ':', ' '], '.', $test->getSignature());
$logFilePath = Yii::getAlias('@runtime/logs') . DIRECTORY_SEPARATOR . 'app.log';
if (file_exists($logFilePath)) {
copy($logFilePath, $filePath . '.app.log');
}
if (!Yii::$app->db->isActive) {
return;
}
preg_match('/host=([^;]+)/', Yii::$app->db->dsn, $hostMatch);
preg_match('/dbname=([^;]+)/', Yii::$app->db->dsn, $dbMatch);
exec(sprintf(
'mysqldump --skip-column-statistics -u%s -p%s -h%s %s > %s',
escapeshellarg(Yii::$app->db->username ?? 'root'),
escapeshellarg(Yii::$app->db->password ?? 'root'),
escapeshellarg($hostMatch[1] ?? '127.0.0.1'),
escapeshellarg($dbMatch[1] ?? 'humhub_test'),
escapeshellarg($filePath . '.dump.sql'),
));
}
}

View File

@ -23,23 +23,56 @@ class FormatterTest extends Unit
999 => '999',
1000 => '1K',
1234 => '1K',
9990 => '9K',
9990 => '10K',
123456 => '123K',
123999 => '123K',
999999 => '999K',
123999 => '124K',
899999 => '900K',
999999 => '1M',
1234567 => '1M',
123456789 => '123M',
123999500 => '124M',
999999499 => '999M',
999999500 => '1000M',
999999499 => '1B',
999999500 => '1B',
1234567899 => '1B',
123456789999 => '123B',
12345678999999 => '12345B',
999999999499999 => '999999B',
12345678999999 => '12346B',
999999999499999 => '1000000B',
999999999500000 => '1000000B',
];
foreach ($testNumbers as $numberValue => $result) {
$this->assertEquals(Yii::$app->formatter->asShortInteger($numberValue), $result);
}
}
public function testAsShortIntegerArabic()
{
Yii::$app->formatter->locale = 'ar';
$testNumbers = [
1 => '١',
12 => '١٢',
123 => '١٢٣',
999 => '٩٩٩',
1000 => '١K',
1234 => '١K',
9990 => '١٠K',
123456 => '١٢٣K',
123999 => '١٢٤K',
899999 => ٠٠K',
999999 => '١M',
1234567 => '١M',
123456789 => '١٢٣M',
123999500 => '١٢٤M',
999999499 => '١B',
999999500 => '١B',
1234567899 => '١B',
123456789999 => '١٢٣B',
12345678999999 => '١٢٣٤٦B',
999999999499999 => '١٠٠٠٠٠٠B',
999999999500000 => '١٠٠٠٠٠٠B',
];
foreach ($testNumbers as $numberValue => $result) {
$this->assertEquals(Yii::$app->formatter->asShortInteger($numberValue), $result);
}
}
}

View File

@ -32,6 +32,14 @@ use yii\base\Widget;
*/
class LanguageChooser extends Widget
{
/**
* @inheritdoc
*/
public function beforeRun()
{
return parent::beforeRun() && Yii::$app->user->isGuest;
}
/**
* Displays / Run the Widget
*/

View File

@ -6,6 +6,7 @@
*/
use humhub\modules\ui\menu\MenuLink;
use humhub\widgets\LanguageChooser;
use humhub\widgets\PoweredBy;
use yii\helpers\Html;
@ -28,7 +29,9 @@ use yii\helpers\Html;
<?php endif; ?>
<?php endforeach; ?>
<?= PoweredBy::widget(); ?>
<?= PoweredBy::widget() ?>
<?= LanguageChooser::widget() ?>
</small>
</div>
<br/>

View File

@ -6,6 +6,7 @@
*/
use humhub\modules\ui\menu\MenuLink;
use humhub\widgets\LanguageChooser;
use humhub\widgets\PoweredBy;
use yii\helpers\Html;
@ -30,6 +31,8 @@ use yii\helpers\Html;
<?php endforeach; ?>
<?= PoweredBy::widget(); ?>
<?= LanguageChooser::widget() ?>
</small>
</div>
<br/>

View File

@ -92,6 +92,10 @@ humhub.module('ui.modal', function (module, require, $) {
});
this.set(options);
// Store initial data to reset it
this.initialOptions = options;
this.initialDialog = this.getDialog().clone(true);
};
Modal.prototype.checkAriaLabel = function () {
@ -144,16 +148,14 @@ humhub.module('ui.modal', function (module, require, $) {
* @returns {undefined}
*/
Modal.prototype.reset = function () {
// Clear old script tags.
var $content = this.getContent().empty();
this.$.find('script').remove();
$content.append('<div class="modal-body" />');
loader.set(this.getBody());
this.isFilled = false;
this.getDialog().removeClass('modal-dialog-large modal-dialog-normal modal-dialog-small modal-dialog-extra-small modal-dialog-medium');
//reset listeners:
// Remove scripts from previous modal window
this.$.find('script').remove();
// Reset dialog to initial html
this.getDialog().replaceWith(this.initialDialog.clone(true));
// Reset options in order to display initial window on load new window
this.options = this.initialOptions;
// Reset listeners to avoid unexpected js behaviour
this.resetListener();
};

Some files were not shown because too many files have changed in this diff Show More