Merge branch 'develop' of github.com:humhub/humhub into develop

This commit is contained in:
HumHub Translations 2023-02-28 07:27:17 +00:00
commit 17cd8bbff0
149 changed files with 2434 additions and 1496 deletions

View File

@ -5,10 +5,14 @@ on:
branches:
- master
- develop
paths-ignore:
- '*.md'
pull_request:
branches:
- master
- develop
paths-ignore:
- '*.md'
permissions:
contents: read

View File

@ -10,9 +10,17 @@ HumHub Changelog (DEVELOP)
- Fix #6018: Disable profile fields textarea and checkbox list when they are not editable
- Ehn #6017: Hide Password Tab in administration for LDAP users
- Enh #6031: On users list grid, show Auth mode badge only for sys admins
- Enh #6033: Moved more logic into `AbstractQueryContentContainer`
- Enh #6035: Added Estonian language
- Fix #5956: Display all newer comments after current comment
- Enh #6061: Administration: Add a confirmation on profile field delete button
- Enh #5699: Allow users to invite by link
- Enh #6081: Added corresponding CSS variables for LESS color variables
- Fix #6022: Fix Changelog Link with new Marketplace URL
- Enh #5973: Stylesheet Prefix Cleanup and removed temporary style
- Enh #6077: Always display content tabs
- Enh #5263: Allow members of groups other than system admin to view all content (groups that can manage users for profile content and groups that can manage spaces for space content)
- Enh #6102: Also allow Messages module to inject new message count into page title
- Enh #6109: Added enabled Pretty URL as self test
- Enh #6119: Added UserInfoWidget for User Notification Settings
- Enh #6116: Scheduled content publishing

View File

@ -4,7 +4,12 @@ HumHub Changelog
1.13.2 (Unreleased)
-------------------------
- Fix #5965: Suppress log warning 'Invalid session auth key attempted for user'
- Fix #6084: Automatic LDAP user registration broken when not all req. attributes provided
- Fix #6104: Fix update user with not existing group
- Fix #6103: Fix null passing to parse_str()
- Fix #6108: Fix log time in the `date()` function
- Fix #6122: Fix deleting a content with empty reason
- Fix #6128: Reset backuped content after submit form
1.13.1 (January 25, 2023)
-------------------------

View File

@ -73,7 +73,7 @@
"web-token/jwt-signature-algorithm-hmac": ">=1.0 <3.0",
"web-token/jwt-signature-algorithm-rsa": ">=1.0 <3.0",
"xj/yii2-jplayer-widget": "*",
"yiisoft/yii2": "2.0.47",
"yiisoft/yii2": "dev-master#e2b40a2",
"yiisoft/yii2-authclient": "~2.2.0",
"yiisoft/yii2-bootstrap": "~2.0.0",
"yiisoft/yii2-httpclient": "~2.0.0",

67
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9b5f7b0d900013a6baefdd0afd4fd998",
"content-hash": "74fc5c69dbbd413e42d9b63e83706576",
"packages": [
{
"name": "async-aws/core",
@ -4241,16 +4241,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.27.0",
"version": "1.27.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "eeb8582f9cabf5a7f4ef78015691163233a1834f"
"reference": "ef4e6ef74990239946d3983451a9bbed5ef1be5d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eeb8582f9cabf5a7f4ef78015691163233a1834f",
"reference": "eeb8582f9cabf5a7f4ef78015691163233a1834f",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/ef4e6ef74990239946d3983451a9bbed5ef1be5d",
"reference": "ef4e6ef74990239946d3983451a9bbed5ef1be5d",
"shasum": ""
},
"require": {
@ -4277,7 +4277,7 @@
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.2.4",
@ -4340,9 +4340,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.27.0"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.27.1"
},
"time": "2023-01-24T20:07:45+00:00"
"time": "2023-02-08T07:02:13+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -7956,16 +7956,16 @@
},
{
"name": "twig/twig",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "3ffcf4b7d890770466da3b2666f82ac054e7ec72"
"reference": "a6e0510cc793912b451fd40ab983a1d28f611c15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3ffcf4b7d890770466da3b2666f82ac054e7ec72",
"reference": "3ffcf4b7d890770466da3b2666f82ac054e7ec72",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15",
"reference": "a6e0510cc793912b451fd40ab983a1d28f611c15",
"shasum": ""
},
"require": {
@ -8016,7 +8016,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.5.0"
"source": "https://github.com/twigphp/Twig/tree/v3.5.1"
},
"funding": [
{
@ -8028,7 +8028,7 @@
"type": "tidelift"
}
],
"time": "2022-12-27T12:28:18+00:00"
"time": "2023-02-08T07:49:20+00:00"
},
{
"name": "voku/portable-ascii",
@ -8624,16 +8624,16 @@
},
{
"name": "yiisoft/yii2",
"version": "2.0.47",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-framework.git",
"reference": "8ecf57895d9c4b29cf9658ffe57af5f3d0e25254"
"reference": "e2b40a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/8ecf57895d9c4b29cf9658ffe57af5f3d0e25254",
"reference": "8ecf57895d9c4b29cf9658ffe57af5f3d0e25254",
"url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/e2b40a2",
"reference": "e2b40a2",
"shasum": ""
},
"require": {
@ -8650,6 +8650,7 @@
"php": ">=5.4.0",
"yiisoft/yii2-composer": "~2.0.4"
},
"default-branch": true,
"bin": [
"yii"
],
@ -8742,7 +8743,7 @@
"type": "tidelift"
}
],
"time": "2022-11-18T16:21:58+00:00"
"time": "2023-02-11T16:21:51+00:00"
},
{
"name": "yiisoft/yii2-authclient",
@ -10387,37 +10388,38 @@
},
{
"name": "php-webdriver/webdriver",
"version": "1.13.1",
"version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/php-webdriver/php-webdriver.git",
"reference": "6dfe5f814b796c1b5748850aa19f781b9274c36c"
"reference": "3ea4f924afb43056bf9c630509e657d951608563"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/6dfe5f814b796c1b5748850aa19f781b9274c36c",
"reference": "6dfe5f814b796c1b5748850aa19f781b9274c36c",
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3ea4f924afb43056bf9c630509e657d951608563",
"reference": "3ea4f924afb43056bf9c630509e657d951608563",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-zip": "*",
"php": "^5.6 || ~7.0 || ^8.0",
"php": "^7.3 || ^8.0",
"symfony/polyfill-mbstring": "^1.12",
"symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0 || ^6.0"
"symfony/process": "^5.0 || ^6.0"
},
"replace": {
"facebook/webdriver": "*"
},
"require-dev": {
"ondram/ci-detector": "^2.1 || ^3.5 || ^4.0",
"ergebnis/composer-normalize": "^2.20.0",
"ondram/ci-detector": "^4.0",
"php-coveralls/php-coveralls": "^2.4",
"php-mock/php-mock-phpunit": "^1.1 || ^2.0",
"php-mock/php-mock-phpunit": "^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9",
"phpunit/phpunit": "^9.3",
"squizlabs/php_codesniffer": "^3.5",
"symfony/var-dumper": "^3.3 || ^4.0 || ^5.0 || ^6.0"
"symfony/var-dumper": "^5.0 || ^6.0"
},
"suggest": {
"ext-SimpleXML": "For Firefox profile creation"
@ -10446,9 +10448,9 @@
],
"support": {
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.13.1"
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.14.0"
},
"time": "2022-10-11T11:49:44+00:00"
"time": "2023-02-09T12:12:19+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@ -11585,7 +11587,8 @@
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"npm-asset/select2-bootstrap-theme": 10
"npm-asset/select2-bootstrap-theme": 10,
"yiisoft/yii2": 20
},
"prefer-stable": false,
"prefer-lowest": false,

View File

