From: Tim Düsterhus Date: Wed, 19 Apr 2023 11:51:31 +0000 (+0200) Subject: Merge branch '5.3' into 5.4 X-Git-Tag: 5.4.27~4 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=019f32e614d64c7276bb8829393f58aa48858941;p=GitHub%2FWoltLab%2FWCF.git Merge branch '5.3' into 5.4 --- 019f32e614d64c7276bb8829393f58aa48858941 diff --cc ts/WoltLabSuite/Core/StringUtil.ts index 9c7c3b438b,0000000000..52e4896f9d mode 100644,000000..100644 --- a/ts/WoltLabSuite/Core/StringUtil.ts +++ b/ts/WoltLabSuite/Core/StringUtil.ts @@@ -1,143 -1,0 +1,143 @@@ +/** + * Provides helper functions for String handling. + * + * @author Tim Duesterhus, Joshua Ruesweg + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module StringUtil (alias) + * @module WoltLabSuite/Core/StringUtil + */ + +import * as NumberUtil from "./NumberUtil"; + +let _decimalPoint = "."; +let _thousandsSeparator = ","; + +/** + * Adds thousands separators to a given number. + * + * @see http://stackoverflow.com/a/6502556/782822 + */ +export function addThousandsSeparator(number: number): string { + return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, "$1" + _thousandsSeparator); +} + +/** + * Escapes special HTML-characters within a string + */ +export function escapeHTML(string: string): string { + return String(string).replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); +} + +/** + * Escapes a String to work with RegExp. + * + * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25 + */ +export function escapeRegExp(string: string): string { + return String(string).replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1"); +} + +/** + * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands separators. + */ +export function formatNumeric(number: number, decimalPlaces?: number): string { + let tmp = NumberUtil.round(number, decimalPlaces || -2).toString(); + const numberParts = tmp.split("."); + + tmp = addThousandsSeparator(+numberParts[0]); + if (numberParts.length > 1) { + tmp += _decimalPoint + numberParts[1]; + } + + tmp = tmp.replace("-", "\u2212"); + + return tmp; +} + +/** + * Makes a string's first character lowercase. + */ +export function lcfirst(string: string): string { + return String(string).substring(0, 1).toLowerCase() + string.substring(1); +} + +/** + * Makes a string's first character uppercase. + */ +export function ucfirst(string: string): string { + return String(string).substring(0, 1).toUpperCase() + string.substring(1); +} + +/** + * Unescapes special HTML-characters within a string. + */ +export function unescapeHTML(string: string): string { + return String(string) - .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/</g, "<") - .replace(/>/g, ">"); ++ .replace(/>/g, ">") ++ .replace(/&/g, "&"); +} + +/** + * Shortens numbers larger than 1000 by using unit suffixes. + */ +export function shortUnit(number: number): string { + let unitSuffix = ""; + + if (number >= 1000000) { + number /= 1000000; + + if (number > 10) { + number = Math.floor(number); + } else { + number = NumberUtil.round(number, -1); + } + + unitSuffix = "M"; + } else if (number >= 1000) { + number /= 1000; + + if (number > 10) { + number = Math.floor(number); + } else { + number = NumberUtil.round(number, -1); + } + + unitSuffix = "k"; + } + + return formatNumeric(number) + unitSuffix; +} + +/** + * Converts a lower-case string containing dashed to camelCase for use + * with the `dataset` property. + */ +export function toCamelCase(value: string): string { + if (!value.includes("-")) { + return value; + } + + return value + .split("-") + .map((part, index) => { + if (index > 0) { + part = ucfirst(part); + } + + return part; + }) + .join(""); +} + +interface I18nValues { + decimalPoint: string; + thousandsSeparator: string; +} + +export function setupI18n(values: I18nValues): void { + _decimalPoint = values.decimalPoint; + _thousandsSeparator = values.thousandsSeparator; +} diff --cc wcfsetup/install/files/js/WoltLabSuite/Core/StringUtil.js index 28e090fadb,37f4754e71..dee8ef2a53 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/StringUtil.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/StringUtil.js @@@ -1,133 -1,141 +1,133 @@@ /** * Provides helper functions for String handling. - * - * @author Tim Duesterhus, Joshua Ruesweg - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module StringUtil (alias) - * @module WoltLabSuite/Core/StringUtil + * + * @author Tim Duesterhus, Joshua Ruesweg + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module StringUtil (alias) + * @module WoltLabSuite/Core/StringUtil */ -define(['Language', './NumberUtil'], function(Language, NumberUtil) { - "use strict"; - - /** - * @exports WoltLabSuite/Core/StringUtil - */ - return { - /** - * Adds thousands separators to a given number. - * - * @see http://stackoverflow.com/a/6502556/782822 - * @param {?} number - * @return {String} - */ - addThousandsSeparator: function(number) { - // Fetch Language, as it cannot be provided because of a circular dependency - if (Language === undefined) Language = require('Language'); - - return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + Language.get('wcf.global.thousandsSeparator')); - }, - - /** - * Escapes special HTML-characters within a string - * - * @param {?} string - * @return {String} - */ - escapeHTML: function (string) { - return String(string).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); - }, - - /** - * Escapes a String to work with RegExp. - * - * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25 - * @param {?} string - * @return {String} - */ - escapeRegExp: function(string) { - return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); - }, - - /** - * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands separators. - * - * @param {?} number - * @param {int} decimalPlaces The number of decimal places to leave after rounding. - * @return {String} - */ - formatNumeric: function(number, decimalPlaces) { - // Fetch Language, as it cannot be provided because of a circular dependency - if (Language === undefined) Language = require('Language'); - - number = String(NumberUtil.round(number, decimalPlaces || -2)); - var numberParts = number.split('.'); - - number = this.addThousandsSeparator(numberParts[0]); - if (numberParts.length > 1) number += Language.get('wcf.global.decimalPoint') + numberParts[1]; - - number = number.replace('-', '\u2212'); - - return number; - }, - - /** - * Makes a string's first character lowercase. - * - * @param {?} string - * @return {String} - */ - lcfirst: function(string) { - return String(string).substring(0, 1).toLowerCase() + string.substring(1); - }, - - /** - * Makes a string's first character uppercase. - * - * @param {?} string - * @return {String} - */ - ucfirst: function(string) { - return String(string).substring(0, 1).toUpperCase() + string.substring(1); - }, - - /** - * Unescapes special HTML-characters within a string. - * - * @param {?} string - * @return {String} - */ - unescapeHTML: function(string) { - return String(string).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); - }, - - /** - * Shortens numbers larger than 1000 by using unit suffixes. - * - * @param {?} number - * @return {String} - */ - shortUnit: function(number) { - var unitSuffix = ''; - - if (number >= 1000000) { - number /= 1000000; - - if (number > 10) { - number = Math.floor(number); - } - else { - number = NumberUtil.round(number, -1); - } - - unitSuffix = 'M'; - } - else if (number >= 1000) { - number /= 1000; - - if (number > 10) { - number = Math.floor(number); - } - else { - number = NumberUtil.round(number, -1); - } - - unitSuffix = 'k'; - } - - return this.formatNumeric(number) + unitSuffix; - } - }; +define(["require", "exports", "tslib", "./NumberUtil"], function (require, exports, tslib_1, NumberUtil) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setupI18n = exports.toCamelCase = exports.shortUnit = exports.unescapeHTML = exports.ucfirst = exports.lcfirst = exports.formatNumeric = exports.escapeRegExp = exports.escapeHTML = exports.addThousandsSeparator = void 0; + NumberUtil = tslib_1.__importStar(NumberUtil); + let _decimalPoint = "."; + let _thousandsSeparator = ","; + /** + * Adds thousands separators to a given number. + * + * @see http://stackoverflow.com/a/6502556/782822 + */ + function addThousandsSeparator(number) { + return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, "$1" + _thousandsSeparator); + } + exports.addThousandsSeparator = addThousandsSeparator; + /** + * Escapes special HTML-characters within a string + */ + function escapeHTML(string) { + return String(string).replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); + } + exports.escapeHTML = escapeHTML; + /** + * Escapes a String to work with RegExp. + * + * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25 + */ + function escapeRegExp(string) { + return String(string).replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1"); + } + exports.escapeRegExp = escapeRegExp; + /** + * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands separators. + */ + function formatNumeric(number, decimalPlaces) { + let tmp = NumberUtil.round(number, decimalPlaces || -2).toString(); + const numberParts = tmp.split("."); + tmp = addThousandsSeparator(+numberParts[0]); + if (numberParts.length > 1) { + tmp += _decimalPoint + numberParts[1]; + } + tmp = tmp.replace("-", "\u2212"); + return tmp; + } + exports.formatNumeric = formatNumeric; + /** + * Makes a string's first character lowercase. + */ + function lcfirst(string) { + return String(string).substring(0, 1).toLowerCase() + string.substring(1); + } + exports.lcfirst = lcfirst; + /** + * Makes a string's first character uppercase. + */ + function ucfirst(string) { + return String(string).substring(0, 1).toUpperCase() + string.substring(1); + } + exports.ucfirst = ucfirst; + /** + * Unescapes special HTML-characters within a string. + */ + function unescapeHTML(string) { + return String(string) - .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/</g, "<") - .replace(/>/g, ">"); ++ .replace(/>/g, ">") ++ .replace(/&/g, "&"); + } + exports.unescapeHTML = unescapeHTML; + /** + * Shortens numbers larger than 1000 by using unit suffixes. + */ + function shortUnit(number) { + let unitSuffix = ""; + if (number >= 1000000) { + number /= 1000000; + if (number > 10) { + number = Math.floor(number); + } + else { + number = NumberUtil.round(number, -1); + } + unitSuffix = "M"; + } + else if (number >= 1000) { + number /= 1000; + if (number > 10) { + number = Math.floor(number); + } + else { + number = NumberUtil.round(number, -1); + } + unitSuffix = "k"; + } + return formatNumeric(number) + unitSuffix; + } + exports.shortUnit = shortUnit; + /** + * Converts a lower-case string containing dashed to camelCase for use + * with the `dataset` property. + */ + function toCamelCase(value) { + if (!value.includes("-")) { + return value; + } + return value + .split("-") + .map((part, index) => { + if (index > 0) { + part = ucfirst(part); + } + return part; + }) + .join(""); + } + exports.toCamelCase = toCamelCase; + function setupI18n(values) { + _decimalPoint = values.decimalPoint; + _thousandsSeparator = values.thousandsSeparator; + } + exports.setupI18n = setupI18n; }); diff --cc wcfsetup/install/files/lib/data/article/ArticleAction.class.php index 2e47b398ca,172b265c16..b3756bf04e --- a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php +++ b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php @@@ -29,889 -27,814 +29,893 @@@ use wcf\system\WCF /** * Executes article related actions. - * - * @author Marcel Werk - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @package WoltLabSuite\Core\Data\Article - * @since 3.0 - * - * @method ArticleEditor[] getObjects() - * @method ArticleEditor getSingleObject() + * + * @author Marcel Werk + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @package WoltLabSuite\Core\Data\Article + * @since 3.0 + * + * @method ArticleEditor[] getObjects() + * @method ArticleEditor getSingleObject() */ -class ArticleAction extends AbstractDatabaseObjectAction { - /** - * article editor instance - * @var ArticleEditor - */ - public $articleEditor; - - /** - * language object - * @var Language - */ - public $language; - - /** - * @inheritDoc - */ - protected $className = ArticleEditor::class; - - /** - * @inheritDoc - */ - protected $permissionsCreate = ['admin.content.article.canManageArticle']; - - /** - * @inheritDoc - */ - protected $permissionsDelete = ['admin.content.article.canManageArticle']; - - /** - * @inheritDoc - */ - protected $permissionsUpdate = ['admin.content.article.canManageArticle']; - - /** - * @inheritDoc - */ - protected $requireACP = ['create', 'update']; - - /** - * @inheritDoc - */ - protected $allowGuestAccess = ['markAllAsRead']; - - /** - * @inheritDoc - * @return Article - */ - public function create() { - /** @var Article $article */ - $article = parent::create(); - - // save article content - if (!empty($this->parameters['content'])) { - foreach ($this->parameters['content'] as $languageID => $content) { - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['content'] = $content['htmlInputProcessor']->getHtml(); - } - - /** @var ArticleContent $articleContent */ - $articleContent = ArticleContentEditor::create([ - 'articleID' => $article->articleID, - 'languageID' => $languageID ?: null, - 'title' => $content['title'], - 'teaser' => $content['teaser'], - 'content' => $content['content'], - 'imageID' => $content['imageID'], - 'teaserImageID' => $content['teaserImageID'], - 'metaTitle' => $content['metaTitle'] ?? '', - 'metaDescription' => $content['metaDescription'] ?? '', - ]); - $articleContentEditor = new ArticleContentEditor($articleContent); - - // save tags - if (!empty($content['tags'])) { - TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID())); - } - - // update search index - SearchIndexManager::getInstance()->set( - 'com.woltlab.wcf.article', - $articleContent->articleContentID, - $articleContent->content, - $articleContent->title, - $article->time, - $article->userID, - $article->username, - $languageID ?: null, - $articleContent->teaser - ); - - // save embedded objects - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); - if (MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { - $articleContentEditor->update(['hasEmbeddedObjects' => 1]); - } - } - } - } - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - if ($article->publicationStatus == Article::PUBLISHED) { - ArticleEditor::updateArticleCounter([$article->userID => 1]); - - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $article->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($article) - ); - - UserActivityEventHandler::getInstance()->fireEvent('com.woltlab.wcf.article.recentActivityEvent', $article->articleID, null, $article->userID, $article->time); - } - - return $article; - } - - /** - * @inheritDoc - */ - public function update() { - parent::update(); - - $isRevert = (!empty($this->parameters['isRevert'])); - - // update article content - if (!empty($this->parameters['content'])) { - foreach ($this->getObjects() as $article) { - $versionData = []; - $hasChanges = false; - - foreach ($this->parameters['content'] as $languageID => $content) { - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['content'] = $content['htmlInputProcessor']->getHtml(); - } - - $articleContent = ArticleContent::getArticleContent($article->articleID, ($languageID ?: null)); - $articleContentEditor = null; - if ($articleContent !== null) { - // update - $articleContentEditor = new ArticleContentEditor($articleContent); - $articleContentEditor->update([ - 'title' => $content['title'], - 'teaser' => $content['teaser'], - 'content' => $content['content'], - 'imageID' => ($isRevert) ? $articleContent->imageID : $content['imageID'], - 'teaserImageID' => ($isRevert) ? $articleContent->teaserImageID : $content['teaserImageID'], - 'metaTitle' => $content['metaTitle'] ?? '', - 'metaDescription' => $content['metaDescription'] ?? '', - ]); - - $versionData[] = $articleContent; - if ($articleContent->content != $content['content'] || $articleContent->teaser != $content['teaser'] || $articleContent->title != $content['title']) { - $hasChanges = true; - } - - // delete tags - if (!$isRevert && empty($content['tags'])) { - TagEngine::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, ($languageID ?: null)); - } - } - else { - /** @var ArticleContent $articleContent */ - $articleContent = ArticleContentEditor::create([ - 'articleID' => $article->articleID, - 'languageID' => $languageID ?: null, - 'title' => $content['title'], - 'teaser' => $content['teaser'], - 'content' => $content['content'], - 'imageID' => ($isRevert) ? null : $content['imageID'], - 'teaserImageID' => ($isRevert) ? null : $content['teaserImageID'], - 'metaTitle' => $content['metaTitle'] ?? '', - 'metaDescription' => $content['metaDescription'] ?? '', - ]); - $articleContentEditor = new ArticleContentEditor($articleContent); - - $versionData[] = $articleContent; - $hasChanges = true; - } - - // save tags - if (!$isRevert && !empty($content['tags'])) { - TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID())); - } - - // update search index - SearchIndexManager::getInstance()->set( - 'com.woltlab.wcf.article', - $articleContent->articleContentID, - isset($content['content']) ? $content['content'] : $articleContent->content, - isset($content['title']) ? $content['title'] : $articleContent->title, - $this->parameters['data']['time'] ?? $article->time, - $this->parameters['data']['userID'] ?? $article->userID, - $this->parameters['data']['username'] ?? $article->username, - $languageID ?: null, - isset($content['teaser']) ? $content['teaser'] : $articleContent->teaser - ); - - // save embedded objects - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); - if ($articleContent->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { - $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ? 0 : 1]); - } - } - } - - if ($hasChanges) { - $articleObj = new ArticleVersionTracker($article->getDecoratedObject()); - $articleObj->setContent($versionData); - VersionTracker::getInstance()->add('com.woltlab.wcf.article', $articleObj); - } - } - } - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - $publicationStatus = (isset($this->parameters['data']['publicationStatus'])) ? $this->parameters['data']['publicationStatus'] : null; - if ($publicationStatus !== null) { - $usersToArticles = $resetArticleIDs = []; - /** @var ArticleEditor $articleEditor */ - foreach ($this->objects as $articleEditor) { - if ($publicationStatus != $articleEditor->publicationStatus) { - // The article was published before or was now published. - if ($publicationStatus == Article::PUBLISHED || $articleEditor->publicationStatus == Article::PUBLISHED) { - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID] += ($publicationStatus == Article::PUBLISHED) ? 1 : -1; - } - - if ($publicationStatus == Article::PUBLISHED) { - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $articleEditor->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) - ); - - UserActivityEventHandler::getInstance()->fireEvent( - 'com.woltlab.wcf.article.recentActivityEvent', - $articleEditor->articleID, - null, - $this->parameters['data']['userID'] ?? $articleEditor->userID, - $this->parameters['data']['time'] ?? $articleEditor->time - ); - } - else { - $resetArticleIDs[] = $articleEditor->articleID; - } - } - } - - if (!empty($resetArticleIDs)) { - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.article.notification', $resetArticleIDs); - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents('com.woltlab.wcf.article.recentActivityEvent', $resetArticleIDs); - } - - if (!empty($usersToArticles)) { - ArticleEditor::updateArticleCounter($usersToArticles); - } - } - - // update author in recent activities - if (isset($this->parameters['data']['userID'])) { - $sql = "UPDATE wcf".WCF_N."_user_activity_event SET userID = ? WHERE objectTypeID = ? AND objectID = ?"; - $statement = WCF::getDB()->prepareStatement($sql); - - foreach ($this->objects as $articleEditor) { - if ($articleEditor->userID != $this->parameters['data']['userID']) { - $statement->execute([ - $this->parameters['data']['userID'], - UserActivityEventHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article.recentActivityEvent'), - $articleEditor->articleID, - ]); - } - } - } - } - - /** - * Validates parameters to delete articles. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateDelete() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canDelete()) { - throw new PermissionDeniedException(); - } - - if (!$article->isDeleted) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * @inheritDoc - */ - public function delete() { - $usersToArticles = $articleIDs = $articleContentIDs = []; - foreach ($this->getObjects() as $article) { - $articleIDs[] = $article->articleID; - foreach ($article->getArticleContents() as $articleContent) { - $articleContentIDs[] = $articleContent->articleContentID; - } - - if ($article->publicationStatus == Article::PUBLISHED) { - if (!isset($usersToArticles[$article->userID])) { - $usersToArticles[$article->userID] = 0; - } - $usersToArticles[$article->userID]--; - } - } - - // delete articles - parent::delete(); - - if (!empty($articleIDs)) { - // delete like data - ReactionHandler::getInstance()->removeReactions('com.woltlab.wcf.likeableArticle', $articleIDs); - // delete comments - CommentHandler::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs); - // delete tag to object entries - TagEngine::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs); - // delete entry from search index - SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs); - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.article.notification', $articleIDs); - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents('com.woltlab.wcf.article.recentActivityEvent', $articleIDs); - // delete embedded object references - MessageEmbeddedObjectManager::getInstance()->removeObjects('com.woltlab.wcf.article.content', $articleContentIDs); - // update wcf1_user.articles - ArticleEditor::updateArticleCounter($usersToArticles); - } - - $this->unmarkItems(); - - return [ - 'objectIDs' => $this->objectIDs, - 'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true]) - ]; - } - - /** - * Validates parameters to move articles to the trash bin. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateTrash() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canDelete()) { - throw new PermissionDeniedException(); - } - - if ($article->isDeleted) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * Moves articles to the trash bin. - */ - public function trash() { - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['isDeleted' => 1]); - } - - $this->unmarkItems(); - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - return ['objectIDs' => $this->objectIDs]; - } - - /** - * Validates parameters to restore articles. - * - * @throws UserInputException - */ - public function validateRestore() { - $this->validateDelete(); - } - - /** - * Restores articles. - */ - public function restore() { - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['isDeleted' => 0]); - } - - $this->unmarkItems(); - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - return ['objectIDs' => $this->objectIDs]; - } - - /** - * Validates parameters to toggle between i18n and monolingual mode. - * - * @throws UserInputException - */ - public function validateToggleI18n() { - WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); - - $this->articleEditor = $this->getSingleObject(); - if ($this->articleEditor->getDecoratedObject()->isMultilingual) { - $this->readInteger('languageID'); - $this->language = LanguageFactory::getInstance()->getLanguage($this->parameters['languageID']); - if ($this->language === null) { - throw new UserInputException('languageID'); - } - - $contents = $this->articleEditor->getArticleContents(); - if (!isset($contents[$this->language->languageID])) { - // there is no content - throw new UserInputException('languageID'); - } - } - } - - /** - * Toggles between i18n and monolingual mode. - */ - public function toggleI18n() { - $removeContent = []; - - // i18n -> monolingual - if ($this->articleEditor->getDecoratedObject()->isMultilingual) { - foreach ($this->articleEditor->getArticleContents() as $articleContent) { - if ($articleContent->languageID == $this->language->languageID) { - $articleContentEditor = new ArticleContentEditor($articleContent); - $articleContentEditor->update(['languageID' => null]); - } - else { - $removeContent[] = $articleContent; - } - } - } - else { - // monolingual -> i18n - $articleContent = $this->articleEditor->getArticleContent(); - $data = []; - foreach (LanguageFactory::getInstance()->getLanguages() as $language) { - $data[$language->languageID] = [ - 'title' => $articleContent->title, - 'teaser' => $articleContent->teaser, - 'content' => $articleContent->content, - 'imageID' => $articleContent->imageID ?: null, - 'teaserImageID' => $articleContent->teaserImageID ?: null - ]; - } - - $action = new ArticleAction([$this->articleEditor], 'update', ['content' => $data]); - $action->executeAction(); - - $removeContent[] = $articleContent; - } - - if (!empty($removeContent)) { - $action = new ArticleContentAction($removeContent, 'delete'); - $action->executeAction(); - } - - // flush edit history - VersionTracker::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor->getDecoratedObject()->articleID); - - // update article's i18n state - $this->articleEditor->update([ - 'isMultilingual' => ($this->articleEditor->getDecoratedObject()->isMultilingual) ? 0 : 1 - ]); - } - - /** - * Marks articles as read. - */ - public function markAsRead() { - if (empty($this->parameters['visitTime'])) { - $this->parameters['visitTime'] = TIME_NOW; - } - - if (empty($this->objects)) { - $this->readObjects(); - } - - foreach ($this->getObjects() as $article) { - VisitTracker::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID, $this->parameters['visitTime']); - } - - // reset storage - if (WCF::getUser()->userID) { - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); - } - } - - /** - * Marks all articles as read. - */ - public function markAllAsRead() { - VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article'); - - // reset storage - if (WCF::getUser()->userID) { - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); - } - } - - /** - * Validates the mark all as read action. - */ - public function validateMarkAllAsRead() { - // does nothing - } - - /** - * Validates the `setCategory` action. - * - * @throws UserInputException - */ - public function validateSetCategory() { - WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); - - $this->readBoolean('useMarkedArticles', true); - - // if no object ids are given, use clipboard handler - if (empty($this->objectIDs) && $this->parameters['useMarkedArticles']) { - $this->objectIDs = array_keys(ClipboardHandler::getInstance()->getMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article'))); - } - - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - $this->readInteger('categoryID'); - $category = ArticleCategory::getCategory($this->parameters['categoryID']); - if ($category === null) { - throw new UserInputException('categoryID'); - } - if (!$category->isAccessible()) { - throw new UserInputException('categoryID'); - } - } - - /** - * Sets the category of articles. - */ - public function setCategory() { - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['categoryID' => $this->parameters['categoryID']]); - } - - $this->unmarkItems(); - } - - /** - * Validates the `publish` action. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validatePublish() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canPublish()) { - throw new PermissionDeniedException(); - } - - if ($article->publicationStatus == Article::PUBLISHED) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * Publishes articles. - */ - public function publish() { - $usersToArticles = []; - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update([ - 'time' => TIME_NOW, - 'publicationStatus' => Article::PUBLISHED, - 'publicationDate' => 0 - ]); - - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID]++; - - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $articleEditor->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) - ); - - UserActivityEventHandler::getInstance()->fireEvent('com.woltlab.wcf.article.recentActivityEvent', $articleEditor->articleID, null, $articleEditor->userID, TIME_NOW); - } - - ArticleEditor::updateArticleCounter($usersToArticles); - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - $this->unmarkItems(); - } - - /** - * Validates the `unpublish` action. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateUnpublish() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canPublish()) { - throw new PermissionDeniedException(); - } - - if ($article->publicationStatus != Article::PUBLISHED) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * Unpublishes articles. - */ - public function unpublish() { - $usersToArticles = $articleIDs = []; - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['publicationStatus' => Article::UNPUBLISHED]); - - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID]--; - - $articleIDs[] = $articleEditor->articleID; - } - - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.article.notification', $articleIDs); - - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents('com.woltlab.wcf.article.recentActivityEvent', $articleIDs); - - ArticleEditor::updateArticleCounter($usersToArticles); - - $this->unmarkItems(); - } - - /** - * Validates parameters to search for an article by its localized title. - */ - public function validateSearch() { - $this->readString('searchString'); - } - - /** - * Searches for an article by its localized title. - * - * @return array list of matching articles - */ - public function search() { - $sql = "SELECT articleID - FROM wcf".WCF_N."_article_content - WHERE title LIKE ? - AND ( - languageID = ? - OR languageID IS NULL - ) - ORDER BY title"; - $statement = WCF::getDB()->prepareStatement($sql, 5); - $statement->execute([ - '%' . $this->parameters['searchString'] . '%', - WCF::getLanguage()->languageID, - ]); - - $articleIDs = []; - while ($articleID = $statement->fetchColumn()) { - $articleIDs[] = $articleID; - } - - $articleList = new ArticleList(); - $articleList->setObjectIDs($articleIDs); - $articleList->readObjects(); - - $articles = []; - foreach ($articleList as $article) { - $articles[] = [ - 'displayLink' => $article->getLink(), - 'name' => $article->getTitle(), - 'articleID' => $article->articleID, - ]; - } - - return $articles; - } - - /** - * Unmarks articles. - * - * @param integer[] $articleIDs - */ - protected function unmarkItems(array $articleIDs = []) { - if (empty($articleIDs)) { - foreach ($this->getObjects() as $article) { - $articleIDs[] = $article->articleID; - } - } - - if (!empty($articleIDs)) { - ClipboardHandler::getInstance()->unmark($articleIDs, ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article')); - } - } +class ArticleAction extends AbstractDatabaseObjectAction +{ + /** + * article editor instance + * @var ArticleEditor + */ + public $articleEditor; + + /** + * language object + * @var Language + */ + public $language; + + /** + * @inheritDoc + */ + protected $className = ArticleEditor::class; + + /** + * @inheritDoc + */ + protected $permissionsCreate = ['admin.content.article.canManageArticle']; + + /** + * @inheritDoc + */ + protected $permissionsDelete = ['admin.content.article.canManageArticle']; + + /** + * @inheritDoc + */ + protected $permissionsUpdate = ['admin.content.article.canManageArticle']; + + /** + * @inheritDoc + */ + protected $requireACP = ['create', 'update']; + + /** + * @inheritDoc + */ + protected $allowGuestAccess = ['markAllAsRead']; + + /** + * @inheritDoc + * @return Article + */ + public function create() + { + /** @var Article $article */ + $article = parent::create(); + + // save article content + if (!empty($this->parameters['content'])) { + foreach ($this->parameters['content'] as $languageID => $content) { + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['content'] = $content['htmlInputProcessor']->getHtml(); + } + + /** @var ArticleContent $articleContent */ + $articleContent = ArticleContentEditor::create([ + 'articleID' => $article->articleID, + 'languageID' => $languageID ?: null, + 'title' => $content['title'], + 'teaser' => $content['teaser'], + 'content' => $content['content'], + 'imageID' => $content['imageID'], + 'teaserImageID' => $content['teaserImageID'], + 'metaTitle' => $content['metaTitle'] ?? '', + 'metaDescription' => $content['metaDescription'] ?? '', + ]); + $articleContentEditor = new ArticleContentEditor($articleContent); + + // save tags + if (!empty($content['tags'])) { + TagEngine::getInstance()->addObjectTags( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $content['tags'], + ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()) + ); + } + + // update search index + SearchIndexManager::getInstance()->set( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $articleContent->content, + $articleContent->title, + $article->time, + $article->userID, + $article->username, + $languageID ?: null, + $articleContent->teaser + ); + + // save embedded objects + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); + if (MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { + $articleContentEditor->update(['hasEmbeddedObjects' => 1]); + } + } + } + } + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + if ($article->publicationStatus == Article::PUBLISHED) { + ArticleEditor::updateArticleCounter([$article->userID => 1]); + + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $article->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($article) + ); + + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $article->articleID, + null, + $article->userID, + $article->time + ); + } + + return $article; + } + + /** + * @inheritDoc + */ + public function update() + { + parent::update(); + + $isRevert = (!empty($this->parameters['isRevert'])); + + // update article content + if (!empty($this->parameters['content'])) { + foreach ($this->getObjects() as $article) { + $versionData = []; + $hasChanges = false; + + foreach ($this->parameters['content'] as $languageID => $content) { + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['content'] = $content['htmlInputProcessor']->getHtml(); + } + + $articleContent = ArticleContent::getArticleContent($article->articleID, ($languageID ?: null)); + $articleContentEditor = null; + if ($articleContent !== null) { + // update + $articleContentEditor = new ArticleContentEditor($articleContent); + $articleContentEditor->update([ + 'title' => $content['title'], + 'teaser' => $content['teaser'], + 'content' => $content['content'], + 'imageID' => ($isRevert) ? $articleContent->imageID : $content['imageID'], + 'teaserImageID' => ($isRevert) ? $articleContent->teaserImageID : $content['teaserImageID'], + 'metaTitle' => $content['metaTitle'] ?? '', + 'metaDescription' => $content['metaDescription'] ?? '', + ]); + + $versionData[] = $articleContent; + if ($articleContent->content != $content['content'] || $articleContent->teaser != $content['teaser'] || $articleContent->title != $content['title']) { + $hasChanges = true; + } + + // delete tags + if (!$isRevert && empty($content['tags'])) { + TagEngine::getInstance()->deleteObjectTags( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + ($languageID ?: null) + ); + } + } else { + /** @var ArticleContent $articleContent */ + $articleContent = ArticleContentEditor::create([ + 'articleID' => $article->articleID, + 'languageID' => $languageID ?: null, + 'title' => $content['title'], + 'teaser' => $content['teaser'], + 'content' => $content['content'], + 'imageID' => ($isRevert) ? null : $content['imageID'], + 'teaserImageID' => ($isRevert) ? null : $content['teaserImageID'], + 'metaTitle' => $content['metaTitle'] ?? '', + 'metaDescription' => $content['metaDescription'] ?? '', + ]); + $articleContentEditor = new ArticleContentEditor($articleContent); + + $versionData[] = $articleContent; + $hasChanges = true; + } + + // save tags + if (!$isRevert && !empty($content['tags'])) { + TagEngine::getInstance()->addObjectTags( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $content['tags'], + ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()) + ); + } + + // update search index + SearchIndexManager::getInstance()->set( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $content['content'] ?? $articleContent->content, + $content['title'] ?? $articleContent->title, + $this->parameters['data']['time'] ?? $article->time, + $this->parameters['data']['userID'] ?? $article->userID, + $this->parameters['data']['username'] ?? $article->username, + $languageID ?: null, + $content['teaser'] ?? $articleContent->teaser + ); + + // save embedded objects + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); + if ($articleContent->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { + $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ? 0 : 1]); + } + } + } + + if ($hasChanges) { + $articleObj = new ArticleVersionTracker($article->getDecoratedObject()); + $articleObj->setContent($versionData); + VersionTracker::getInstance()->add('com.woltlab.wcf.article', $articleObj); + } + } + } + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + $publicationStatus = (isset($this->parameters['data']['publicationStatus'])) ? $this->parameters['data']['publicationStatus'] : null; + if ($publicationStatus !== null) { + $usersToArticles = $resetArticleIDs = []; + /** @var ArticleEditor $articleEditor */ + foreach ($this->objects as $articleEditor) { + if ($publicationStatus != $articleEditor->publicationStatus) { + // The article was published before or was now published. + if ($publicationStatus == Article::PUBLISHED || $articleEditor->publicationStatus == Article::PUBLISHED) { + if (!isset($usersToArticles[$articleEditor->userID])) { + $usersToArticles[$articleEditor->userID] = 0; + } + + $usersToArticles[$articleEditor->userID] += ($publicationStatus == Article::PUBLISHED) ? 1 : -1; + } + + if ($publicationStatus == Article::PUBLISHED) { + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $articleEditor->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) + ); + + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleEditor->articleID, + null, + $this->parameters['data']['userID'] ?? $articleEditor->userID, + $this->parameters['data']['time'] ?? $articleEditor->time + ); + } else { + $resetArticleIDs[] = $articleEditor->articleID; + } + } + } + + if (!empty($resetArticleIDs)) { + // delete user notifications + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + $resetArticleIDs + ); + // delete recent activity events + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + $resetArticleIDs + ); + } + + if (!empty($usersToArticles)) { + ArticleEditor::updateArticleCounter($usersToArticles); + } + } + + // update author in recent activities + if (isset($this->parameters['data']['userID'])) { + $sql = "UPDATE wcf" . WCF_N . "_user_activity_event + SET userID = ? + WHERE objectTypeID = ? + AND objectID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + foreach ($this->objects as $articleEditor) { + if ($articleEditor->userID != $this->parameters['data']['userID']) { + $statement->execute([ + $this->parameters['data']['userID'], + UserActivityEventHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article.recentActivityEvent'), + $articleEditor->articleID, + ]); + } + } + } + } + + /** + * Validates parameters to delete articles. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validateDelete() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canDelete()) { + throw new PermissionDeniedException(); + } + + if (!$article->isDeleted) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * @inheritDoc + */ + public function delete() + { + $usersToArticles = $articleIDs = $articleContentIDs = []; + foreach ($this->getObjects() as $article) { + $articleIDs[] = $article->articleID; + foreach ($article->getArticleContents() as $articleContent) { + $articleContentIDs[] = $articleContent->articleContentID; + } + + if ($article->publicationStatus == Article::PUBLISHED) { + if (!isset($usersToArticles[$article->userID])) { + $usersToArticles[$article->userID] = 0; + } + $usersToArticles[$article->userID]--; + } + } + + // delete articles + parent::delete(); + + if (!empty($articleIDs)) { + // delete like data + ReactionHandler::getInstance()->removeReactions('com.woltlab.wcf.likeableArticle', $articleIDs); + // delete comments + CommentHandler::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs); + // delete tag to object entries + TagEngine::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs); + // delete entry from search index + SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs); + // delete user notifications + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + $articleIDs + ); + // delete recent activity events + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleIDs + ); + // delete embedded object references + MessageEmbeddedObjectManager::getInstance()->removeObjects( + 'com.woltlab.wcf.article.content', + $articleContentIDs + ); + // update wcf1_user.articles + ArticleEditor::updateArticleCounter($usersToArticles); + } + + $this->unmarkItems(); + + return [ + 'objectIDs' => $this->objectIDs, + 'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true]), + ]; + } + + /** + * Validates parameters to move articles to the trash bin. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validateTrash() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canDelete()) { + throw new PermissionDeniedException(); + } + + if ($article->isDeleted) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * Moves articles to the trash bin. + */ + public function trash() + { + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['isDeleted' => 1]); + } + + $this->unmarkItems(); + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + return ['objectIDs' => $this->objectIDs]; + } + + /** + * Validates parameters to restore articles. + * + * @throws UserInputException + */ + public function validateRestore() + { + $this->validateDelete(); + } + + /** + * Restores articles. + */ + public function restore() + { + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['isDeleted' => 0]); + } + + $this->unmarkItems(); + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + return ['objectIDs' => $this->objectIDs]; + } + + /** + * Validates parameters to toggle between i18n and monolingual mode. + * + * @throws UserInputException + */ + public function validateToggleI18n() + { + WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); + + $this->articleEditor = $this->getSingleObject(); + if ($this->articleEditor->getDecoratedObject()->isMultilingual) { + $this->readInteger('languageID'); + $this->language = LanguageFactory::getInstance()->getLanguage($this->parameters['languageID']); + if ($this->language === null) { + throw new UserInputException('languageID'); + } + + $contents = $this->articleEditor->getArticleContents(); + if (!isset($contents[$this->language->languageID])) { + // there is no content + throw new UserInputException('languageID'); + } + } + } + + /** + * Toggles between i18n and monolingual mode. + */ + public function toggleI18n() + { + $removeContent = []; + + // i18n -> monolingual + if ($this->articleEditor->getDecoratedObject()->isMultilingual) { + foreach ($this->articleEditor->getArticleContents() as $articleContent) { + if ($articleContent->languageID == $this->language->languageID) { + $articleContentEditor = new ArticleContentEditor($articleContent); + $articleContentEditor->update(['languageID' => null]); + } else { + $removeContent[] = $articleContent; + } + } + } else { + // monolingual -> i18n + $articleContent = $this->articleEditor->getArticleContent(); + $data = []; + foreach (LanguageFactory::getInstance()->getLanguages() as $language) { + $data[$language->languageID] = [ + 'title' => $articleContent->title, + 'teaser' => $articleContent->teaser, + 'content' => $articleContent->content, + 'imageID' => $articleContent->imageID ?: null, + 'teaserImageID' => $articleContent->teaserImageID ?: null, + ]; + } + + $action = new self([$this->articleEditor], 'update', ['content' => $data]); + $action->executeAction(); + + $removeContent[] = $articleContent; + } + + if (!empty($removeContent)) { + $action = new ArticleContentAction($removeContent, 'delete'); + $action->executeAction(); + } + + // flush edit history + VersionTracker::getInstance()->reset( + 'com.woltlab.wcf.article', + $this->articleEditor->getDecoratedObject()->articleID + ); + + // update article's i18n state + $this->articleEditor->update([ + 'isMultilingual' => ($this->articleEditor->getDecoratedObject()->isMultilingual) ? 0 : 1, + ]); + } + + /** + * Marks articles as read. + */ + public function markAsRead() + { + if (empty($this->parameters['visitTime'])) { + $this->parameters['visitTime'] = TIME_NOW; + } + + if (empty($this->objects)) { + $this->readObjects(); + } + + foreach ($this->getObjects() as $article) { + VisitTracker::getInstance()->trackObjectVisit( + 'com.woltlab.wcf.article', + $article->articleID, + $this->parameters['visitTime'] + ); + } + + // reset storage + if (WCF::getUser()->userID) { + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); + } + } + + /** + * Marks all articles as read. + */ + public function markAllAsRead() + { + VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article'); + + // reset storage + if (WCF::getUser()->userID) { + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); + } + } + + /** + * Validates the mark all as read action. + */ + public function validateMarkAllAsRead() + { + // does nothing + } + + /** + * Validates the `setCategory` action. + * + * @throws UserInputException + */ + public function validateSetCategory() + { + WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); + + $this->readBoolean('useMarkedArticles', true); + + // if no object ids are given, use clipboard handler + if (empty($this->objectIDs) && $this->parameters['useMarkedArticles']) { + $this->objectIDs = \array_keys(ClipboardHandler::getInstance()->getMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article'))); + } + + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + $this->readInteger('categoryID'); - if (ArticleCategory::getCategory($this->parameters['categoryID']) === null) { ++ $category = ArticleCategory::getCategory($this->parameters['categoryID']); ++ if ($category === null) { ++ throw new UserInputException('categoryID'); ++ } ++ if (!$category->isAccessible()) { + throw new UserInputException('categoryID'); + } + } + + /** + * Sets the category of articles. + */ + public function setCategory() + { + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['categoryID' => $this->parameters['categoryID']]); + } + + $this->unmarkItems(); + } + + /** + * Validates the `publish` action. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validatePublish() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canPublish()) { + throw new PermissionDeniedException(); + } + + if ($article->publicationStatus == Article::PUBLISHED) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * Publishes articles. + */ + public function publish() + { + $usersToArticles = []; + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update([ + 'time' => TIME_NOW, + 'publicationStatus' => Article::PUBLISHED, + 'publicationDate' => 0, + ]); + + if (!isset($usersToArticles[$articleEditor->userID])) { + $usersToArticles[$articleEditor->userID] = 0; + } + + $usersToArticles[$articleEditor->userID]++; + + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $articleEditor->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) + ); + + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleEditor->articleID, + null, + $articleEditor->userID, + TIME_NOW + ); + } + + ArticleEditor::updateArticleCounter($usersToArticles); + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + $this->unmarkItems(); + } + + /** + * Validates the `unpublish` action. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validateUnpublish() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canPublish()) { + throw new PermissionDeniedException(); + } + + if ($article->publicationStatus != Article::PUBLISHED) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * Unpublishes articles. + */ + public function unpublish() + { + $usersToArticles = $articleIDs = []; + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['publicationStatus' => Article::UNPUBLISHED]); + + if (!isset($usersToArticles[$articleEditor->userID])) { + $usersToArticles[$articleEditor->userID] = 0; + } + + $usersToArticles[$articleEditor->userID]--; + + $articleIDs[] = $articleEditor->articleID; + } + + // delete user notifications + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + $articleIDs + ); + + // delete recent activity events + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleIDs + ); + + ArticleEditor::updateArticleCounter($usersToArticles); + + $this->unmarkItems(); + } + + /** + * Validates parameters to search for an article by its localized title. + */ + public function validateSearch() + { + $this->readString('searchString'); + } + + /** + * Searches for an article by its localized title. + * + * @return array list of matching articles + */ + public function search() + { + $sql = "SELECT articleID + FROM wcf" . WCF_N . "_article_content + WHERE title LIKE ? + AND ( + languageID = ? + OR languageID IS NULL + ) + ORDER BY title"; + $statement = WCF::getDB()->prepareStatement($sql, 5); + $statement->execute([ + '%' . $this->parameters['searchString'] . '%', + WCF::getLanguage()->languageID, + ]); + + $articleIDs = []; + while ($articleID = $statement->fetchColumn()) { + $articleIDs[] = $articleID; + } + + $articleList = new ArticleList(); + $articleList->setObjectIDs($articleIDs); + $articleList->readObjects(); + + $articles = []; + foreach ($articleList as $article) { + $articles[] = [ + 'displayLink' => $article->getLink(), + 'name' => $article->getTitle(), + 'articleID' => $article->articleID, + ]; + } + + return $articles; + } + + /** + * Unmarks articles. + * + * @param int[] $articleIDs + */ + protected function unmarkItems(array $articleIDs = []) + { + if (empty($articleIDs)) { + foreach ($this->getObjects() as $article) { + $articleIDs[] = $article->articleID; + } + } + + if (!empty($articleIDs)) { + ClipboardHandler::getInstance()->unmark( + $articleIDs, + ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article') + ); + } + } } diff --cc wcfsetup/install/files/lib/system/clipboard/action/ArticleClipboardAction.class.php index 39eb6f81b8,4502f6f523..a7cbb62eed --- a/wcfsetup/install/files/lib/system/clipboard/action/ArticleClipboardAction.class.php +++ b/wcfsetup/install/files/lib/system/clipboard/action/ArticleClipboardAction.class.php @@@ -10,198 -8,185 +10,207 @@@ use wcf\system\WCF /** * Clipboard action implementation for articles. - * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @package WoltLabSuite\Core\System\Clipboard\Action - * @since 3.1 + * + * @author Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @package WoltLabSuite\Core\System\Clipboard\Action + * @since 3.1 */ -class ArticleClipboardAction extends AbstractClipboardAction { - /** - * @inheritDoc - */ - protected $actionClassActions = [ - 'delete', - 'publish', - 'restore', - 'trash', - 'unpublish' - ]; - - /** - * @inheritDoc - */ - protected $supportedActions = [ - 'delete', - 'publish', - 'restore', - 'setCategory', - 'trash', - 'unpublish' - ]; - - /** - * @inheritDoc - */ - public function execute(array $objects, ClipboardAction $action) { - $item = parent::execute($objects, $action); - - if ($item === null) { - return null; - } - - // handle actions - switch ($action->actionName) { - case 'delete': - $item->addInternalData('confirmMessage', WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.com.woltlab.wcf.article.delete.confirmMessage', [ - 'count' => $item->getCount() - ])); - break; - - case 'setCategory': - $item->addInternalData('template', WCF::getTPL()->fetch('articleCategoryDialog', 'wcf', [ - 'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator() - ])); - break; - - case 'trash': - $item->addInternalData('confirmMessage', WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.com.woltlab.wcf.article.trash.confirmMessage', [ - 'count' => $item->getCount() - ])); - break; - } - - return $item; - } - - /** - * @inheritDoc - */ - public function getClassName() { - return ArticleAction::class; - } - - /** - * @inheritDoc - */ - public function getTypeName() { - return 'com.woltlab.wcf.article'; - } - - /** - * Returns the ids of the articles that can be deleted. - * - * @return integer[] - */ - public function validateDelete() { - $objectIDs = []; - - /** @var Article $article */ - foreach ($this->objects as $article) { - if ($article->canDelete() && $article->isDeleted) { - $objectIDs[] = $article->articleID; - } - } - - return $objectIDs; - } - - /** - * Returns the ids of the articles that can be published. - * - * @return integer[] - */ - public function validatePublish() { - $objectIDs = []; - - /** @var Article $article */ - foreach ($this->objects as $article) { - if ($article->canPublish() && $article->publicationStatus == Article::UNPUBLISHED) { - $objectIDs[] = $article->articleID; - } - } - - return $objectIDs; - } - - /** - * Returns the ids of the articles that can be restored. - * - * @return integer[] - */ - public function validateRestore() { - return $this->validateDelete(); - } - - /** - * Returns the ids of the articles whose category can be set. - * - * @return integer[] - */ - public function validateSetCategory() { - if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) { - return []; - } - - $objectIDs = []; - - /** @var Article $article */ - foreach ($this->objects as $article) { - if ($article->canEdit()) { - $objectIDs[] = $article->articleID; - } - } - - return $objectIDs; - } - - /** - * Returns the ids of the articles that can be trashed. - * - * @return integer[] - */ - public function validateTrash() { - $objectIDs = []; - - /** @var Article $article */ - foreach ($this->objects as $article) { - if ($article->canDelete() && !$article->isDeleted) { - $objectIDs[] = $article->articleID; - } - } - - return $objectIDs; - } - - /** - * Returns the ids of the articles that can be unpublished. - * - * @return integer[] - */ - public function validateUnpublish() { - $objectIDs = []; - - /** @var Article $article */ - foreach ($this->objects as $article) { - if ($article->canPublish() && $article->publicationStatus == Article::PUBLISHED) { - $objectIDs[] = $article->articleID; - } - } - - return $objectIDs; - } +class ArticleClipboardAction extends AbstractClipboardAction +{ + /** + * @inheritDoc + */ + protected $actionClassActions = [ + 'delete', + 'publish', + 'restore', + 'trash', + 'unpublish', + ]; + + /** + * @inheritDoc + */ + protected $supportedActions = [ + 'delete', + 'publish', + 'restore', + 'setCategory', + 'trash', + 'unpublish', + ]; + + /** + * @inheritDoc + */ + public function execute(array $objects, ClipboardAction $action) + { + $item = parent::execute($objects, $action); + + if ($item === null) { + return; + } + + // handle actions + switch ($action->actionName) { + case 'delete': + $item->addInternalData( + 'confirmMessage', + WCF::getLanguage()->getDynamicVariable( + 'wcf.clipboard.item.com.woltlab.wcf.article.delete.confirmMessage', + [ + 'count' => $item->getCount(), + ] + ) + ); + break; + + case 'setCategory': + $item->addInternalData('template', WCF::getTPL()->fetch('articleCategoryDialog', 'wcf', [ + 'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator(), + ])); + break; + + case 'trash': + $item->addInternalData( + 'confirmMessage', + WCF::getLanguage()->getDynamicVariable( + 'wcf.clipboard.item.com.woltlab.wcf.article.trash.confirmMessage', + [ + 'count' => $item->getCount(), + ] + ) + ); + break; + } + + return $item; + } + + /** + * @inheritDoc + */ + public function getClassName() + { + return ArticleAction::class; + } + + /** + * @inheritDoc + */ + public function getTypeName() + { + return 'com.woltlab.wcf.article'; + } + + /** + * Returns the ids of the articles that can be deleted. + * + * @return int[] + */ + public function validateDelete() + { + $objectIDs = []; + + /** @var Article $article */ + foreach ($this->objects as $article) { + if ($article->canDelete() && $article->isDeleted) { + $objectIDs[] = $article->articleID; + } + } + + return $objectIDs; + } + + /** + * Returns the ids of the articles that can be published. + * + * @return int[] + */ + public function validatePublish() + { + $objectIDs = []; + + /** @var Article $article */ + foreach ($this->objects as $article) { + if ($article->canPublish() && $article->publicationStatus == Article::UNPUBLISHED) { + $objectIDs[] = $article->articleID; + } + } + + return $objectIDs; + } + + /** + * Returns the ids of the articles that can be restored. + * + * @return int[] + */ + public function validateRestore() + { + return $this->validateDelete(); + } + + /** + * Returns the ids of the articles whose category can be set. + * + * @return int[] + */ + public function validateSetCategory() + { + if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) { + return []; + } + - return \array_keys($this->objects); ++ $objectIDs = []; ++ ++ /** @var Article $article */ ++ foreach ($this->objects as $article) { ++ if ($article->canEdit()) { ++ $objectIDs[] = $article->articleID; ++ } ++ } ++ ++ return $objectIDs; + } + + /** + * Returns the ids of the articles that can be trashed. + * + * @return int[] + */ + public function validateTrash() + { + $objectIDs = []; + + /** @var Article $article */ + foreach ($this->objects as $article) { + if ($article->canDelete() && !$article->isDeleted) { + $objectIDs[] = $article->articleID; + } + } + + return $objectIDs; + } + + /** + * Returns the ids of the articles that can be unpublished. + * + * @return int[] + */ + public function validateUnpublish() + { + $objectIDs = []; + + /** @var Article $article */ + foreach ($this->objects as $article) { + if ($article->canPublish() && $article->publicationStatus == Article::PUBLISHED) { + $objectIDs[] = $article->articleID; + } + } + + return $objectIDs; + } }