@ -2,7 +2,7 @@
/**
* This file is generated by the "yii asset" command.
* DO NOT MODIFY THIS FILE DIRECTLY.
* @version 2022-11-29 14:04:39
* @version 2023-02-27 09:01:41
*/
return [
'app' => [

View File

@ -11,6 +11,7 @@ namespace humhub\libs;
use Yii;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\db\conditions\LikeCondition;
use yii\db\StaleObjectException;
use yii\helpers\Json;
@ -112,7 +113,11 @@ abstract class BaseSettingsManager extends Component
{
$value = $this->get($name, $default);
if (is_string($value)) {
$value = Json::decode($value);
try {
$value = Json::decode($value);
} catch (InvalidArgumentException $ex) {
Yii::error($ex->getMessage());
}
}
return $value;
}

View File

@ -8,6 +8,7 @@
namespace humhub\libs;
use humhub\models\Setting;
use humhub\modules\admin\libs\HumHubAPI;
use humhub\modules\ldap\helpers\LdapHelper;
use humhub\modules\marketplace\Module;
@ -434,13 +435,29 @@ class SelfTest
];
}
if (Setting::isInstalled()) {
$title = Yii::t('AdminModule.information', 'Settings') . ' - ' . Yii::t('AdminModule.information', 'Pretty URLs');
if (Yii::$app->urlManager->enablePrettyUrl) {
$checks[] = [
'title' => $title,
'state' => 'OK'
];
} else {
$checks[] = [
'title' => $title,
'state' => 'WARNING',
'hint' => Html::a(Yii::t('AdminModule.information', 'HumHub Documentation'), 'https://docs.humhub.org/docs/admin/installation#pretty-urls'),
];
}
}
$title = Yii::t('AdminModule.information', 'Settings') . ' - ' . Yii::t('AdminModule.information', 'Base URL');
$sslPort = 443;
$httpPort = 80;
$scheme = $_SERVER['REQUEST_SCHEME'] ?? (
isset($_SERVER['HTTPS'])
? ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] === 1 || $_SERVER['SERVER_PORT'] == $sslPort ? 'https' : 'http')
: ($_SERVER['SERVER_PORT'] == $sslPort ? 'https' : 'http'));
isset($_SERVER['HTTPS'])
? ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] === 1 || $_SERVER['SERVER_PORT'] == $sslPort ? 'https' : 'http')
: ($_SERVER['SERVER_PORT'] == $sslPort ? 'https' : 'http'));
$currentBaseUrl = $scheme . '://' . $_SERVER['HTTP_HOST']
. (($scheme === 'https' && $_SERVER['SERVER_PORT'] == $sslPort) ||
($scheme === 'http' && $_SERVER['SERVER_PORT'] == $httpPort) ? '' : ':' . $_SERVER['SERVER_PORT'])
@ -610,7 +627,7 @@ class SelfTest
];
} else {
$allowedDriverTitles = [];
foreach(self::getSupportedDatabaseDrivers() as $allowedDriver) {
foreach (self::getSupportedDatabaseDrivers() as $allowedDriver) {
$allowedDriverTitles[] = $allowedDriver['title'];
}
$checks[] = [
@ -745,7 +762,7 @@ class SelfTest
$supportedDrivers = self::getSupportedDatabaseDrivers();
// Firstly parse driver name from version:
if (preg_match('/(' . implode('|', array_keys($supportedDrivers)). ')/i', $driver['version'], $verMatch)) {
if (preg_match('/(' . implode('|', array_keys($supportedDrivers)) . ')/i', $driver['version'], $verMatch)) {
$driver['name'] = strtolower($verMatch[1]);
} else {
$driver['name'] = Yii::$app->getDb()->getDriverName();

View File

@ -8,7 +8,9 @@
namespace humhub\modules\activity;
use humhub\components\ActiveRecord;
use humhub\modules\activity\components\MailSummary;
use humhub\modules\activity\helpers\ActivityHelper;
use humhub\modules\activity\jobs\SendMailSummary;
use humhub\modules\activity\models\Activity;
use humhub\modules\admin\permissions\ManageSettings;
@ -19,7 +21,7 @@ use Yii;
use yii\base\ActionEvent;
use yii\base\BaseObject;
use yii\base\Event;
use yii\db\ActiveRecord;
use yii\base\InvalidArgumentException;
use yii\db\IntegrityException;
/**
@ -66,23 +68,10 @@ class Events extends BaseObject
public static function onActiveRecordDelete(Event $event)
{
if (!($event->sender instanceof ActiveRecord)) {
throw new \LogicException('The handler can be applied only to the \yii\db\ActiveRecord.');
throw new InvalidArgumentException('The handler can be applied only to the \humhub\components\ActiveRecord.');
}
/** @var \yii\db\ActiveRecord $activeRecordModel */
$activeRecordModel = $event->sender;
$pk = $activeRecordModel->getPrimaryKey();
// Check if primary key exists and is not array (multiple pk)
if ($pk !== null && !is_array($pk)) {
$modelsActivity = Activity::find()->where([
'object_id' => $pk,
'object_model' => get_class($activeRecordModel)
])->each();
foreach ($modelsActivity as $activity) {
$activity->delete();
}
}
ActivityHelper::deleteActivitiesForRecord($event->sender);
}
public static function onAccountMenuInit($event)

View File

@ -0,0 +1,32 @@
<?php
namespace humhub\modules\activity\helpers;
use humhub\components\ActiveRecord;
use humhub\modules\activity\models\Activity;
use Yii;
class ActivityHelper
{
public static function deleteActivitiesForRecord(ActiveRecord $record)
{
$pk = $record->getPrimaryKey();
// Check if primary key exists and is not array (multiple pk)
if ($pk !== null && !is_array($pk)) {
$modelsActivity = Activity::find()->where([
'object_id' => $pk,
'object_model' => get_class($record)
])->each();
foreach ($modelsActivity as $activity) {
$activity->delete();
}
Yii::debug('Deleted activities for ' . get_class($record) . " with PK " . $pk, 'activity');
}
}
}

View File

@ -15,7 +15,6 @@ use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecord;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\activity\components\ActivityWebRenderer;
use humhub\components\behaviors\PolymorphicRelation;
use yii\db\IntegrityException;
use humhub\modules\activity\widgets\Activity as ActivityStreamEntryWidget;
@ -124,4 +123,13 @@ class Activity extends ContentActiveRecord
{
return $this->getPolymorphicRelation();
}
/**
* @return bool|int
*/
public function delete()
{
// Always hard delete activities
return $this->hardDelete();
}
}

View File

@ -96,7 +96,7 @@ use yii\helpers\Html;
<span
style="text-decoration: none; color: <?= $this->theme->variable('primary') ?>;"> - <a
href="<?= $url ?>"
style="text-decoration: none; color: <?= $this->theme->variable('primary') ?>; "><?= Yii::t('ActivityModule.base', 'see online') ?></a></span>
style="text-decoration: none; color: <?= $this->theme->variable('primary') ?>; font-weight: bold;"><?= Yii::t('ActivityModule.base', 'see online') ?></a></span>
<!-- END: CONTENT LINK -->
<?php endif; ?>
</div>

View File

@ -59,7 +59,8 @@ class PendingRegistrationsController extends Controller
'searchModel' => $searchModel,
'types' => [
null => null,
PendingRegistrationSearch::SOURCE_INVITE => Yii::t('AdminModule.base', 'Invite'),
PendingRegistrationSearch::SOURCE_INVITE => Yii::t('AdminModule.base', 'Invite by email'),
PendingRegistrationSearch::SOURCE_INVITE_BY_LINK => Yii::t('AdminModule.base', 'Invite by link'),
PendingRegistrationSearch::SOURCE_SELF => Yii::t('AdminModule.base', 'Sign up'),
]
]);

View File

@ -314,7 +314,7 @@ class SettingController extends Controller
// I wish..
if ($dating) {
$dating = date('Y-m-d H:i:s', $dating->log_time);
$dating = date('Y-m-d H:i:s', (int) $dating->log_time);
} else {
$dating = "the begining of time";
}

View File

@ -149,7 +149,8 @@ class UserController extends Controller
'items' => UserEditForm::getGroupItems(),
'options' => [
'data-placeholder' => Yii::t('AdminModule.user', 'Select Groups'),
'data-placeholder-more' => Yii::t('AdminModule.user', 'Add Groups...')
'data-placeholder-more' => Yii::t('AdminModule.user', 'Add Groups...'),
'data-tags' => 'false'
],
'maxSelection' => 250,
'isVisible' => Yii::$app->user->can(new ManageGroups())

View File

@ -76,7 +76,7 @@ class PendingRegistrationSearch extends Invite
$query->andFilterWhere(['like', 'username', $this->getAttribute('originator.username')]);
$query->andFilterWhere(['like', 'user_invite.email', $this->email]);
$query->andFilterWhere(['like', 'user_invite.language', $this->language]);
$query->andFilterWhere(['like', 'source', $this->source]);
$query->andFilterWhere(['source' => $this->source]);
return $dataProvider;
}

View File

@ -2,12 +2,11 @@
namespace humhub\modules\admin\models\forms;
use humhub\libs\Html;
use humhub\modules\user\models\GroupUser;
use Yii;
use humhub\modules\user\models\User;
use humhub\modules\user\models\Group;
use humhub\modules\admin\permissions\ManageGroups;
use humhub\modules\user\models\Group;
use humhub\modules\user\models\GroupUser;
use humhub\modules\user\models\User;
use Yii;
/**
* Description of UserEditForm
@ -18,14 +17,13 @@ class UserEditForm extends User
{
/**
* GroupId selection array of the form.
* @var type
* @var array
*/
public $groupSelection;
/**
* Current member groups (models) of the given $user
* @var type
*
* @var Group[]
*/
public $currentGroups;
@ -103,19 +101,19 @@ class UserEditForm extends User
if (!$this->isCurrentlyMemberOf($groupId)) {
/* @var $group Group */
$group = Group::findOne(['id' => $groupId]);
if(!$group->is_admin_group || Yii::$app->user->isAdmin()) {
if ($group && (!$group->is_admin_group || Yii::$app->user->isAdmin())) {
$group->addUser($this);
}
}
}
}
return parent::afterSave($insert, $changedAttributes);
parent::afterSave($insert, $changedAttributes);
}
/**
* Checks if the given group (id or model object) is contained in the form selection
* @param integer $groupId groupId or Group model object
* @param int|Group $groupId groupId or Group model object
* @return boolean true if contained in selection else false
*/
private function isInGroupSelection($groupId)

View File

@ -9,7 +9,7 @@ use yii\web\View;
/* @var $name string */
/** @var OEmbedProviderForm $model */
parse_str($model->endpoint, $query);
parse_str($model->endpoint ?? '', $query);
$this->registerJs(<<<JS
function initEndpointInputs() {

View File

@ -67,7 +67,7 @@ AdminPendingRegistrationsAsset::register($this);
'options' => ['width' => '40px'],
'format' => 'raw',
'value' => function ($data) use ($types) {
return isset($types[$data->source]) ?: Html::encode($data->source);
return $types[$data->source] ?? Html::encode($data->source);
},
],
[

View File

@ -65,7 +65,7 @@ class CommentTest extends HumHubDbTestCase
$comment->save();
$post = Post::findOne(['id' => 11]);
$post->delete();
$post->hardDelete();
$this->assertNull(Comment::findOne(['id' => $comment->id]));
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\content;
use humhub\commands\IntegrityController;
use humhub\components\Event;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\search\interfaces\Searchable;
@ -92,7 +93,7 @@ class Events extends BaseObject
/**
* On init of the WallEntryAddonWidget, attach the wall entry links widget.
*
* @param CEvent $event
* @param Event $event
*/
public static function onWallEntryAddonInit($event)
{
@ -105,13 +106,13 @@ class Events extends BaseObject
/**
* On rebuild of the search index, rebuild all user records
*
* @param type $event
* @param Event $event
*/
public static function onSearchRebuild($event)
{
foreach (Content::find()->each() as $content) {
$contentObject = $content->getPolymorphicRelation();
if ($contentObject instanceof Searchable) {
if ($contentObject instanceof Searchable && $content->state === Content::STATE_PUBLISHED) {
Yii::$app->search->add($contentObject);
}
}
@ -126,7 +127,10 @@ class Events extends BaseObject
{
/** @var ContentActiveRecord $record */
$record = $event->sender;
SearchHelper::queueUpdate($record);
if ($record->content->state === Content::STATE_PUBLISHED) {
SearchHelper::queueUpdate($record);
}
}
/**
@ -141,4 +145,21 @@ class Events extends BaseObject
SearchHelper::queueDelete($record);
}
/**
* Callback on daily cron job run
*/
public static function onCronDailyRun(): void
{
Yii::$app->queue->push(new jobs\PurgeDeletedContents());
}
/**
* Callback on hourly cron job run
*/
public static function onCronHourlyRun(): void
{
Yii::$app->queue->push(new jobs\PublishScheduledContents());
}
}

View File

@ -21,6 +21,13 @@ abstract class AbstractActiveQueryContentContainer extends ActiveQuery
*/
const MAX_SEARCH_NEEDLES = 5;
/**
* During search, keyword will be walked through and each character of the set will be changed to another
* of the same set to create variants to maximise search.
* @var array
*/
protected $multiCharacterSearchVariants = [['\'', '', '`'], ['"', '”', '“']];
/**
* Filter query by visible records for the given or current user
*
@ -29,36 +36,40 @@ abstract class AbstractActiveQueryContentContainer extends ActiveQuery
*/
abstract public function visible(?User $user = null): ActiveQuery;
/**
* Performs a container text search
*
* @param string|array $keywords
* @param array|null $fields if empty the fields will be used from the method getSearchableFields()
* @return ActiveQuery
*/
abstract public function search($keywords, ?array $fields = null): ActiveQuery;
/**
* Returns a list of fields to be included in a container search.
* If additional tables are needed, they must be added via `joinWith`.
*
* @return array
*/
abstract protected function getSearchableFields(): array;
/**
* Filter this query by keyword
* Performs a container text search
*
* @param string $keyword
* @param string $keywords
* @param array|null $fields if empty the fields will be used from the method getSearchableFields()
* @return ActiveQuery
* @return self
*/
abstract public function searchKeyword(string $keyword, ?array $fields = null): ActiveQuery;
public function search(string $keywords, ?array $fields = null): ActiveQuery
{
if (empty($keywords)) {
return $this;
}
foreach ($this->setUpKeywords($keywords) as $keyword) {
$this->searchKeyword($keyword, $fields);
}
return $this;
}
/**
* @param string|array $keywords
* @return array
*/
protected function setUpKeywords($keywords): array
private function setUpKeywords($keywords): array
{
if (!is_array($keywords)) {
$keywords = explode(' ', $keywords);
@ -66,4 +77,58 @@ abstract class AbstractActiveQueryContentContainer extends ActiveQuery
return array_slice($keywords, 0, static::MAX_SEARCH_NEEDLES);
}
/**
* @return self
*/
private function searchKeyword(string $keyword, ?array $fields = null): ActiveQuery
{
if (empty($fields)) {
$fields = $this->getSearchableFields();
}
$conditions = [];
foreach ($this->prepareKeywordVariants($keyword) as $variant) {
$subConditions = [];
foreach ($fields as $field) {
$subConditions[] = ['LIKE', $field, $variant];
}
$conditions[] = array_merge(['OR'], $subConditions);
}
return $this->andWhere(array_merge(['OR'], $conditions));
}
/**
* This function will look through keyword and prepare other variants of the words according to config
* This is used to search for different apostrophes and quotes characters as for now.
* Example: word "o'Surname", will create array ["o'Surname", "oSurname", "o`Surname"]
*
* @param $keyword
* @return array
*/
private function prepareKeywordVariants($keyword): array
{
$variants = [$keyword];
foreach ($this->multiCharacterSearchVariants as $set) {
foreach ($set as $character) {
if (strpos($keyword, $character) === false) {
continue;
}
foreach ($set as $replaceWithCharacter) {
if ($character === $replaceWithCharacter) {
continue;
}
$variants[] = str_replace($character, $replaceWithCharacter, $keyword);
}
}
}
return $variants;
}
}

View File

@ -8,12 +8,14 @@
namespace humhub\modules\content\components;
use humhub\modules\content\models\Content;
use humhub\modules\content\models\ContentTag;
use humhub\modules\content\models\ContentTagRelation;
use humhub\modules\space\models\Space;
use humhub\modules\user\helpers\AuthHelper;
use humhub\modules\user\models\User;
use Yii;
use yii\db\ActiveQuery;
use yii\db\Expression;
/**
@ -23,9 +25,8 @@ use yii\db\Expression;
*
* @author luke
*/
class ActiveQueryContent extends \yii\db\ActiveQuery
class ActiveQueryContent extends ActiveQuery
{
/**
* Own content scope for userRelated
* @see ActiveQueryContent::userRelated
@ -36,6 +37,21 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
const USER_RELATED_SCOPE_FOLLOWED_USERS = 4;
const USER_RELATED_SCOPE_OWN_PROFILE = 5;
/**
* State filter that is used for queries. By default, only Published content is returned.
*
* Example to include drafts:
* ```
* $query = Post::find();
* $query->stateFilterCondition[] = ['content.state' => Content::STATE_DRAFT];
* $posts = $query->readable()->all();
* ```
*
* @since 1.14
* @var array
*/
public $stateFilterCondition = ['OR', ['content.state' => Content::STATE_PUBLISHED]];
/**
* Only returns user readable records
*
@ -49,8 +65,9 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
$user = Yii::$app->user->getIdentity();
}
$this->joinWith(['content', 'content.contentContainer', 'content.createdBy']);
$this->andWhere($this->stateFilterCondition);
$this->joinWith(['content', 'content.contentContainer', 'content.createdBy']);
$this->leftJoin('space', 'contentcontainer.pk=space.id AND contentcontainer.class=:spaceClass', [':spaceClass' => Space::class]);
$this->leftJoin('user cuser', 'contentcontainer.pk=cuser.id AND contentcontainer.class=:userClass', [':userClass' => User::class]);
$conditionSpace = '';
@ -60,19 +77,23 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
if ($user !== null) {
$this->leftJoin('space_membership', 'contentcontainer.pk=space_membership.space_id AND contentcontainer.class=:spaceClass AND space_membership.user_id=:userId', [':userId' => $user->id, ':spaceClass' => Space::class]);
if ($user->canViewAllContent()) {
if ($user->canViewAllContent(Space::class)) {
// Don't restrict if user can view all content:
$conditionSpaceMembershipRestriction = '';
$conditionUserPrivateRestriction = '';
} else {
// User must be a space's member OR Space and Content are public
$conditionSpaceMembershipRestriction = ' AND ( space_membership.status=3 OR (content.visibility=1 AND space.visibility != 0) )';
}
if ($user->canViewAllContent(User::class)) {
// Don't restrict if user can view all content:
$conditionUserPrivateRestriction = '';
} else {
// User can view only content of own profile
$conditionUserPrivateRestriction = ' AND content.contentcontainer_id=' . $user->contentcontainer_id;
}
// Build Access Check based on Space Content Container
$conditionSpace = 'space.id IS NOT NULL' . $conditionSpaceMembershipRestriction; // space content
$conditionSpace = 'space.id IS NOT NULL' . $conditionSpaceMembershipRestriction;
// Build Access Check based on User Content Container
$conditionUser = 'cuser.id IS NOT NULL AND ('; // user content
@ -139,7 +160,7 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
$contentTagQuery = ContentTagRelation::find()->select('content_id');
$contentTagQuery->andWhere(['content_tag_relation.tag_id' => $contentTag->id]);
$contentTagQuery->andWhere('content_tag_relation.content_id=content.id');
$this->andWhere(['content.id' =>$contentTagQuery]);
$this->andWhere(['content.id' => $contentTagQuery]);
}
} else if ($mode == 'OR') {
$names = array_map(function ($v) {
@ -182,6 +203,10 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
public function userRelated($scopes = [], $user = null)
{
if ($user === null) {
if ( Yii::$app->user->isGuest) {
return $this->andWhere('false');
}
$user = Yii::$app->user->getIdentity();
}

View File

@ -158,13 +158,13 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
*/
public function __construct($contentContainer = [], $visibility = null, $config = [])
{
if(is_array($contentContainer)) {
if (is_array($contentContainer)) {
parent::__construct($contentContainer);
} elseif($contentContainer instanceof ContentContainerActiveRecord) {
} elseif ($contentContainer instanceof ContentContainerActiveRecord) {
$this->content->setContainer($contentContainer);
if(is_array($visibility)) {
if (is_array($visibility)) {
$config = $visibility;
} elseif($visibility !== null) {
} elseif ($visibility !== null) {
$this->content->visibility = $visibility;
}
parent::__construct($config);
@ -194,7 +194,7 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
if ($name === 'content') {
$content = parent::__get('content');
if(!$content) {
if (!$content) {
$content = new Content();
$content->setPolymorphicRelation($this);
$this->populateRelation('content', $content);
@ -248,7 +248,7 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
$labels[] = Label::danger(Yii::t('ContentModule.base', 'Pinned'))->icon('fa-map-pin')->sortOrder(100);
}
if($this->content->isArchived()) {
if ($this->content->isArchived()) {
$labels[] = Label::warning(Yii::t('ContentModule.base', 'Archived'))->icon('fa-archive')->sortOrder(200);
}
@ -285,9 +285,9 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
/**
* Returns the $createPermission settings interpretable by an PermissionManager instance.
*
* @since 1.13
* @see ContentActiveRecord::$createPermission
* @return null|object|string
* @see ContentActiveRecord::$createPermission
* @since 1.13
*/
public function getCreatePermission()
{
@ -299,8 +299,8 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
/**
* Determines whether or not the record has an additional createPermission set.
*
* @since 1.13
* @return boolean
* @since 1.13
*/
public function hasCreatePermission()
{
@ -310,9 +310,9 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
/**
* Returns the $managePermission settings interpretable by an PermissionManager instance.
*
* @since 1.2.1
* @see ContentActiveRecord::$managePermission
* @return null|object|string
* @see ContentActiveRecord::$managePermission
* @since 1.2.1
*/
public function getManagePermission()
{
@ -324,10 +324,10 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
/**
* Returns the permission value interpretable by an PermissionManager instance.
*
* @since 1.13
* @see ContentActiveRecord::$managePermission, ContentActiveRecord::$createPermission
* @param string|array|null
* @return null|object|string
* @since 1.13
* @see ContentActiveRecord::$managePermission, ContentActiveRecord::$createPermission
*/
private function getPermissionValue($perm)
{
@ -337,7 +337,7 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
if (is_array($perm)) {
if (isset($perm['class'])) { // ['class' => '...', 'callback' => '...']
$handler = $perm['class'].'::'.$perm['callback'];
$handler = $perm['class'] . '::' . $perm['callback'];
return call_user_func($handler, $this);
}
// Simple Permission array specification
@ -358,8 +358,8 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
/**
* Determines weather or not this records has an additional managePermission set.
*
* @since 1.2.1
* @return boolean
* @since 1.2.1
*/
public function hasManagePermission()
{
@ -370,18 +370,18 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
* Returns the wall output widget of this content.
*
* @param array $params optional parameters for WallEntryWidget
* @deprecated since 1.7 use StreamEntryWidget::renderStreamEntry()
* @return string
* @deprecated since 1.7 use StreamEntryWidget::renderStreamEntry()
*/
public function getWallOut($params = [])
{
if(is_subclass_of($this->wallEntryClass, StreamEntryWidget::class, true)) {
if (is_subclass_of($this->wallEntryClass, StreamEntryWidget::class, true)) {
$params['model'] = $this;
} else if(!empty($this->wallEntryClass)) {
} else if (!empty($this->wallEntryClass)) {
$params['contentObject'] = $this; // legacy WallEntry widget
}
return call_user_func($this->wallEntryClass.'::widget', $params);
return call_user_func($this->wallEntryClass . '::widget', $params);
}
/**
@ -394,18 +394,18 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
*/
public function getWallEntryWidget()
{
if(empty($this->wallEntryClass)) {
if (empty($this->wallEntryClass)) {
return null;
}
if (is_subclass_of($this->wallEntryClass, WallEntry::class) ) {
if (is_subclass_of($this->wallEntryClass, WallEntry::class)) {
$class = $this->wallEntryClass;
$widget = new $class;
$widget->contentObject = $this;
return $widget;
}
if($this->wallEntryClass) {
if ($this->wallEntryClass) {
$class = $this->wallEntryClass;
$widget = new $class(['model' => $this]);
return $widget;
@ -470,10 +470,33 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
*
* @return string
*/
public static function getObjectModel() {
public static function getObjectModel()
{
return static::class;
}
/**
* Marks this content for deletion (soft delete).
* Use `hardDelete()` method to delete record immediately.
*
* @return bool|int
*/
public function delete()
{
return $this->content->softDelete();
}
/**
* Deletes this content record immediately and permanently
*
* @return bool
* @since 1.14
*/
public function hardDelete(): bool
{
return (parent::delete() !== false);
}
/**
* @inheritdoc
*/
@ -499,8 +522,8 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
* Checks if the given user or the current logged in user if no user was given, is the owner of this content
* @param null $user
* @return bool
* @since 1.3
* @throws \Throwable
* @since 1.3
*/
public function isOwner($user = null)
{
@ -569,13 +592,13 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
*/
public function canMove(ContentContainerActiveRecord $container = null)
{
if(!$this->canMove) {
if (!$this->canMove) {
return Yii::t('ContentModule.base', 'This content type can\'t be moved.');
}
if($container && is_string($this->canMove) && is_subclass_of($this->canMove, BasePermission::class)) {
if ($container && is_string($this->canMove) && is_subclass_of($this->canMove, BasePermission::class)) {
$ownerPermissions = $container->getPermissionManager($this->content->createdBy);
if(!$ownerPermissions->can($this->canMove)) {
if (!$ownerPermissions->can($this->canMove)) {
return Yii::t('ContentModule.base', 'The author of this content is not allowed to create this type of content within this space.');
}
}
@ -596,5 +619,7 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
* in order to define model specific logic as moving sub-content or other related.
* @param ContentContainerActiveRecord|null $container
*/
public function afterMove(ContentContainerActiveRecord $container = null) {}
public function afterMove(ContentContainerActiveRecord $container = null)
{
}
}

View File

@ -1,8 +1,8 @@
<?php
use humhub\commands\CronController;
use humhub\modules\content\Events;
use humhub\commands\IntegrityController;
use humhub\modules\content\widgets\WallEntryControls;
use humhub\modules\content\widgets\WallEntryAddons;
use humhub\modules\user\models\User;
use humhub\modules\space\models\Space;
@ -23,6 +23,7 @@ return [
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_INSERT, 'callback' => [Events::class, 'onContentActiveRecordSave']],
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_UPDATE, 'callback' => [Events::class, 'onContentActiveRecordSave']],
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_DELETE, 'callback' => [Events::class, 'onContentActiveRecordDelete']],
['class' => CronController::class, 'event' => CronController::EVENT_ON_DAILY_RUN, 'callback' => [Events::class, 'onCronDailyRun']],
['class' => CronController::class, 'event' => CronController::EVENT_ON_HOURLY_RUN, 'callback' => [Events::class, 'onCronHourlyRun']]
],
];
?>

View File

@ -12,19 +12,16 @@ use humhub\components\behaviors\AccessControl;
use humhub\components\Controller;
use humhub\modules\content\models\Content;
use humhub\modules\content\models\forms\AdminDeleteContentForm;
use humhub\modules\content\models\forms\ScheduleOptionsForm;
use humhub\modules\content\Module;
use humhub\modules\content\notifications\ContentDeleted;
use humhub\modules\content\permissions\CreatePublicContent;
use humhub\modules\content\widgets\AdminDeleteModal;
use humhub\modules\stream\actions\StreamEntryResponse;
use Yii;
use yii\base\BaseObject;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\web\BadRequestHttpException;
use yii\web\ForbiddenHttpException;
use yii\web\HttpException;
use yii\web\NotAcceptableHttpException;
use yii\web\NotFoundHttpException;
use yii\web\Response;
@ -57,17 +54,15 @@ class ContentController extends Controller
*/
public function actionDelete()
{
Yii::$app->response->format = 'json';
$this->forcePostRequest();
$model = Yii::$app->request->get('model');
// Due to backward compatibility we use the old delete mechanism in case a model parameter is provided
$id = (int)($model != null) ? Yii::$app->request->get('id') : Yii::$app->request->post('id');
$id = $model ? Yii::$app->request->get('id') : Yii::$app->request->post('id');
/* @var $contentObjs Content */
$contentObj = ($model != null) ? Content::Get($model, $id) : Content::findOne(['id' => $id]);
/* @var $contentObj Content */
$contentObj = $model ? Content::Get($model, $id) : Content::findOne(['id' => $id]);
if (!$contentObj) {
throw new NotFoundHttpException();
@ -77,37 +72,19 @@ class ContentController extends Controller
throw new ForbiddenHttpException();
}
if ($contentObj !== null) {
$form = new AdminDeleteContentForm();
$form = new AdminDeleteContentForm(['content' => $contentObj]);
$form->load(Yii::$app->request->post());
if ($form->load(Yii::$app->request->post())) {
if (!$form->validate()) {
throw new BadRequestHttpException();
}
if ($form->notify) {
$contentDeleted = ContentDeleted::instance()
->from(Yii::$app->user->getIdentity())
->payload(['contentTitle' => (new ContentDeleted)->getContentPlainTextInfo($contentObj), 'reason' => $form->message]);
$contentDeleted->saveRecord($contentObj->createdBy);
$contentDeleted->record->updateAttributes([
'send_web_notifications' => 1
]);
}
}
$json = [
'success' => $contentObj->delete(),
'uniqueId' => $contentObj->getUniqueId(),
'model' => $model,
'pk' => $id
];
} else {
throw new HttpException(500, Yii::t('ContentModule.base', 'Could not delete content!'));
if (!$form->delete()) {
return $this->asJson(['error' => $form->getErrorsAsString()]);
}
return $json;
return $this->asJson([
'success' => true,
'uniqueId' => $contentObj->getUniqueId(),
'model' => $model,
'pk' => $id
]);
}
/**
@ -126,7 +103,7 @@ class ContentController extends Controller
}
if (!$contentObj->canEdit()) {
throw new HttpException(400);
throw new ForbiddenHttpException();
}
return [
@ -192,9 +169,7 @@ class ContentController extends Controller
{
$this->forcePostRequest();
$post = Yii::$app->request->post();
$content = Content::findOne(['id' => $post['id']]);
$content = Content::findOne(['id' => Yii::$app->request->post('id')]);
if (!$content) {
throw new NotFoundHttpException();
@ -204,26 +179,14 @@ class ContentController extends Controller
throw new ForbiddenHttpException();
}
$form = new AdminDeleteContentForm();
$form = new AdminDeleteContentForm(['content' => $content]);
$form->load(Yii::$app->request->post());
if ($form->load($post)) {
if (!$form->validate()) {
throw new BadRequestHttpException();
}
if($form->notify) {
$contentDeleted = ContentDeleted::instance()
->from(Yii::$app->user->getIdentity())
->payload(['contentTitle' => (new ContentDeleted)->getContentPlainTextInfo($content), 'reason' => $form->message]);
$contentDeleted->saveRecord($content->createdBy);
$contentDeleted->record->updateAttributes([
'send_web_notifications' => 1
]);
}
if (!$form->delete()) {
return $this->asJson(['error' => $form->getErrorsAsString()]);
}
return $this->asJson(['success' => $content->delete()]);
return $this->asJson(['success' => true]);
}
public function actionReload($id)
@ -231,11 +194,11 @@ class ContentController extends Controller
$content = Content::findOne(['id' => $id]);
if (!$content) {
throw new HttpException(400, Yii::t('ContentModule.base', 'Invalid content id given!'));
throw new NotFoundHttpException(Yii::t('ContentModule.base', 'Invalid content id given!'));
}
if (!$content->canView()) {
throw new HttpException(403);
throw new ForbiddenHttpException();
}
return StreamEntryResponse::getAsJson($content);
@ -244,10 +207,9 @@ class ContentController extends Controller
/**
* Switches the content visibility for the given content.
*
* @param type $id content id
* @param int $id content id
* @return Response
* @throws Exception
* @throws HttpException
* @throws InvalidConfigException
* @throws \Throwable
* @throws \yii\db\IntegrityException
@ -258,11 +220,11 @@ class ContentController extends Controller
$content = Content::findOne(['id' => $id]);
if (!$content) {
throw new HttpException(400, Yii::t('ContentModule.base', 'Invalid content id given!'));
throw new NotFoundHttpException(Yii::t('ContentModule.base', 'Invalid content id given!'));
} elseif (!$content->canEdit()) {
throw new HttpException(403);
throw new ForbiddenHttpException();
} elseif ($content->isPrivate() && !$content->container->permissionManager->can(new CreatePublicContent())) {
throw new HttpException(403);
throw new ForbiddenHttpException();
}
if ($content->isPrivate()) {
@ -284,7 +246,6 @@ class ContentController extends Controller
* @param bool $lockComments True to lock comments, False to unlock
* @return Response
* @throws Exception
* @throws HttpException
* @throws InvalidConfigException
* @throws \Throwable
* @throws \yii\db\IntegrityException
@ -295,9 +256,9 @@ class ContentController extends Controller
$content = Content::findOne(['id' => $id]);
if (!$content) {
throw new HttpException(400, Yii::t('ContentModule.base', 'Invalid content id given!'));
throw new NotFoundHttpException(Yii::t('ContentModule.base', 'Invalid content id given!'));
} elseif (!$content->canLockComments()) {
throw new HttpException(403);
throw new ForbiddenHttpException();
}
$content->locked_comments = $lockComments;
@ -313,7 +274,6 @@ class ContentController extends Controller
* @param int $id Content id
* @return Response
* @throws Exception
* @throws HttpException
* @throws InvalidConfigException
* @throws \Throwable
* @throws \yii\db\IntegrityException
@ -329,7 +289,6 @@ class ContentController extends Controller
* @param int $id Content id
* @return Response
* @throws Exception
* @throws HttpException
* @throws InvalidConfigException
* @throws \Throwable
* @throws \yii\db\IntegrityException
@ -345,7 +304,6 @@ class ContentController extends Controller
* Returns JSON Output.
* @return Response
* @throws ForbiddenHttpException
* @throws HttpException
* @throws NotFoundHttpException
* @throws Exception
* @throws InvalidConfigException
@ -402,6 +360,25 @@ class ContentController extends Controller
return $this->asJson($json);
}
public function actionPublishDraft()
{
$this->forcePostRequest();
$json = [];
$json['success'] = false;
$content = Content::findOne(['id' => Yii::$app->request->get('id', '')]);
if ($content !== null && $content->canEdit() && $content->state === Content::STATE_DRAFT) {
$content->state = Content::STATE_PUBLISHED;
$content->save();
$json['message'] = Yii::t('ContentModule.base', 'The content has been successfully published.');
$json['success'] = true;
}
return $this->asJson($json);
}
public function actionNotificationSwitch()
{
$this->forcePostRequest();
@ -419,4 +396,29 @@ class ContentController extends Controller
return $this->asJson($json);
}
public function actionScheduleOptions($id = null)
{
$this->forcePostRequest();
$content = $id ? Content::findOne($id) : null;
if ($content instanceof Content && !$content->canEdit()) {
throw new ForbiddenHttpException();
}
$scheduleOptions = new ScheduleOptionsForm(['content' => $content]);
if ($scheduleOptions->load(Yii::$app->request->post())) {
// Disable in order to don't focus the date field because modal window will be closed anyway
$disableInputs = $scheduleOptions->save();
} else {
$disableInputs = !$scheduleOptions->enabled;
}
return $this->renderAjax('scheduleOptions', [
'scheduleOptions' => $scheduleOptions,
'disableInputs' => $disableInputs
]);
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\jobs;
use DateTime;
use DateTimeZone;
use humhub\modules\content\models\Content;
use humhub\modules\queue\ActiveJob;
class PublishScheduledContents extends ActiveJob
{
/**
* @inheritdoc
*/
public function run()
{
$now = new DateTime('now', new DateTimeZone('UTC'));
/* @var Content[] $contents*/
$contents = Content::find()
->where(['state' => Content::STATE_SCHEDULED])
->andWhere(['<=', 'scheduled_at', $now->format('Y-m-d H:i:s')])
->all();
foreach ($contents as $content) {
$content->setState(Content::STATE_PUBLISHED);
$content->save();
}
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\jobs;
use humhub\modules\content\models\Content;
use humhub\modules\queue\ActiveJob;
class PurgeDeletedContents extends ActiveJob
{
/**
* @inheritdoc
*/
public function run()
{
foreach (Content::findAll(['content.state' => Content::STATE_DELETED]) as $content) {
$content->delete();
}
}
}

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'አገልግሎት ይሰጣል',
'Configure' => 'አዋቅር',
'Enable' => 'እንዲጠቀሙ አድርግ',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'تم التفعيل',
'Configure' => 'إعدادات',
'Enable' => 'تفعيل',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Активиран',
'Configure' => 'Настройване',
'Enable' => 'Активиране',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Activat',
'Configure' => 'Configura',
'Enable' => 'Habilita',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktivováno',
'Configure' => 'Nastavit',
'Enable' => 'Dostupné',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktiveret',
'Configure' => 'Konfigurer',
'Enable' => 'Aktiver',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktiviert',
'Configure' => 'Konfigurieren',
'Enable' => 'Aktivieren',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Ενεργοποιήθηκε',
'Configure' => 'Ρύθμιση',
'Enable' => 'Ενεργοποίηση',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Activado',
'Configure' => 'Configurar',
'Enable' => 'Habilitar',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'فعال شد',
'Configure' => 'تنظیم',
'Enable' => 'فعال',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktivoitu',
'Configure' => 'Hallitse',
'Enable' => 'Ota käyttöön',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Activé',
'Configure' => 'Configurer',
'Enable' => 'Activer',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'הופעל',
'Configure' => '',
'Enable' => 'פעיל',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktivirano',
'Configure' => 'Konfiguriraj',
'Enable' => 'Omogući',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktiválva',
'Configure' => 'Konfiguráció',
'Enable' => 'Engedélyezés',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => '',
'Configure' => '',
'Enable' => 'Aktifkan',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Attivato',
'Configure' => 'Configura',
'Enable' => 'Abilita',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => '作動中',
'Configure' => '設定',
'Enable' => '有効化',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktyvuota',
'Configure' => 'Konfigūruoti',
'Enable' => 'Įgalinti',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktivizēti',
'Configure' => '',
'Enable' => '',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktivert',
'Configure' => 'Konfigurer',
'Enable' => 'Aktiver',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Geactiveerd',
'Configure' => 'Configureren',
'Enable' => 'Inschakelen',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktywowane',
'Configure' => 'Konfiguruj',
'Enable' => 'Włącz',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Ativado',
'Configure' => 'Configurar',
'Enable' => 'Habilitar',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Ativado',
'Configure' => 'Configurar',
'Enable' => 'Ativar',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Включен',
'Configure' => 'Настройки',
'Enable' => 'Включить',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktiverad',
'Configure' => 'Konfigurera',
'Enable' => 'Aktivera',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'เปิดใช้งานแล้ว',
'Configure' => 'กำหนดค่า',
'Enable' => 'เปิดใช้งาน',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Aktif',
'Configure' => 'Yapılandırma',
'Enable' => 'Etkinleştirme',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => 'Đã được kích hoạt',
'Configure' => 'Cấu hình',
'Enable' => 'Kích hoạt',
);

View File

@ -1,6 +0,0 @@
<?php
return array (
'Activated' => '启用',
'Configure' => '配置',
'Enable' => '生效',
);

View File

@ -1,7 +0,0 @@
<?php
return [
'Activated' => '',
'Configure' => '',
'Enable' => '',
];

View File

@ -0,0 +1,46 @@
<?php
use yii\db\Migration;
/**
* Class m230127_195245_content_state
*/
class m230127_195245_content_state extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->addColumn(
'content',
'state',
$this->tinyInteger()->defaultValue(1)->notNull()->after('visibility')
);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
echo "m230127_195245_content_state cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m230127_195245_content_state cannot be reverted.\n";
return false;
}
*/
}

View File

@ -0,0 +1,29 @@
<?php
use humhub\components\Migration;
use humhub\modules\content\models\Content;
/**
* Class m230217_112400_content_scheduled_at
*/
class m230217_112400_content_scheduled_at extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->safeAddColumn(Content::tableName(), 'scheduled_at', $this
->dateTime()
->null()
->after('locked_comments'));
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->safeDropColumn(Content::tableName(), 'scheduled_at');
}
}

View File

@ -12,6 +12,8 @@ use humhub\components\ActiveRecord;
use humhub\components\behaviors\GUID;
use humhub\components\behaviors\PolymorphicRelation;
use humhub\components\Module;
use humhub\libs\DbDateValidator;
use humhub\modules\activity\helpers\ActivityHelper;
use humhub\modules\admin\permissions\ManageUsers;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\components\ContentContainerActiveRecord;
@ -21,6 +23,7 @@ use humhub\modules\content\live\NewContent;
use humhub\modules\content\permissions\CreatePrivateContent;
use humhub\modules\content\permissions\CreatePublicContent;
use humhub\modules\content\permissions\ManageContent;
use humhub\modules\notification\models\Notification;
use humhub\modules\search\libs\SearchHelper;
use humhub\modules\space\models\Space;
use humhub\modules\user\components\PermissionManager;
@ -29,7 +32,6 @@ use humhub\modules\user\models\User;
use Yii;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\db\Expression;
use yii\db\IntegrityException;
use yii\helpers\Url;
@ -59,11 +61,13 @@ use yii\helpers\Url;
* @property integer $visibility
* @property integer $pinned
* @property integer $archived
* @property integer $state
* @property integer $locked_comments
* @property string $created_at
* @property integer $created_by
* @property string $updated_at
* @property integer $updated_by
* @property string $scheduled_at
* @property string $stream_sort_date
* @property string $stream_channel
* @property integer $contentcontainer_id;
@ -103,6 +107,14 @@ class Content extends ActiveRecord implements Movable, ContentOwner
*/
const VISIBILITY_OWNER = 2;
/**
* Content States - By default, only content with the "Published" state is returned.
*/
const STATE_PUBLISHED = 1;
const STATE_DRAFT = 10;
const STATE_SCHEDULED = 20;
const STATE_DELETED = 100;
/**
* @var ContentContainerActiveRecord the Container (e.g. Space or User) where this content belongs to.
*/
@ -191,21 +203,13 @@ class Content extends ActiveRecord implements Movable, ContentOwner
throw new Exception("Could not save content with object_model or object_id!");
}
// Set some default values
if (!$this->archived) {
$this->archived = 0;
}
if (!$this->visibility) {
$this->visibility = self::VISIBILITY_PRIVATE;
}
if (!$this->pinned) {
$this->pinned = 0;
}
$this->archived ??= 0;
$this->visibility ??= self::VISIBILITY_PRIVATE;
$this->pinned ??= 0;
$this->state ??= Content::STATE_PUBLISHED;
if ($insert) {
if ($this->created_by == "") {
$this->created_by = Yii::$app->user->id;
}
$this->created_by ??= Yii::$app->user->id;
}
$this->stream_sort_date = date('Y-m-d G:i:s');
@ -222,15 +226,38 @@ class Content extends ActiveRecord implements Movable, ContentOwner
*/
public function afterSave($insert, $changedAttributes)
{
/* @var $contentSource ContentActiveRecord */
$contentSource = $this->getModel();
if (// New Content with State Published:
($insert && $this->state == Content::STATE_PUBLISHED) ||
// Content Updated from Draft to Published
(array_key_exists('state', $changedAttributes) &&
$this->state == Content::STATE_PUBLISHED &&
$changedAttributes['state'] == Content::STATE_DRAFT
)) {
$this->processNewContent();
}
if ($this->state === static::STATE_PUBLISHED) {
SearchHelper::queueUpdate($this->getModel());
} else {
SearchHelper::queueDelete($this->getModel());
}
parent::afterSave($insert, $changedAttributes);
}
private function processNewContent()
{
$record = $this->getModel();
Yii::debug('Process new content: ' . get_class($record) . ' ID: ' . $record->getPrimaryKey(), 'content');
foreach ($this->notifyUsersOfNewContent as $user) {
$contentSource->follow($user->id);
$record->follow($user->id);
}
// TODO: handle ContentCreated notifications and live events for global content
if ($insert && !$this->isMuted()) {
if (!$this->isMuted()) {
$this->notifyContentCreated();
}
@ -241,20 +268,17 @@ class Content extends ActiveRecord implements Movable, ContentOwner
'originator' => $this->createdBy->guid,
'contentContainerId' => $this->container->contentContainerRecord->id,
'visibility' => $this->visibility,
'sourceClass' => get_class($contentSource),
'sourceId' => $contentSource->getPrimaryKey(),
'sourceClass' => get_class($record),
'sourceId' => $record->getPrimaryKey(),
'silent' => $this->isMuted(),
'streamChannel' => $this->stream_channel,
'contentId' => $this->id,
'insert' => $insert
'insert' => true
]));
}
SearchHelper::queueUpdate($contentSource);
parent::afterSave($insert, $changedAttributes);
}
/**
* @return bool checks if the given content allows content creation notifications and activities
* @throws IntegrityException
@ -298,8 +322,12 @@ class Content extends ActiveRecord implements Movable, ContentOwner
{
// Try delete the underlying object (Post, Question, Task, ...)
$this->resetPolymorphicRelation();
if ($this->getPolymorphicRelation() !== null) {
$this->getPolymorphicRelation()->delete();
/** @var ContentActiveRecord $record */
$record = $this->getPolymorphicRelation();
if ($record) {
$record->hardDelete();
}
parent::afterDelete();
@ -859,13 +887,18 @@ class Content extends ActiveRecord implements Movable, ContentOwner
return $this->checkGuestAccess();
}
// If content is draft, in trash, unapproved - restrict view access to editors
if ($this->state !== static::STATE_PUBLISHED) {
return $this->canEdit();
}
// Public visible content
if ($this->isPublic()) {
return true;
}
// Check system admin can see all content module configuration
if ($user->canViewAllContent()) {
if ($user->canViewAllContent(get_class($this->container))) {
return true;
}
@ -948,4 +981,74 @@ class Content extends ActiveRecord implements Movable, ContentOwner
{
return $this->created_at !== $this->updated_at && !empty($this->updated_at) && is_string($this->updated_at);
}
/**
* Marks the content as deleted.
*
* Content which are marked as deleted will not longer returned in queries/stream/search.
* A cron job will remove these content permanently.
* If installed, such content can also be restored using the `recycle-bin` module.
*
* @return bool
* @since 1.14
*/
public function softDelete(): bool
{
ActivityHelper::deleteActivitiesForRecord($this->getModel());
Notification::deleteAll([
'source_class' => get_class($this),
'source_pk' => $this->getPrimaryKey(),
]);
$this->setState(self::STATE_DELETED);
return $this->save();
}
public static function getAllowedStates(): array
{
return [
self::STATE_PUBLISHED,
self::STATE_DRAFT,
self::STATE_SCHEDULED,
self::STATE_DELETED
];
}
/**
* @param int|string|null $state
* @return bool
* @since 1.14
*/
public function canChangeState($state): bool
{
return in_array($state, self::getAllowedStates());
}
/**
* @param int|string|null $state
* @param array $options Additional options depending on state
* @since 1.14
*/
public function setState($state, array $options = [])
{
if (!$this->canChangeState($state)) {
return;
}
if ((int) $state === self::STATE_SCHEDULED) {
if (empty($options['scheduled_at'])) {
return;
}
$this->scheduled_at = $options['scheduled_at'];
(new DbDateValidator())->validateAttribute($this, 'scheduled_at');
if ($this->hasErrors('scheduled_at')) {
$this->scheduled_at = null;
return;
}
}
$this->state = $state;
}
}

View File

@ -2,8 +2,8 @@
namespace humhub\modules\content\models\forms;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\content\notifications\ContentDeleted;
use Yii;
use yii\base\Model;
@ -12,6 +12,11 @@ use yii\base\Model;
*/
class AdminDeleteContentForm extends Model
{
/**
* @var Content
*/
public $content;
/**
* @var string
*/
@ -43,4 +48,46 @@ class AdminDeleteContentForm extends Model
'notify' => Yii::t('CommentModule.base', 'Send a notification to author')
];
}
public function delete(): bool
{
if (!$this->validate()) {
return false;
}
if (!$this->notify()) {
return false;
}
return (bool) $this->content->softDelete();
}
public function notify(): bool
{
if (!$this->notify) {
return true;
}
$contentDeleted = ContentDeleted::instance()
->from(Yii::$app->user->getIdentity())
->payload([
'contentTitle' => (new ContentDeleted)->getContentPlainTextInfo($this->content),
'reason' => $this->message
]);
if (!$contentDeleted->saveRecord($this->content->createdBy)) {
$this->addError('message', Yii::t('ContentModule.base', 'Cannot notify the author.'));
return false;
}
$contentDeleted->record->updateAttributes([
'send_web_notifications' => 1
]);
return true;
}
public function getErrorsAsString(): string
{
return implode(' ', $this->getErrorSummary(true));
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\models\forms;
use DateTime;
use DateTimeZone;
use humhub\libs\DbDateValidator;
use humhub\modules\content\models\Content;
use Yii;
use yii\base\Model;
class ScheduleOptionsForm extends Model
{
public ?Content $content = null;
public bool $enabled = false;
public ?string $date = null;
public ?string $time = null;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if ($this->hasContent() && $this->content->scheduled_at !== null) {
$this->enabled = $this->content->state == Content::STATE_SCHEDULED;
$this->date = $this->content->scheduled_at;
}
if ($this->date === null) {
$this->date = (new DateTime('tomorrow'))->format('Y-m-d H:i:s');
}
if ($this->time === null) {
$this->initTime();
}
}
private function initTime()
{
if ($this->date === null) {
$this->time = '';
} else {
$scheduledDateTime = new DateTime($this->date, new DateTimeZone('UTC'));
$this->time = Yii::$app->formatter->asTime($scheduledDateTime, 'short');
}
}
/**
* @inheritdoc
*/
public function rules()
{
return [
['enabled', 'boolean'],
['date', DbDateValidator::class, 'timeAttribute' => 'time'],
['time', 'date', 'type' => 'time', 'format' => Yii::$app->formatter->isShowMeridiem() ? 'h:mm a' : 'php:H:i']
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'enabled' => Yii::t('ContentModule.base', 'Activate scheduling')
];
}
public function load($data, $formName = null)
{
if (!parent::load($data, $formName)) {
return false;
}
if (!$this->isSubmitted() && !$this->hasContent()) {
$this->normalizeDate();
$this->initTime();
}
return true;
}
public function save(): bool
{
if (!$this->validate()) {
return false;
}
if ($this->hasContent()) {
if ($this->enabled) {
$this->content->setState(Content::STATE_SCHEDULED, ['scheduled_at' => $this->date]);
} else {
$this->content->setState(Content::STATE_DRAFT);
}
return $this->content->save();
}
return $this->isSubmitted();
}
public function getStateTitle(): string
{
return Yii::t('ContentModule.base', 'Scheduled at {dateTime}', [
'dateTime' => Yii::$app->formatter->asDatetime($this->date, 'short')
]);
}
public function hasContent(): bool
{
return $this->content instanceof Content && !$this->content->isNewRecord;
}
public function isSubmitted(): bool
{
return Yii::$app->request->post('state') == Content::STATE_SCHEDULED;
}
private function normalizeDate()
{
if ($this->date === null) {
return;
}
$datetime = new DateTime('now', new DateTimeZone('UTC'));
$datetime->setTimestamp(strtotime($this->date));
$this->date = $datetime->format('Y-m-d H:i:s');
}
}

View File

@ -11,6 +11,7 @@ humhub.module('content.form', function(module, require, $) {
var event = require('event');
var Widget = require('ui.widget').Widget;
var loader = require('ui.loader');
var modal = require('ui.modal');
var instance;
@ -28,16 +29,15 @@ humhub.module('content.form', function(module, require, $) {
this.setDefaultVisibility();
this.$.fadeIn('fast');
this.showMenu();
if(!module.config['disabled']) {
var that = this;
$('#contentFormBody').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;
}
that.showMenu();
$('.contentForm_options').fadeIn();
});
} else {
@ -96,6 +96,7 @@ humhub.module('content.form', function(module, require, $) {
this.setDefaultVisibility();
this.resetFilePreview();
this.resetFileUpload();
this.resetState();
$('#public').attr('checked', false);
$('#contentFormBody').find('.humhub-ui-richtext').trigger('clear');
@ -186,6 +187,93 @@ humhub.module('content.form', function(module, require, $) {
}
};
CreateForm.prototype.changeState = function(state, title) {
const stateInput = this.$.find('input[name=state]');
let stateLabel = this.$.find('.label-content-state');
if (!stateLabel.length) {
stateLabel = $('<span>').addClass('label label-warning label-content-state');
this.$.find('.label-container').append(stateLabel);
}
if (stateInput.data('initial') === undefined) {
stateInput.data('initial', stateInput.val());
}
if (typeof(state) === 'object') {
title = state.$target.data('state-title');
state = state.$target.data('state');
if (stateInput.val() == state) {
return this.resetState();
}
}
stateInput.val(state);
stateLabel.show().html(title);
}
CreateForm.prototype.resetState = function() {
const stateInput = this.$.find('input[name=state]');
if (stateInput.data('initial') !== undefined) {
stateInput.val(stateInput.data('initial'));
}
this.$.find('input[name^=scheduled]').remove();
this.$.find('.label-content-state').hide();
}
CreateForm.prototype.scheduleOptions = function(evt) {
const that = this;
const modalGlobal = modal.global.$;
const scheduledDate = that.$.find('input[name=scheduledDate]');
const data = {};
if (scheduledDate.length) {
data.ScheduleOptionsForm = {
enabled: 1,
date: scheduledDate.val()
};
}
modal.post(evt, {data}).then(function () {
modalGlobal.one('submitted', function () {
if (modalGlobal.find('.has-error').length) {
return;
}
if (modalGlobal.find('#scheduleoptionsform-enabled').is(':checked')) {
that.changeState(modalGlobal.find('input[name=state]').val(), modalGlobal.find('input[name=stateTitle]').val());
that.setScheduleOption('scheduledDate', modalGlobal.find('input[name=scheduledDate]').val());
} else {
that.resetState();
that.resetScheduleOption('scheduledDate');
}
modal.global.close(true);
});
}).catch(function (e) {
module.log.error(e, true);
});
}
CreateForm.prototype.setScheduleOption = function(name, value) {
let input = this.$.find('input[name=' + name + ']');
if (value === undefined) {
input.remove();
return;
}
if (!input.length) {
input = $('<input name="' + name + '" type="hidden">');
this.$.find('input[name=state]').after(input);
}
input.val(value);
}
CreateForm.prototype.resetScheduleOption = function(name) {
this.setScheduleOption(name);
}
const CreateFormMenu = Widget.extend();
CreateFormMenu.prototype.init = function() {

View File

@ -7,12 +7,12 @@
humhub.module('content', function (module, require, $) {
var client = require('client');
var util = require('util');
var object = util.object;
var string = util.string;
var actions = require('action');
var Component = actions.Component;
var event = require('event');
var modal = require('ui.modal');
var status = require('ui.status');
var DATA_CONTENT_KEY = "content-key";
var DATA_CONTENT_DELETE_URL = "content-delete-url";
@ -130,9 +130,13 @@ humhub.module('content', function (module, require, $) {
client.post(deleteUrl, {
data: postData
}).then(function (response) {
that.remove().then(function () {
resolve(true);
});
if (response.response.success) {
that.remove().then(function () {
resolve(true);
});
} else {
status.error(response.response.error);
}
}).catch(function (err) {
reject(err);
}).finally(function () {

View File

@ -10,6 +10,7 @@ humhub.module('ui.richtext.prosemirror', function(module, require, $) {
var client = require('client');
var Widget = require('ui.widget').Widget;
var additions = require('ui.additions');
var event = require('event');
var MarkdownEditor = prosemirror.MarkdownEditor;
var MentionProvider = prosemirror.MentionProvider;
@ -74,9 +75,8 @@ humhub.module('ui.richtext.prosemirror', function(module, require, $) {
})
if (this.options.backupInterval) {
setInterval(function () {
that.backup();
}, this.options.backupInterval * 1000);
setInterval(() => this.backup(), this.options.backupInterval * 1000);
event.on('humhub:content:afterSubmit', () => this.resetBackup());
}
};
@ -101,11 +101,14 @@ humhub.module('ui.richtext.prosemirror', function(module, require, $) {
return {};
}
RichTextEditor.prototype.backup = function() {
RichTextEditor.prototype.backup = function(currentValue) {
var inputId = this.getInput().attr('id');
var currentValue = this.editor.serialize();
var isBackuped = typeof this.backupedValue !== 'undefined';
if (typeof currentValue === 'undefined') {
currentValue = this.editor.serialize();
}
if (!isBackuped && currentValue === '') {
// Don't back up first empty value
return;
@ -132,6 +135,10 @@ humhub.module('ui.richtext.prosemirror', function(module, require, $) {
}
};
RichTextEditor.prototype.resetBackup = function() {
this.backup('');
}
RichTextEditor.prototype.focus = function() {
this.editor.view.focus();
};

View File

@ -19,3 +19,6 @@ modules:
url: 'http://localhost:8080/'
browser: chrome
port: 4444
capabilities:
chromeOptions:
args: [ "--lang=en-US" ]

View File

@ -0,0 +1,43 @@
<?php
use content\AcceptanceTester;
class DraftCest
{
public function testCreateDraftPost(AcceptanceTester $I)
{
$I->amSpaceAdmin(false, 3);
$I->wantTo('create a draft post.');
$I->waitForText('What\'s on your mind?');
$I->click('#contentFormBody .humhub-ui-richtext[contenteditable]');
$I->fillField('#contentFormBody .humhub-ui-richtext[contenteditable]', 'Some Schabernack');
$I->click('#contentFormBody ul.preferences');
$I->waitForText('Create as draft');
$I->click('Create as draft');
$I->waitForText('DRAFT', '10', '.label-container');
$I->click('#post_submit_button', '#contentFormBody');
$I->wantTo('ensure draft has a draft badge.');
$I->waitForText('DRAFT', '5', '//div[@class="wall-entry"][1]');
$I->wantTo('ensure draft is not visible for other users.');
$I->amUser2(true);
$I->amOnSpace3();
$I->dontSee('Schabernack');
$I->wantTo('publish draft');
$I->amSpaceAdmin(true, 3);
$I->waitForText('Schabernack');
$I->click('//div[@class="wall-entry"][1]//ul[contains(@class, "preferences")]');
$I->waitForText('Publish draft', '5');
$I->click('Publish draft');
$I->waitForText('The content has been successfully published.');
$I->dontSee('DRAFT');
$I->wantTo('ensure published draft is now visible for other users.');
$I->amUser2(true);
$I->amOnSpace3();
$I->waitForText('Schabernack');
}
}

View File

@ -0,0 +1,72 @@
<?php
use content\AcceptanceTester;
class ScheduledCest
{
const DATE_FORMAT = 'short';
public function testCreateDraftPost(AcceptanceTester $I)
{
$I->amSpaceAdmin(false, 3);
$I->wantTo('create a scheduled post.');
$I->waitForText('What\'s on your mind?');
$I->click('#contentFormBody .humhub-ui-richtext[contenteditable]');
$postContent = 'Sample text for a scheduled post';
$I->fillField('#contentFormBody .humhub-ui-richtext[contenteditable]', $postContent);
$I->click('#contentFormBody ul.preferences');
$datetime = (new Datetime('tomorrow'))->setTime(19, 15);
$this->updateSchedulingOptions($I, $datetime);
$I->click('#post_submit_button', '#contentFormBody');
$I->wantTo('ensure the scheduled content has a proper badge.');
$I->waitForText($postContent, null, '.wall-entry');
$I->see($this->getLabelText($datetime), '//div[@class="wall-entry"][1]');
$I->wantTo('ensure draft is not visible for other users.');
$I->amUser2(true);
$I->amOnSpace3();
$I->dontSee($postContent);
$I->wantTo('update scheduled options of the existing content');
$I->amSpaceAdmin(true, 3);
$I->waitForText($postContent);
$I->jsClick('.wall-entry:first .dropdown-toggle');
$datetime = (new Datetime('today'))->setTime(7, 45);
$this->updateSchedulingOptions($I, $datetime, '.label-state-scheduled');
$I->wantTo('ensure the scheduled content can be modified to draft');
$I->jsClick('.wall-entry:first .dropdown-toggle');
$this->disableSchedulingOptions($I);
}
private function getLabelText(?Datetime $datetime = null): string
{
return $datetime instanceof DateTime
? 'SCHEDULED AT ' . Yii::$app->formatter->asDatetime($datetime, self::DATE_FORMAT)
: 'DRAFT';
}
private function updateSchedulingOptions(AcceptanceTester $I, ?Datetime $datetime = null, $labelSelector = '.label-content-state')
{
$I->waitForText('Schedule publication');
$I->jsClick('.dropdown.open [data-action-click=scheduleOptions]');
$I->waitForText('Scheduling Options', null, '#globalModal');
if ($datetime instanceof DateTime) {
$I->checkOption('#scheduleoptionsform-enabled');
$I->fillField('ScheduleOptionsForm[date]', Yii::$app->formatter->asDate($datetime, self::DATE_FORMAT));
$I->fillField('ScheduleOptionsForm[time]', Yii::$app->formatter->asTime($datetime, self::DATE_FORMAT));
$I->click('.field-scheduleoptionsform-time');// to unfocus a datepicker in order to make the "Save" button visible
} else {
$I->uncheckOption('#scheduleoptionsform-enabled');
}
$I->click('Save');
$I->waitForText($this->getLabelText($datetime), 5, $labelSelector);
}
private function disableSchedulingOptions(AcceptanceTester $I)
{
$this->updateSchedulingOptions($I, null, '.label-state-draft');
}
}

View File

@ -95,6 +95,40 @@ class ContentContainerStreamTest extends HumHubDbTestCase
$this->assertTrue(in_array($w2, $ids));
}
public function testDraftContent()
{
$this->becomeUser('User2');
$draft1Id = $this->createPost('Some Draft', Content::VISIBILITY_PRIVATE, Content::STATE_DRAFT);
$regular1Id = $this->createPost('Regular 1 by U2', Content::VISIBILITY_PRIVATE,);
$this->becomeUser('Admin');
$regular2Id = $this->createPost('Regular 2 by Admin', Content::VISIBILITY_PRIVATE);
$this->becomeUser('User2');
$ids = $this->getStreamActionIds($this->space, 2);
// Check draft is first for Author
$this->assertTrue($ids[0] === $draft1Id);
// Check draft is not visible for other users
$this->becomeUser('Admin');
$ids = $this->getStreamActionIds($this->space, 5);
$this->assertTrue(!in_array($draft1Id, $ids));
}
public function testDeletedContent()
{
$this->becomeUser('User2');
$deleteId = $this->createPost('Something to delete', Content::VISIBILITY_PRIVATE);
$post = Post::findOne(['id' => $deleteId]);
$post->content->softDelete();
$ids = $this->getStreamActionIds($this->space, 3);
// Deleted Content should not appear in stream
$this->assertTrue(!in_array($deleteId, $ids));
}
private function getStreamActionIds($container, $limit = 4)
{
$action = new ContentContainerStream('stream', Yii::$app->controller, [
@ -107,7 +141,9 @@ class ContentContainerStreamTest extends HumHubDbTestCase
$wallEntries = $action->getStreamQuery()->all();
$wallEntryIds = array_map(static function($entry) {return $entry->id; }, $wallEntries);
$wallEntryIds = array_map(static function ($entry) {
return $entry->id;
}, $wallEntries);
return $wallEntryIds;
}
@ -122,12 +158,13 @@ class ContentContainerStreamTest extends HumHubDbTestCase
return $this->createPost('Public Post', Content::VISIBILITY_PUBLIC);
}
private function createPost($message, $visibility)
private function createPost($message, $visibility, $state = Content::STATE_PUBLISHED)
{
$post = new Post;
$post->message = $message;
$post->content->setContainer($this->space);
$post->content->visibility = $visibility;
$post->content->state = $state;
$post->save();
return $post->content->id;

View File

@ -0,0 +1,63 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace tests\codeception\unit\modules\content;
use DateTime;
use humhub\modules\content\jobs\PublishScheduledContents;
use humhub\modules\content\models\Content;
use humhub\modules\content\widgets\WallCreateContentForm;
use humhub\modules\post\models\Post;
use humhub\modules\space\models\Space;
use tests\codeception\_support\HumHubDbTestCase;
use Yii;
class PublishScheduledContentTest extends HumHubDbTestCase
{
public function testPublishScheduledContent()
{
$this->becomeUser('Admin');
$postA = $this->createScheduledPost('-1 hour');
$postB = $this->createScheduledPost('now');
$postC = $this->createScheduledPost('1 hour');
$postD = $this->createScheduledPost('tomorrow');
(new PublishScheduledContents())->run();
$postA = Post::findOne($postA->id);
$this->assertEquals(Content::STATE_PUBLISHED, $postA->content->state);
$postB = Post::findOne($postB->id);
$this->assertEquals(Content::STATE_PUBLISHED, $postB->content->state);
$postC = Post::findOne($postC->id);
$this->assertEquals(Content::STATE_SCHEDULED, $postC->content->state);
$postD = Post::findOne($postD->id);
$this->assertEquals(Content::STATE_SCHEDULED, $postD->content->state);
}
private function createScheduledPost($date): Post
{
$datetime = (new DateTime($date))->format('Y-m-d H:i:s');
$space = Space::findOne(1);
$post = new Post($space, ['message' => 'Post for test scheduling']);
Yii::$app->request->setBodyParams([
'state' => Content::STATE_SCHEDULED,
'scheduledDate' => $datetime
]);
$result = WallCreateContentForm::create($post, $space);
$this->assertArrayHasKey('id', $result);
$this->assertEquals(Content::STATE_SCHEDULED, $post->content->state);
$this->assertEquals($datetime, $post->content->scheduled_at);
return $post;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\content\models\Content;
use humhub\modules\content\models\forms\ScheduleOptionsForm;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\modules\ui\form\widgets\DatePicker;
use humhub\modules\ui\form\widgets\TimePicker;
use humhub\widgets\ModalButton;
use humhub\widgets\ModalDialog;
/* @var ScheduleOptionsForm $scheduleOptions */
/* @var bool $disableInputs */
?>
<?php ModalDialog::begin(['header' => Yii::t('ContentModule.base', '<strong>Scheduling </strong> Options')]) ?>
<?php $form = ActiveForm::begin() ?>
<?= Html::hiddenInput('state', Content::STATE_SCHEDULED) ?>
<?= Html::hiddenInput('stateTitle', $scheduleOptions->getStateTitle()) ?>
<?= Html::hiddenInput('scheduledDate', $scheduleOptions->date) ?>
<div class="modal-body">
<?= $form->field($scheduleOptions, 'enabled')->checkbox() ?>
<div class="row">
<div class="col-sm-3 col-xs-6">
<?= $form->field($scheduleOptions, 'date')
->widget(DatePicker::class, ['options' => ['disabled' => $disableInputs]])
->label(false) ?>
</div>
<div class="col-sm-3 col-xs-6" style="padding-left:0">
<?= $form->field($scheduleOptions, 'time')
->widget(TimePicker::class, ['disabled' => $disableInputs])
->label(false) ?>
</div>
</div>
</div>
<div class="modal-footer">
<?= ModalButton::submitModal() ?>
<?= ModalButton::cancel() ?>
</div>
<?php ActiveForm::end() ?>
<?php ModalDialog::end() ?>
<script <?= Html::nonce() ?>>
$('#scheduleoptionsform-enabled').click(function () {
$(this).closest('form').find('input[type=text]').prop('disabled', !$(this).is(':checked'));
});
</script>

View File

@ -47,12 +47,12 @@ class ModuleActionButtons extends Widget
if ($this->module->getContentContainerConfigUrl($this->contentContainer) &&
$this->contentContainer->moduleManager->isEnabled($this->module->id)) {
$html .= Button::asLink(Yii::t('ContentModule.modules', 'Configure'), $this->module->getContentContainerConfigUrl($this->contentContainer))
$html .= Button::asLink(Yii::t('ContentModule.base', 'Configure'), $this->module->getContentContainerConfigUrl($this->contentContainer))
->cssClass('btn btn-sm btn-info configure-module-' . $this->module->id);
}
if ($this->contentContainer->moduleManager->canDisable($this->module->id)) {
$html .= Button::asLink('<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('ContentModule.modules', 'Activated'), '#')
$html .= Button::asLink('<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('ContentModule.base', 'Activated'), '#')
->cssClass('btn btn-sm btn-info active disable disable-module-' . $this->module->id)
->style($this->contentContainer->moduleManager->isEnabled($this->module->id) ? '' : 'display:none')
->options([
@ -64,7 +64,7 @@ class ModuleActionButtons extends Widget
]);
}
$html .= Button::asLink(Yii::t('ContentModule.modules', 'Enable'), '#')
$html .= Button::asLink(Yii::t('ContentModule.base', 'Enable'), '#')
->cssClass('btn btn-sm btn-info enable enable-module-' . $this->module->id)
->style($this->contentContainer->moduleManager->isEnabled($this->module->id) ? 'display:none' : '')
->options([

View File

@ -0,0 +1,39 @@
<?php
namespace humhub\modules\content\widgets;
use humhub\components\Widget;
use humhub\libs\Html;
use humhub\modules\content\models\Content;
use Yii;
use yii\helpers\Url;
class PublishDraftLink extends Widget
{
/**
* @var \humhub\modules\content\components\ContentActiveRecord
*/
public $content;
/**
* @inheritdoc
*/
public function run()
{
if ($this->content->content->state !== Content::STATE_DRAFT ||
!$this->content->content->canEdit()) {
return '';
}
$publishUrl = Url::to(['/content/content/publish-draft', 'id' => $this->content->content->id]);
return Html::tag('li',
Html::a(
'<i class="fa fa-mail-reply-all"></i> '
. Yii::t('ContentModule.base', 'Publish draft'),
'#', ['data-action-click' => 'publishDraft', 'data-action-url' => $publishUrl])
);
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\widgets;
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\models\Content;
use humhub\widgets\Link;
use Yii;
use yii\base\Widget;
/**
* Schedule link for updating the schedule options of Wall Entries.
*
* @package humhub.modules_core.wall.widgets
* @since 1.14
*/
class ScheduleLink extends Widget
{
public ContentActiveRecord $contentRecord;
public array $allowedStates = [Content::STATE_DRAFT, Content::STATE_SCHEDULED];
/**
* @inheritdoc
*/
public function run()
{
$content = $this->contentRecord->content;
if (!in_array($content->state, $this->allowedStates)) {
return '';
}
$contentContainer = $content->container;
if (!$contentContainer instanceof ContentContainerActiveRecord) {
return '';
}
if (!$content->canEdit()) {
return '';
}
return Html::tag('li', Link::withAction(Yii::t('ContentModule.base', 'Schedule publication'),
'scheduleOptions',
$contentContainer->createUrl('/content/content/schedule-options', ['id' => $content->id]))
->icon('clock-o'));
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\widgets;
use DateTime;
use DateTimeZone;
use humhub\components\Widget;
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use Yii;
/**
* Can be used to render an archive icon for archived content.
* @package humhub\modules\content\widgets
* @since 1.14
*/
class StateBadge extends Widget
{
public ?ContentActiveRecord $model;
/**
* @throws \yii\base\InvalidConfigException
* @throws \Exception
*/
public function run()
{
if ($this->model === null) {
return '';
}
switch ($this->model->content->state) {
case Content::STATE_DRAFT:
return Html::tag('span', Yii::t('ContentModule.base', 'Draft'),
['class' => 'label label-danger label-state-draft']
);
case Content::STATE_SCHEDULED:
$scheduledDateTime = new DateTime($this->model->content->scheduled_at, new DateTimeZone('UTC'));
return Html::tag('span', Yii::t('ContentModule.base', 'Scheduled at {dateTime}', [
'dateTime' => Yii::$app->formatter->asDatetime($scheduledDateTime, 'short')
]),
['class' => 'label label-warning label-state-scheduled']
);
case Content::STATE_DELETED:
return Html::tag('span', Yii::t('ContentModule.base', 'Deleted'),
['class' => 'label label-danger label-state-deleted']
);
}
return '';
}
}

View File

@ -114,6 +114,9 @@ abstract class WallCreateContentForm extends Widget
$record->content->visibility = $visibility;
$record->content->container = $contentContainer;
$record->content->setState(Yii::$app->request->post('state'), [
'scheduled_at' => Yii::$app->request->post('scheduledDate')
]);
// Handle Notify User Features of ContentFormWidget
// ToDo: Check permissions of user guids
@ -129,7 +132,7 @@ abstract class WallCreateContentForm extends Widget
if ($record->save()) {
$topics = Yii::$app->request->post('postTopicInput');
if(!empty($topics)) {
if (!empty($topics)) {
Topic::attach($record->content, $topics);
}

View File

@ -66,6 +66,7 @@ class WallCreateContentFormFooter extends Widget
'canSwitchVisibility' => $this->contentContainer->visibility !== Space::VISIBILITY_NONE && $this->contentContainer->can(CreatePublicContent::class),
'fileHandlers' => FileHandlerCollection::getByType([FileHandlerCollection::TYPE_IMPORT, FileHandlerCollection::TYPE_CREATE]),
'pickerUrl' => $this->contentContainer instanceof Space ? $this->contentContainer->createUrl('/space/membership/search') : null,
'scheduleUrl' => $this->contentContainer->createUrl('/content/content/schedule-options')
]);
}
}

View File

@ -6,6 +6,7 @@ namespace humhub\modules\content\widgets\stream;
use Exception;
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\content\widgets\ArchiveLink;
use humhub\modules\content\widgets\DeleteLink;
use humhub\modules\content\widgets\LockCommentsLink;
@ -14,6 +15,8 @@ use humhub\modules\content\widgets\MoveContentLink;
use humhub\modules\content\widgets\NotificationSwitchLink;
use humhub\modules\content\widgets\PermaLink;
use humhub\modules\content\widgets\PinLink;
use humhub\modules\content\widgets\PublishDraftLink;
use humhub\modules\content\widgets\ScheduleLink;
use humhub\modules\content\widgets\VisibilityLink;
use humhub\modules\dashboard\controllers\DashboardController;
use humhub\modules\space\models\Space;
@ -324,6 +327,10 @@ abstract class WallStreamEntryWidget extends StreamEntryWidget
*/
public function getControlsMenuEntries()
{
if ($this->model->content->state === Content::STATE_DELETED) {
return [];
}
if($this->renderOptions->isViewContext([WallStreamEntryOptions::VIEW_CONTEXT_SEARCH])) {
return [
[PermaLink::class, ['content' => $this->model], ['sortOrder' => 200]]
@ -331,10 +338,12 @@ abstract class WallStreamEntryWidget extends StreamEntryWidget
}
$result = [
[PublishDraftLink::class, ['content' => $this->model], ['sortOrder' => 100]],
[PermaLink::class, ['content' => $this->model], ['sortOrder' => 200]],
[DeleteLink::class, ['content' => $this->model], ['sortOrder' => 300]],
new DropdownDivider(['sortOrder' => 350]),
[VisibilityLink::class, ['contentRecord' => $this->model], ['sortOrder' => 400]],
[ScheduleLink::class, ['contentRecord' => $this->model], ['sortOrder' => 420]],
[LockCommentsLink::class, ['contentRecord' => $this->model], ['sortOrder' => 450]],
[NotificationSwitchLink::class, ['content' => $this->model], ['sortOrder' => 500]],
[MoveContentLink::class, ['model' => $this->model], ['sortOrder' => 700]],

View File

@ -2,8 +2,10 @@
use humhub\libs\Html;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\content\widgets\ArchivedIcon;
use humhub\modules\content\widgets\LockCommentsIcon;
use humhub\modules\content\widgets\StateBadge;
use humhub\modules\content\widgets\stream\WallStreamEntryOptions;
use humhub\modules\content\widgets\UpdatedIcon;
use humhub\modules\content\widgets\VisibilityIcon;
@ -29,6 +31,7 @@ $container = $model->content->container;
<?php elseif ($renderOptions->isPinned($model)) : ?>
<?= Icon::get('map-pin', ['htmlOptions' => ['class' => 'icon-pin tt', 'title' => Yii::t('ContentModule.base', 'Pinned')]]) ?>
<?php endif; ?>
<?= StateBadge::widget(['model' => $model]); ?>
</div>
<!-- since v1.2 -->

View File

@ -6,6 +6,7 @@
*/
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\models\Content;
use humhub\modules\file\handler\BaseFileHandler;
use humhub\modules\file\widgets\FilePreview;
use humhub\modules\topic\widgets\TopicPicker;
@ -24,6 +25,7 @@ use yii\helpers\Html;
/* @var $canSwitchVisibility boolean */
/* @var $contentContainer ContentContainerActiveRecord */
/* @var $pickerUrl string */
/* @var $scheduleUrl string */
?>
<div id="notifyUserContainer" class="form-group" style="margin-top:15px;display:none">
@ -64,25 +66,30 @@ use yii\helpers\Html;
<?= FileHandlerButtonDropdown::widget(['primaryButton' => $uploadButton, 'handlers' => $fileHandlers, 'cssButtonClass' => 'btn-default']); ?>
<!-- public checkbox -->
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true', 'title' => Yii::t('ContentModule.base', 'Content visibility') ]); ?>
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true']); ?>
<!-- state data -->
<?= Html::hiddenInput('state', Content::STATE_PUBLISHED) ?>
<!-- content sharing -->
<div class="pull-right">
<span class="label label-info label-public hidden"><?= Yii::t('ContentModule.base', 'Public'); ?></span>
<span class="label-container">
<span class="label label-info label-public hidden"><?= Yii::t('ContentModule.base', 'Public'); ?></span>
</span>
<ul class="nav nav-pills preferences" style="right:0;top:5px">
<li class="dropdown">
<a class="dropdown-toggle" style="padding:5px 10px" data-toggle="dropdown" href="#" aria-label="<?= Yii::t('base', 'Toggle post menu'); ?>" aria-haspopup="true">
<?= Icon::get('cogs')?>
<a class="dropdown-toggle" style="padding:5px 10px" data-toggle="dropdown" href="#"
aria-label="<?= Yii::t('base', 'Toggle post menu'); ?>" aria-haspopup="true">
<?= Icon::get('cogs') ?>
</a>
<ul class="dropdown-menu pull-right">
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('bell')?>
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('bell') ?>
</li>
<?php if (TopicPicker::showTopicPicker($contentContainer)) : ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
</li>
<?php endif; ?>
<?php if ($canSwitchVisibility): ?>
@ -91,6 +98,18 @@ use yii\helpers\Html;
->id('contentForm_visibility_entry')->icon('unlock') ?>
</li>
<?php endif; ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Create as draft'), 'changeState')
->icon('edit')
->options([
'data-state' => Content::STATE_DRAFT,
'data-state-title' => Yii::t('ContentModule.base', 'Draft')
]) ?>
</li>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Schedule publication'), 'scheduleOptions', $scheduleUrl)
->icon('clock-o') ?>
</li>
</ul>
</li>
</ul>

View File

@ -173,7 +173,7 @@ class Module extends \humhub\components\Module
];
/**
* Step: Setup Admin User
* Step: Setup Admin User
*/
$this->configSteps['admin'] = [
'sort' => 400,
@ -183,7 +183,6 @@ class Module extends \humhub\components\Module
},
];
/**
* Step: Sample Data
*/

View File

@ -232,6 +232,7 @@ class ConfigController extends Controller
}
}
/**
* Sample Data
*/
@ -541,7 +542,7 @@ class ConfigController extends Controller
try {
Yii::$app->user->logout();
} catch (Exception $e) {
} catch (\Exception $e) {
;
}
return $this->render('finished');

View File

@ -9,11 +9,12 @@
namespace humhub\modules\installer\controllers;
use humhub\components\access\ControllerAccess;
use Yii;
use humhub\components\Controller;
use humhub\modules\installer\forms\DatabaseForm;
use humhub\libs\DynamicConfig;
use humhub\modules\admin\widgets\PrerequisitesList;
use humhub\modules\installer\forms\CronForm;
use humhub\modules\installer\forms\DatabaseForm;
use Yii;
/**
* SetupController checks prerequisites and is responsible for database
@ -130,7 +131,7 @@ class SetupController extends Controller
}
/**
* The init action imports the database structure & inital data
* The init action imports the database structure & initial data
*/
public function actionInit()
{
@ -152,7 +153,22 @@ class SetupController extends Controller
$this->module->setDatabaseInstalled();
return $this->redirect(['/installer/config/index']);
return $this->redirect(['/installer/setup/cron']);
}
/**
* Crontab
*/
public function actionCron()
{
return $this->render('cron', []);
}
/**
* Pretty URLs
*/
public function actionPrettyUrls()
{
return $this->render('pretty-urls');
}
}

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