<option name="canSetLabel">
<objecttype>com.woltlab.wcf.label</objecttype>
</option>
+
+ <option name="canReadArticle">
+ <objecttype>com.woltlab.wcf.article.category</objecttype>
+ </option>
</options>
</import>
</data>
\ No newline at end of file
<icon>fa-plus</icon>
</acpmenuitem>
<!-- /cms -->
+
+ <!-- article -->
+ <acpmenuitem name="wcf.acp.menu.link.article">
+ <parent>wcf.acp.menu.link.content</parent>
+ <options>module_article</options>
+ <showorder>2</showorder>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.article.list">
+ <controller><![CDATA[wcf\acp\page\ArticleListPage]]></controller>
+ <parent>wcf.acp.menu.link.article</parent>
+ <permissions>admin.content.article.canManageArticle</permissions>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.article.add">
+ <controller><![CDATA[wcf\acp\form\ArticleAddForm]]></controller>
+ <parent>wcf.acp.menu.link.article.list</parent>
+ <permissions>admin.content.article.canManageArticle</permissions>
+ <icon>fa-plus</icon>
+ </acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.article.category.list">
+ <controller><![CDATA[wcf\acp\page\ArticleCategoryListPage]]></controller>
+ <parent>wcf.acp.menu.link.article</parent>
+ <permissions>admin.content.article.canManageCategory</permissions>
+ </acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.article.category.add">
+ <controller><![CDATA[wcf\acp\form\ArticleCategoryAddForm]]></controller>
+ <parent>wcf.acp.menu.link.article.category.list</parent>
+ <permissions>admin.content.article.canManageCategory</permissions>
+ <icon>fa-plus</icon>
+ </acpmenuitem>
+ <!-- /article -->
+
<!-- label -->
<acpmenuitem name="wcf.acp.menu.link.label">
<parent>wcf.acp.menu.link.content</parent>
- <showorder>2</showorder>
+ <showorder>3</showorder>
</acpmenuitem>
<acpmenuitem name="wcf.acp.menu.link.label.list">
<controller><![CDATA[wcf\acp\page\LabelListPage]]></controller>
<!-- bbcode -->
<acpmenuitem name="wcf.acp.menu.link.bbcode">
<parent>wcf.acp.menu.link.content</parent>
- <showorder>3</showorder>
+ <showorder>4</showorder>
</acpmenuitem>
<acpmenuitem name="wcf.acp.menu.link.bbcode.list">
<controller><![CDATA[wcf\acp\page\BBCodeListPage]]></controller>
<acpmenuitem name="wcf.acp.menu.link.tag">
<parent>wcf.acp.menu.link.content</parent>
<options>module_tagging</options>
- <showorder>4</showorder>
+ <showorder>5</showorder>
</acpmenuitem>
<acpmenuitem name="wcf.acp.menu.link.tag.list">
<!-- attachment -->
<acpmenuitem name="wcf.acp.menu.link.attachment">
<parent>wcf.acp.menu.link.content</parent>
- <showorder>5</showorder>
+ <showorder>6</showorder>
</acpmenuitem>
<acpmenuitem name="wcf.acp.menu.link.attachment.list">
<title language="en"><![CDATA[Dashboard]]></title>
<page>com.woltlab.wcf.Dashboard</page>
</item>
+ <item identifier="com.woltlab.wcf.ArticleList">
+ <menu>com.woltlab.wcf.MainMenu</menu>
+ <title language="de"><![CDATA[Artikel]]></title>
+ <title language="en"><![CDATA[Articles]]></title>
+ <page>com.woltlab.wcf.ArticleList</page>
+ </item>
<item identifier="com.woltlab.wcf.MembersList">
<menu>com.woltlab.wcf.MainMenu</menu>
<definitionname>com.woltlab.wcf.collapsibleContent</definitionname>
</type>
+ <!-- articles -->
+ <type>
+ <name>com.woltlab.wcf.article.category</name>
+ <definitionname>com.woltlab.wcf.category</definitionname>
+ <classname>wcf\system\category\ArticleCategoryType</classname>
+ </type>
+ <type>
+ <name>com.woltlab.wcf.article.category</name>
+ <definitionname>com.woltlab.wcf.acl</definitionname>
+ </type>
+ <type>
+ <name>com.woltlab.wcf.article</name>
+ <definitionname>com.woltlab.wcf.tagging.taggableObject</definitionname>
+ <classname>wcf\system\tagging\TaggableArticle</classname>
+ </type>
+ <type>
+ <name>com.woltlab.wcf.likeableArticle</name>
+ <definitionname>com.woltlab.wcf.like.likeableObject</definitionname>
+ <classname>wcf\data\article\LikeableArticleProvider</classname>
+ </type>
+
+ <!-- /articles -->
+
<type>
<name>com.woltlab.wcf.bbcode.smiley</name>
<definitionname>com.woltlab.wcf.category</definitionname>
<definitionname>com.woltlab.wcf.comment.commentableContent</definitionname>
<classname><![CDATA[wcf\system\comment\manager\PageCommentManager]]></classname>
</type>
+
+ <type>
+ <name>com.woltlab.wcf.article</name>
+ <definitionname>com.woltlab.wcf.comment.commentableContent</definitionname>
+ <classname><![CDATA[wcf\system\comment\manager\ArticleCommentManager]]></classname>
+ </type>
<!-- /comments -->
<!-- moderation -->
<category name="cms.media.thumbnail">
<parent>cms.media</parent>
</category>
+ <category name="cms.article">
+ <parent>cms</parent>
+ </category>
<!-- /cms -->
</categories>
<defaultvalue>1</defaultvalue>
</option>
+ <option name="module_article">
+ <categoryname>module.content</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ </option>
+
<option name="module_attachment">
<categoryname>module.content</categoryname>
<optiontype>boolean</optiontype>
</option>
<!-- /message.general.poll -->
- <!-- cms.media.thumnail -->
+ <!-- cms.media.thumbnail -->
<option name="media_small_thumbnail_width">
<categoryname>cms.media.thumbnail</categoryname>
<optiontype>integer</optiontype>
<optiontype>boolean</optiontype>
<defaultvalue>1</defaultvalue>
</option>
- <!-- cms.media.thumnail -->
+ <!-- /cms.media.thumbnail -->
+
+ <!-- cms.article -->
+ <option name="article_show_about_author">
+ <categoryname>cms.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <options>module_article</options>
+ </option>
+ <option name="article_enable_comments_default_value">
+ <categoryname>cms.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <options>module_article</options>
+ </option>
+ <option name="article_enable_like">
+ <categoryname>cms.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <options>module_article</options>
+ </option>
+ <option name="articles_per_page">
+ <categoryname>cms.article</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>20</defaultvalue>
+ <options>module_article</options>
+ <minvalue>1</minvalue>
+ </option>
+ <option name="article_related_articles">
+ <categoryname>cms.article</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>3</defaultvalue>
+ <options>module_article</options>
+ <minvalue>0</minvalue>
+ </option>
+ <option name="article_related_articles_match_threshold">
+ <categoryname>cms.article</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>50</defaultvalue>
+ <options>module_article</options>
+ <minvalue>1</minvalue>
+ <maxvalue>100</maxvalue>
+ <suffix>percent</suffix>
+ </option>
+ <!-- /cms.article -->
</options>
</import>
<parent>com.woltlab.wcf.MembersList</parent>
<requireObjectID>1</requireObjectID>
</page>
+ <page identifier="com.woltlab.wcf.ArticleList">
+ <pageType>system</pageType>
+ <controller>wcf\page\ArticleListPage</controller>
+ <name language="de"><![CDATA[Artikel-Liste]]></name>
+ <name language="en"><![CDATA[Article List]]></name>
+ <options>module_article</options>
+
+ <content language="en">
+ <title>Articles</title>
+ </content>
+ <content language="de">
+ <title>Artikel</title>
+ </content>
+ </page>
+ <page identifier="com.woltlab.wcf.CategoryArticleList">
+ <pageType>system</pageType>
+ <controller>wcf\page\CategoryArticleListPage</controller>
+ <handler>wcf\system\page\handler\CategoryArticleListPageHandler</handler>
+ <name language="de"><![CDATA[Liste von Artikeln aus bestimmter Kategorie]]></name>
+ <name language="en"><![CDATA[List of Articles in Certain Category]]></name>
+ <options>module_article</options>
+ <parent>com.woltlab.wcf.ArticleList</parent>
+ <requireObjectID>1</requireObjectID>
+ </page>
+ <page identifier="com.woltlab.wcf.Article">
+ <pageType>system</pageType>
+ <controller>wcf\page\ArticlePage</controller>
+ <handler>wcf\system\page\handler\ArticlePageHandler</handler>
+ <name language="de"><![CDATA[Artikel]]></name>
+ <name language="en"><![CDATA[Article]]></name>
+ <options>module_article</options>
+ <parent>com.woltlab.wcf.CategoryArticleList</parent>
+ <requireObjectID>1</requireObjectID>
+ </page>
<!-- static -->
<page identifier="com.woltlab.wcf.Dashboard">
--- /dev/null
+{capture assign='pageTitle'}{$articleContent->title}{/capture}
+
+{capture assign='contentHeader'}
+ <header class="contentHeader articleContentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{$articleContent->title}</h1>
+ <div class="contentHeaderDescription">
+ {$articleContent->getArticle()->getCategory()->getTitle()}
+ </div>
+ <ul class="inlineList contentHeaderMetaData articleMetaData">
+ <li>
+ <span class="icon icon16 fa-user"></span>
+ {if $article->userID}
+ <a href="{link controller='User' id=$article->userID title=$article->username}{/link}" class="userLink" data-user-id="{@$article->userID}">{$article->username}</a>
+ {else}
+ {$article->username}
+ {/if}
+ </li>
+
+ <li>
+ <span class="icon icon16 fa-clock-o"></span>
+ {@$article->time|time}
+ </li>
+
+ <li>
+ <span class="icon icon16 fa-comments"></span>
+ {lang}wcf.article.articleComments{/lang}
+ </li>
+
+ <li>
+ <span class="icon icon16 fa-eye"></span>
+ {lang}wcf.article.articleViews{/lang}
+ </li>
+
+ <li class="articleLikesBadge"></li>
+ </ul>
+ </div>
+
+ {hascontent}
+ <nav class="contentHeaderNavigation">
+ <ul>
+ {content}
+ {event name='contentHeaderNavigation'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+ </header>
+{/capture}
+
+{include file='header'}
+
+{if $articleContent->getImage()}
+ <section class="section">
+ <figure class="articleImage">
+ <div class="articleImageWrapper">{@$articleContent->getImage()->getThumbnailTag('large')}</div>
+ {if $articleContent->getImage()->caption}
+ <figcaption>{$articleContent->getImage()->caption}</figcaption>
+ {/if}
+ </figure>
+ </section>
+{/if}
+
+<section class="section articleContent"
+ data-object-id="{@$article->articleID}"
+ data-object-type="com.woltlab.wcf.likeableArticle" data-like-liked="{if $articleLikeData[$article->articleID]|isset}{@$articleLikeData[$article->articleID]->liked}{/if}" data-like-likes="{if $articleLikeData[$article->articleID]|isset}{@$articleLikeData[$article->articleID]->likes}{else}0{/if}" data-like-dislikes="{if $articleLikeData[$article->articleID]|isset}{@$articleLikeData[$article->articleID]->dislikes}{else}0{/if}" data-like-users='{ {if $articleLikeData[$article->articleID]|isset}{implode from=$articleLikeData[$article->articleID]->getUsers() item=likeUser}"{@$likeUser->userID}": "{$likeUser->username|encodeJSON}"{/implode}{/if} }' data-user-id="{@$article->userID}"
+>
+ <div class="htmlContent">
+ {if $articleContent->teaser}
+ <p class="articleTeaser">{$articleContent->teaser}</p>
+ {/if}
+
+ {@$articleContent->getFormattedContent()}
+ </div>
+
+ {if !$tags|empty}
+ <ul class="tagList articleTagList">
+ {foreach from=$tags item=tag}
+ <li><a href="{link controller='Tagged' object=$tag}objectType=com.woltlab.wcf.article{/link}" class="articleTag">{$tag->name}</a></li>
+ {/foreach}
+ </ul>
+ {/if}
+
+ <div class="articleLikesSummery"></div>
+
+ <ul class="articleLikeButtons buttonGroup"></ul>
+
+ {if ENABLE_SHARE_BUTTONS}
+ <section class="section jsOnly">
+ <h2 class="sectionTitle">{lang}wcf.message.share{/lang}</h2>
+
+ {include file='shareButtons'}
+ </section>
+ {/if}
+</section>
+
+{if ARTICLE_SHOW_ABOUT_AUTHOR}
+ <div class="section articleAboutAuthor">
+ <h2 class="sectionTitle">{lang}wcf.article.aboutAuthor{/lang}</h2>
+
+ <div class="box128">
+ <span class="articleAboutAuthorAvatar">{@$article->getUserProfile()->getAvatar()->getImageTag(128)}</span>
+
+ <div>
+ <div class="articleAboutAuthorText">{$article->getUserProfile()->aboutMe}</div>
+
+ <div class="articleAboutAuthorUsername">
+ <a href="{link controller='User' object=$article->getUserProfile()->getDecoratedObject()}{/link}" class="username userLink" data-user-id="{@$article->getUserProfile()->userID}" rel="author">{if MESSAGE_SIDEBAR_ENABLE_USER_ONLINE_MARKING}{@$article->getUserProfile()->getFormattedUsername()}{else}{$article->getUserProfile()->username}{/if}</a>
+
+ {if MODULE_USER_RANK}
+ {if $article->getUserProfile()->getUserTitle()}
+ <span class="badge userTitleBadge{if $article->getUserProfile()->getRank() && $article->getUserProfile()->getRank()->cssClassName} {@$article->getUserProfile()->getRank()->cssClassName}{/if}">{$article->getUserProfile()->getUserTitle()}</span>
+ {/if}
+ {if $article->getUserProfile()->getRank() && $article->getUserProfile()->getRank()->rankImage}
+ <span class="userRank">{@$article->getUserProfile()->getRank()->getImage()}</span>
+ {/if}
+ {/if}
+ </div>
+ </div>
+ </div>
+ </div>
+{/if}
+
+<footer class="contentFooter">
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</footer>
+
+{if $previousArticle || $nextArticle}
+ <div class="section articleNavigation">
+ <nav>
+ <ul>
+ {if $previousArticle}
+ <li class="previousArticleButton">
+ <a href="{$previousArticle->getLink()}" rel="prev">
+ {if $previousArticle->getImage()}
+ <div class="box96">
+ <span class="articleNavigationArticleImage">{@$previousArticle->getImage()->getElementTag(96)}</span>
+
+ <div>
+ <span class="articleNavigationEntityName">{lang}wcf.article.previousArticle{/lang}</span>
+ <span class="articleNavigationArticleTitle">{$previousArticle->getTitle()}</span>
+ </div>
+ </div>
+ {else}
+ <div>
+ <span class="articleNavigationEntityName">{lang}wcf.article.previousArticle{/lang}</span>
+ <span class="articleNavigationArticleTitle">{$previousArticle->getTitle()}</span>
+ </div>
+ {/if}
+ </a>
+ </li>
+ {/if}
+
+ {if $nextArticle}
+ <li class="nextArticleButton">
+ <a href="{$nextArticle->getLink()}" rel="next">
+ {if $nextArticle->getImage()}
+ <div class="box96">
+ <span class="articleNavigationArticleImage">{@$nextArticle->getImage()->getElementTag(96)}</span>
+
+ <div>
+ <span class="articleNavigationEntityName">{lang}wcf.article.nextArticle{/lang}</span>
+ <span class="articleNavigationArticleTitle">{$nextArticle->getTitle()}</span>
+ </div>
+ </div>
+ {else}
+ <div>
+ <span class="articleNavigationEntityName">{lang}wcf.article.nextArticle{/lang}</span>
+ <span class="articleNavigationArticleTitle">{$nextArticle->getTitle()}</span>
+ </div>
+ {/if}
+ </a>
+ </li>
+ {/if}
+ </ul>
+ </nav>
+ </div>
+{/if}
+
+{if $relatedArticles|count}
+ <section class="section relatedArticles">
+ <h2 class="sectionTitle">{lang}wcf.article.relatedArticles{/lang}</h2>
+
+ <ul class="articleList">
+ {foreach from=$relatedArticles item='relatedArticle'}
+ <li>
+ <a href="{$relatedArticle->getLink()}">
+ {if $relatedArticle->getImage()}
+ <div class="box128">
+ <div class="articleListImage">{@$relatedArticle->getImage()->getThumbnailTag('tiny')}</div>
+ {/if}
+
+ <div>
+ <div class="containerHeadline">
+ <h3 class="articleListTitle">{$relatedArticle->getTitle()}</h3>
+ <ul class="inlineList articleListMetaData">
+ <li>
+ <span class="icon icon16 fa-clock-o"></span>
+ {@$relatedArticle->time|time}
+ </li>
+
+ <li>
+ <span class="icon icon16 fa-comments"></span>
+ {lang article=$relatedArticle}wcf.article.articleComments{/lang}
+ </li>
+
+ {if MODULE_LIKE && $__wcf->getSession()->getPermission('user.like.canViewLike')}
+ <li>
+ {if $relatedArticle->likes || $relatedArticle->dislikes}
+ <span class="icon icon16 fa-thumbs-o-{if $relatedArticle->cumulativeLikes < 0}down{else}up{/if} jsTooltip" title="{lang likes=$relatedArticle->likes dislikes=$relatedArticle->dislikes}wcf.like.tooltip{/lang}"></span>
+ {if $relatedArticle->cumulativeLikes > 0}+{elseif $relatedArticle->cumulativeLikes == 0}±{/if}{#$relatedArticle->cumulativeLikes}
+ {/if}
+ </li>
+ {/if}
+ </ul>
+ </div>
+
+ <div class="containerContent articleListTeaser">
+ {$relatedArticle->getTeaser()}
+ </div>
+ </div>
+
+ {if $relatedArticle->getImage()}
+ </div>
+ {/if}
+ </a>
+ </li>
+ {/foreach}
+ </ul>
+ </section>
+{/if}
+
+{if $article->enableComments}
+ {if $commentList|count || $commentCanAdd}
+ <section class="section sectionContainerList">
+ <h2 class="sectionTitle">{lang}wcf.article.comments{/lang}{if $article->comments} <span class="badge">{#$article->comments}</span>{/if}</h2>
+
+ {include file='__commentJavaScript' commentContainerID='articleCommentList'}
+
+ <ul id="articleCommentList" class="commentList containerList" data-can-add="{if $commentCanAdd}true{else}false{/if}" data-object-id="{@$articleContentID}" data-object-type-id="{@$commentObjectTypeID}" data-comments="{@$commentList->countObjects()}" data-last-comment-time="{@$lastCommentTime}">
+ {include file='commentList'}
+ </ul>
+ </section>
+ {/if}
+{/if}
+
+{if MODULE_LIKE && ARTICLE_ENABLE_LIKE}
+ <script data-relocate="true">
+ require(['WoltLab/WCF/Ui/Like/Handler'], function(UiLikeHandler) {
+ new UiLikeHandler('com.woltlab.wcf.likeableArticle', {
+ // settings
+ isSingleItem: true,
+
+ // permissions
+ canDislike: {if LIKE_ENABLE_DISLIKE}true{else}false{/if},
+ canLike: {if $__wcf->getUser()->userID}true{else}false{/if},
+ canLikeOwnContent: {if LIKE_ALLOW_FOR_OWN_CONTENT}true{else}false{/if},
+ canViewSummary: {if LIKE_SHOW_SUMMARY}true{else}false{/if},
+
+ // selectors
+ badgeContainerSelector: '.articleLikesBadge',
+ buttonAppendToSelector: '.articleLikeButtons',
+ containerSelector: '.articleContent',
+ summarySelector: '.articleLikesSummery'
+ });
+ });
+ </script>
+{/if}
+
+{include file='footer'}
--- /dev/null
+{capture assign='headContent'}
+ {if $pageNo < $pages}
+ <link rel="next" href="{link controller='ArticleList'}pageNo={@$pageNo+1}{/link}" />
+ {/if}
+ {if $pageNo > 1}
+ <link rel="prev" href="{link controller='ArticleList'}{if $pageNo > 2}pageNo={@$pageNo-1}{/if}{/link}" />
+ {/if}
+
+ {if $__wcf->getUser()->userID}
+ <link rel="alternate" type="application/rss+xml" title="{lang}wcf.global.button.rss{/lang}" href="{link controller='ArticleFeed' appendSession=false}at={@$__wcf->getUser()->userID}-{@$__wcf->getUser()->accessToken}{/link}" />
+ {else}
+ <link rel="alternate" type="application/rss+xml" title="{lang}wcf.global.button.rss{/lang}" href="{link controller='ArticleFeed' appendSession=false}{/link}" />
+ {/if}
+{/capture}
+
+{capture assign='headerNavigation'}
+ <li><a rel="alternate" href="{if $__wcf->getUser()->userID}{link controller='ArticleFeed' appendSession=false}at={@$__wcf->getUser()->userID}-{@$__wcf->getUser()->accessToken}{/link}{else}{link controller='ArticleFeed' appendSession=false}{/link}{/if}" title="{lang}wcf.global.button.rss{/lang}" class="jsTooltip"><span class="icon icon16 fa-rss"></span> <span class="invisible">{lang}wcf.global.button.rss{/lang}</span></a></li>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+ <div class="paginationTop">
+ {content}
+ {pages print=true assign='pagesLinks' controller='ArticleList' link="pageNo=%d"}
+ {/content}
+ </div>
+{/hascontent}
+
+{if $objects|count}
+ <div class="section">
+ {include file='articleListItems'}
+ </div>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</footer>
+
+{include file='footer'}
--- /dev/null
+<ul class="articleList">
+ {foreach from=$objects item='article'}
+ <li>
+ <a href="{$article->getLink()}">
+ {if $article->getImage()}
+ <div class="box128">
+ <div class="articleListImage">{@$article->getImage()->getThumbnailTag('tiny')}</div>
+ {/if}
+
+ <div>
+ <div class="containerHeadline">
+ <h3 class="articleListTitle">{$article->getTitle()}</h3>
+ <ul class="inlineList articleListMetaData">
+ <li>
+ <span class="icon icon16 fa-clock-o"></span>
+ {@$article->time|time}
+ </li>
+
+ <li>
+ <span class="icon icon16 fa-comments"></span>
+ {lang}wcf.article.articleComments{/lang}
+ </li>
+
+ {if MODULE_LIKE && $__wcf->getSession()->getPermission('user.like.canViewLike')}
+ <li>
+ {if $article->likes || $article->dislikes}
+ <span class="icon icon16 fa-thumbs-o-{if $article->cumulativeLikes < 0}down{else}up{/if} jsTooltip" title="{lang likes=$article->likes dislikes=$article->dislikes}wcf.like.tooltip{/lang}"></span>
+ {if $article->cumulativeLikes > 0}+{elseif $article->cumulativeLikes == 0}±{/if}{#$article->cumulativeLikes}
+ {/if}
+ </li>
+ {/if}
+ </ul>
+ </div>
+
+ <div class="containerContent articleListTeaser">
+ {$article->getTeaser()}
+ </div>
+ </div>
+
+ {if $article->getImage()}
+ </div>
+ {/if}
+ </a>
+ </li>
+ {/foreach}
+</ul>
\ No newline at end of file
--- /dev/null
+{capture assign='pageTitle'}{$category->getTitle()}{/capture}
+
+{capture assign='contentTitle'}{$category->getTitle()}{/capture}
+
+{capture assign='headContent'}
+ {if $pageNo < $pages}
+ <link rel="next" href="{link controller='CategoryArticleList' object=$category}pageNo={@$pageNo+1}{/link}" />
+ {/if}
+ {if $pageNo > 1}
+ <link rel="prev" href="{link controller='CategoryArticleList' object=$category}{if $pageNo > 2}pageNo={@$pageNo-1}{/if}{/link}" />
+ {/if}
+
+ {if $__wcf->getUser()->userID}
+ <link rel="alternate" type="application/rss+xml" title="{lang}wcf.global.button.rss{/lang}" href="{link controller='ArticleFeed' id=$categoryID appendSession=false}at={@$__wcf->getUser()->userID}-{@$__wcf->getUser()->accessToken}{/link}" />
+ {else}
+ <link rel="alternate" type="application/rss+xml" title="{lang}wcf.global.button.rss{/lang}" href="{link controller='ArticleFeed' id=$categoryID appendSession=false}{/link}" />
+ {/if}
+{/capture}
+
+{capture assign='headerNavigation'}
+ <li><a rel="alternate" href="{if $__wcf->getUser()->userID}{link controller='ArticleFeed' id=$categoryID appendSession=false}at={@$__wcf->getUser()->userID}-{@$__wcf->getUser()->accessToken}{/link}{else}{link controller='ArticleFeed' id=$categoryID appendSession=false}{/link}{/if}" title="{lang}wcf.global.button.rss{/lang}" class="jsTooltip"><span class="icon icon16 fa-rss"></span> <span class="invisible">{lang}wcf.global.button.rss{/lang}</span></a></li>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+ <div class="paginationTop">
+ {content}
+ {pages print=true assign='pagesLinks' controller='CategoryArticleList' object=$category link="pageNo=%d"}
+ {/content}
+ </div>
+{/hascontent}
+
+{if $objects|count}
+ <div class="section">
+ {include file='articleListItems'}
+ </div>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</footer>
+
+{include file='footer'}
{include file='headInclude'}
+ {if !$canonicalURL|empty}
+ <link rel="canonical" href="{$canonicalURL}">
+ {/if}
+
{if !$headContent|empty}
{@$headContent}
{/if}
<link rel="prev" href="{link controller='MembersList'}{if $pageNo > 2}pageNo={@$pageNo-1}&{/if}{@$canonicalURLParameters}{/link}" />
{/if}
<link rel="canonical" href="{link controller='MembersList'}{if $pageNo > 1}pageNo={@$pageNo}&{/if}{@$canonicalURLParameters}{/link}" />
-{/capture}
+{/capture}
{capture assign='sidebarRight'}
{assign var=encodedLetter value=$letter|rawurlencode}
<category name="user.pageComment">
<parent>user</parent>
</category>
+ <category name="user.article">
+ <parent>user</parent>
+ </category>
<category name="mod" />
<category name="mod.general">
<category name="mod.pageComment">
<parent>mod.general</parent>
</category>
+ <category name="mod.article">
+ <parent>mod.general</parent>
+ </category>
<category name="admin" />
<usersonly>1</usersonly>
</option>
+ <option name="admin.content.article.canManageArticle">
+ <categoryname>admin.content</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="admin.content.article.canManageCategory">
+ <categoryname>admin.content</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+
<option name="admin.content.canBulkRevertContentChanges">
<categoryname>admin.content</categoryname>
<optiontype>boolean</optiontype>
<usersonly>1</usersonly>
</option>
+ <option name="user.article.canReadArticle">
+ <categoryname>user.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ </option>
+ <option name="user.article.canAddComment">
+ <categoryname>user.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ </option>
+ <option name="user.article.canEditComment">
+ <categoryname>user.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="user.article.canDeleteComment">
+ <categoryname>user.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+
<option name="mod.general.canUseModeration">
<categoryname>mod.general</categoryname>
<optiontype>boolean</optiontype>
<usersonly>1</usersonly>
</option>
+ <option name="mod.article.canEditComment">
+ <categoryname>mod.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <moddefaultvalue>1</moddefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="mod.article.canDeleteComment">
+ <categoryname>mod.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <moddefaultvalue>1</moddefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="mod.article.canModerateComment">
+ <categoryname>mod.article</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <moddefaultvalue>1</moddefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+
<!-- user.profileComment -->
<option name="user.profileComment.canAddComment">
<categoryname>user.profileComment</categoryname>
define('MEDIA_LARGE_THUMBNAIL_WIDTH', 1200);
define('MEDIA_LARGE_THUMBNAIL_HEIGHT', 900);
define('MEDIA_LARGE_THUMBNAIL_RETAIN_DIMENSIONS', 1);
+define('MODULE_ARTICLE', 1);
+define('ARTICLE_SHOW_ABOUT_AUTHOR', 1);
+define('ARTICLE_ENABLE_COMMENTS_DEFAULT_VALUE', 1);
+define('ARTICLE_ENABLE_LIKE', 1);
+define('ARTICLES_PER_PAGE', 1);
+define('ARTICLE_RELATED_ARTICLES', 1);
+define('ARTICLE_RELATED_ARTICLES_MATCH_THRESHOLD', 1);
--- /dev/null
+{include file='header' pageTitle='wcf.acp.article.'|concat:$action}
+
+<script data-relocate="true">
+ $(function() {
+ $('input[type="radio"][name="publicationStatus"]').change(function(event) {
+ var $selected = $('input[type="radio"][name="publicationStatus"]:checked');
+ if ($selected.length > 0) {
+ if ($selected.val() == 2) {
+ $('#publicationDateDl').show();
+ }
+ else {
+ $('#publicationDateDl').hide();
+ }
+ }
+ }).trigger('change');
+ });
+</script>
+
+<script data-relocate="true">
+ require(['WoltLab/WCF/Ui/User/Search/Input'], function(UiUserSearchInput) {
+ new UiUserSearchInput(elBySel('input[name="username"]'));
+ });
+</script>
+
+{if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}
+ <script data-relocate="true">
+ {include file='mediaJavaScript'}
+
+ require(['WoltLab/WCF/Media/Manager/Select'], function(MediaManagerSelect) {
+ new MediaManagerSelect({
+ dialogTitle: '{lang}wcf.acp.article.image.dialog.title{/lang}',
+ fileTypeFilters: {
+ isImage: 1
+ }
+ });
+ });
+ </script>
+{/if}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{if $action == 'add'}{lang}wcf.acp.article.add{/lang}{else}{lang}wcf.acp.article.edit{/lang}{/if}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ {if $action == 'edit'}
+ <li><a href="{$article->getLink()}" class="button"><span class="icon icon16 fa-search"></span> <span>{lang}wcf.acp.article.button.viewArticle{/lang}</span></a></li>
+ {/if}
+ <li><a href="{link controller='ArticleList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.article.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{include file='formError'}
+
+{if $success|isset}
+ <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<form method="post" action="{if $action == 'add'}{link controller='ArticleAdd'}{/link}{else}{link controller='ArticleEdit' id=$articleID}{/link}{/if}">
+ <div class="section">
+ <dl{if $errorField == 'categoryID'} class="formError"{/if}>
+ <dt><label for="categoryID">{lang}wcf.acp.article.category{/lang}</label></dt>
+ <dd>
+ <select name="categoryID" id="categoryID">
+ <option value="0">{lang}wcf.global.noSelection{/lang}</option>
+
+ {foreach from=$categoryNodeList item=category}
+ <option value="{@$category->categoryID}"{if $category->categoryID == $categoryID} selected="selected"{/if}>{if $category->getDepth() > 1}{@" "|str_repeat:($category->getDepth() - 1)}{/if}{$category->getTitle()}</option>
+ {/foreach}
+ </select>
+ {if $errorField == 'categoryID'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.category.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'username'} class="formError"{/if}>
+ <dt><label for="username">{lang}wcf.acp.article.author{/lang}</label></dt>
+ <dd>
+ <input type="text" id="username" name="username" value="{$username}" class="medium" />
+ {if $errorField == 'username'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.user.username.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'time'} class="formError"{/if}>
+ <dt><label for="time">{lang}wcf.article.time{/lang}</label></dt>
+ <dd>
+ <input type="datetime" id="time" name="time" value="{$time}" class="medium" />
+ {if $errorField == 'time'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.time.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl>
+ <dt><label for="categoryID">{lang}wcf.acp.article.publicationStatus{/lang}</label></dt>
+ <dd>
+ <label><input type="radio" name="publicationStatus" value="0" {if $publicationStatus == 0}checked="checked" {/if}/> {lang}wcf.acp.article.publicationStatus.unpublished{/lang}</label>
+ <label><input type="radio" name="publicationStatus" value="1" {if $publicationStatus == 1}checked="checked" {/if}/> {lang}wcf.acp.article.publicationStatus.published{/lang}</label>
+ <label><input type="radio" name="publicationStatus" value="2" {if $publicationStatus == 2}checked="checked" {/if}/> {lang}wcf.acp.article.publicationStatus.delayed{/lang}</label>
+ </dd>
+ </dl>
+
+ <dl id="publicationDateDl"{if $errorField == 'publicationDate'} class="formError"{/if}{if $publicationStatus != 2} style="display: none"{/if}>
+ <dt><label for="publicationDate">{lang}wcf.acp.article.publicationDate{/lang}</label></dt>
+ <dd>
+ <input type="datetime" id="publicationDate" name="publicationDate" value="{$publicationDate}" class="medium" />
+ {if $errorField == 'publicationDate'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.publicationDate.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl>
+ <dt></dt>
+ <dd>
+ <label><input name="enableComments" type="checkbox" value="1"{if $enableComments} checked="checked"{/if} /> {lang}wcf.acp.article.enableComments{/lang}</label>
+ </dd>
+ </dl>
+ </div>
+
+ {if !$isMultilingual}
+ <div class="section">
+ {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}
+ <dl{if $errorField == 'image'} class="formError"{/if}>
+ <dt><label for="image">{lang}wcf.acp.article.image{/lang}</label></dt>
+ <dd>
+ <div id="imageDisplay">
+ {if $images[0]|isset}
+ {@$images[0]->getThumbnailTag('small')}
+ {/if}
+ </div>
+ <p class="button jsMediaSelectButton" data-store="imageID0" data-display="imageDisplay">{lang}wcf.acp.article.image.button.chooseImage{/lang}</p>
+ <input type="hidden" name="imageID[0]" id="imageID0"{if $imageID[0]|isset} value="{@$imageID[0]}"{/if} />
+ {if $errorField == 'image'}
+ <small class="innerError">{lang}wcf.acp.article.image.error.{@$errorType}{/lang}</small>
+ {/if}
+ </dd>
+ </dl>
+ {elseif $action == 'edit' && $images[0]|isset}
+ <dl>
+ <dt>{lang}wcf.acp.article.image{/lang}</dt>
+ <dd>
+ <div id="imageDisplay">{@$images[0]->getThumbnailTag('small')}</div>
+ </dd>
+ </dl>
+ {/if}
+
+ <dl{if $errorField == 'title'} class="formError"{/if}>
+ <dt><label for="title0">{lang}wcf.acp.article.title{/lang}</label></dt>
+ <dd>
+ <input type="text" id="title0" name="title[0]" value="{if !$title[0]|empty}{$title[0]}{/if}" class="long" />
+ {if $errorField == 'title'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.title.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ {if MODULE_TAGGING}
+ <dl class="jsOnly">
+ <dt><label for="tagSearchInput">{lang}wcf.tagging.tags{/lang}</label></dt>
+ <dd>
+ <input id="tagSearchInput" type="text" value="" class="long" />
+ <small>{lang}wcf.tagging.tags.description{/lang}</small>
+ </dd>
+ </dl>
+
+ <script data-relocate="true">
+ require(['WoltLab/WCF/Ui/ItemList'], function(UiItemList) {
+ UiItemList.init(
+ 'tagSearchInput',
+ [{if !$tags[0]|empty}{implode from=$tags[0] item=tag}'{$tag|encodeJS}'{/implode}{/if}],
+ {
+ ajax: {
+ className: 'wcf\\data\\tag\\TagAction'
+ },
+ maxLength: {@TAGGING_MAX_TAG_LENGTH},
+ submitFieldName: 'tags[0][]'
+ }
+ );
+ });
+ </script>
+ {/if}
+
+ <dl{if $errorField == 'teaser'} class="formError"{/if}>
+ <dt><label for="teaser0">{lang}wcf.acp.article.teaser{/lang}</label></dt>
+ <dd>
+ <textarea name="teaser[0]" id="teaser0" rows="5">{if !$teaser[0]|empty}{$teaser[0]}{/if}</textarea>
+ {if $errorField == 'teaser'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.teaser.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'content'} class="formError"{/if}>
+ <dt><label for="content0">{lang}wcf.acp.article.content{/lang}</label></dt>
+ <dd>
+ <textarea name="content[0]" id="content0" rows="10">{if !$content[0]|empty}{$content[0]}{/if}</textarea>
+ {include file='wysiwyg' wysiwygSelector='content0'}
+ {if $errorField == 'content'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.content.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+ </div>
+ {else}
+ <div class="section tabMenuContainer">
+ <nav class="tabMenu">
+ <ul>
+ {foreach from=$availableLanguages item=availableLanguage}
+ {assign var='containerID' value='language'|concat:$availableLanguage->languageID}
+ <li><a href="{@$__wcf->getAnchor($containerID)}">{$availableLanguage->languageName}</a></li>
+ {/foreach}
+ </ul>
+ </nav>
+
+ {foreach from=$availableLanguages item=availableLanguage}
+ <div id="language{@$availableLanguage->languageID}" class="tabMenuContent">
+ <div class="section">
+ {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}
+ <dl{if $errorField == 'image'|concat:$availableLanguage->languageID} class="formError"{/if}>
+ <dt><label for="image{@$availableLanguage->languageID}">{lang}wcf.acp.article.image{/lang}</label></dt>
+ <dd>
+ <div id="imageDisplay{@$availableLanguage->languageID}">
+ {if $images[$availableLanguage->languageID]|isset}
+ {@$images[$availableLanguage->languageID]->getThumbnailTag('small')}
+ {/if}
+ </div>
+ <p class="button jsMediaSelectButton" data-store="imageID{@$availableLanguage->languageID}" data-display="imageDisplay{@$availableLanguage->languageID}">{lang}wcf.acp.article.image.button.chooseImage{/lang}</p>
+ <input type="hidden" name="imageID[{@$availableLanguage->languageID}]" id="imageID{@$availableLanguage->languageID}"{if $imageID[$availableLanguage->languageID]|isset} value="{@$imageID[$availableLanguage->languageID]}"{/if} />
+ {if $errorField == 'image'|concat:$availableLanguage->languageID}
+ <small class="innerError">{lang}wcf.acp.article.image.error.{@$errorType}{/lang}</small>
+ {/if}
+ </dd>
+ </dl>
+ {elseif $action == 'edit' && $images[$availableLanguage->languageID]|isset}
+ <dl>
+ <dt>{lang}wcf.acp.article.image{/lang}</dt>
+ <dd>
+ <div id="imageDisplay">{@$images[$availableLanguage->languageID]->getThumbnailTag('small')}</div>
+ </dd>
+ </dl>
+ {/if}
+
+ <dl{if $errorField == 'title'|concat:$availableLanguage->languageID} class="formError"{/if}>
+ <dt><label for="title{@$availableLanguage->languageID}">{lang}wcf.acp.article.title{/lang}</label></dt>
+ <dd>
+ <input type="text" id="title{@$availableLanguage->languageID}" name="title[{@$availableLanguage->languageID}]" value="{if !$title[$availableLanguage->languageID]|empty}{$title[$availableLanguage->languageID]}{/if}" class="long" />
+ {if $errorField == 'title'|concat:$availableLanguage->languageID}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.title.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ {if MODULE_TAGGING}
+ <dl class="jsOnly">
+ <dt><label for="tagSearchInput{@$availableLanguage->languageID}">{lang}wcf.tagging.tags{/lang}</label></dt>
+ <dd>
+ <input id="tagSearchInput{@$availableLanguage->languageID}" type="text" value="" class="long" />
+ <small>{lang}wcf.tagging.tags.description{/lang}</small>
+ </dd>
+ </dl>
+
+ <script data-relocate="true">
+ require(['WoltLab/WCF/Ui/ItemList'], function(UiItemList) {
+ UiItemList.init(
+ 'tagSearchInput{@$availableLanguage->languageID}',
+ [{if !$tags[$availableLanguage->languageID]|empty}{implode from=$tags[$availableLanguage->languageID] item=tag}'{$tag|encodeJS}'{/implode}{/if}],
+ {
+ ajax: {
+ className: 'wcf\\data\\tag\\TagAction'
+ },
+ maxLength: {@TAGGING_MAX_TAG_LENGTH},
+ submitFieldName: 'tags[{@$availableLanguage->languageID}][]'
+ }
+ );
+ });
+ </script>
+ {/if}
+
+ <dl{if $errorField == 'teaser'|concat:$availableLanguage->languageID} class="formError"{/if}>
+ <dt><label for="teaser{@$availableLanguage->languageID}">{lang}wcf.acp.article.teaser{/lang}</label></dt>
+ <dd>
+ <textarea name="teaser[{@$availableLanguage->languageID}]" id="teaser{@$availableLanguage->languageID}" rows="5">{if !$teaser[$availableLanguage->languageID]|empty}{$teaser[$availableLanguage->languageID]}{/if}</textarea>
+ {if $errorField == 'teaser'|concat:$availableLanguage->languageID}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.teaser.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'content'|concat:$availableLanguage->languageID} class="formError"{/if}>
+ <dt><label for="content{@$availableLanguage->languageID}">{lang}wcf.acp.article.content{/lang}</label></dt>
+ <dd>
+ <textarea name="content[{@$availableLanguage->languageID}]" id="content{@$availableLanguage->languageID}" rows="10">{if !$content[$availableLanguage->languageID]|empty}{$content[$availableLanguage->languageID]}{/if}</textarea>
+ {include file='wysiwyg' wysiwygSelector='content'|concat:$availableLanguage->languageID}
+ {if $errorField == 'content'|concat:$availableLanguage->languageID}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.article.content.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+ </div>
+ </div>
+ {/foreach}
+ </div>
+ {/if}
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ <input type="hidden" name="isMultilingual" value="{@$isMultilingual}">
+ {@SECURITY_TOKEN_INPUT_TAG}
+ </div>
+</form>
+
+{include file='footer'}
--- /dev/null
+<div id="articleAddDialog" style="display: none">
+ <div class="section">
+ <dl>
+ <dt>{lang}wcf.acp.article.i18n{/lang}</dt>
+ <dd>
+ <label><input type="radio" name="isMultilingual" value="0" checked> {lang}wcf.acp.article.i18n.none{/lang}</label>
+ <small>{lang}wcf.acp.article.i18n.none.description{/lang}</small>
+ <label><input type="radio" name="isMultilingual" value="1"> {lang}wcf.acp.article.i18n.i18n{/lang}</label>
+ <small>{lang}wcf.acp.article.i18n.i18n.description{/lang}</small>
+ </dd>
+ </dl>
+
+ <div class="formSubmit">
+ <button class="buttonPrimary">{lang}wcf.global.button.next{/lang}</button>
+ </div>
+ </div>
+</div>
+<script data-relocate="true">
+ require(['WoltLab/WCF/Acp/Ui/Article/Add'], function(AcpUiArticleAdd) {
+ AcpUiArticleAdd.init('{link controller='ArticleAdd' encode=false}{literal}isMultilingual={$isMultilingual}{/literal}{/link}');
+
+ {if $showArticleAddDialog}
+ window.setTimeout(function() {
+ AcpUiArticleAdd.openDialog();
+ }, 10);
+ {/if}
+ });
+</script>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.article.list'}
+
+<script data-relocate="true">
+ require(['WoltLab/WCF/Ui/User/Search/Input'], function(UiUserSearchInput) {
+ new UiUserSearchInput(elBySel('input[name="username"]'));
+ });
+</script>
+
+<script data-relocate="true">
+ //<![CDATA[
+ $(function() {
+ new WCF.Action.Delete('wcf\\data\\article\\ArticleAction', '.jsArticleRow');
+ new WCF.Action.Toggle('wcf\\data\\article\\ArticleAction', '.jsArticleRow');
+ });
+ //]]>
+</script>
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.article.list{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="#" class="button jsButtonArticleAdd"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.article.add{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+<form method="post" action="{link controller='ArticleList'}{/link}">
+ <section class="section">
+ <h2 class="sectionTitle">{lang}wcf.global.filter{/lang}</h2>
+
+ <div class="row rowColGap formGrid">
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <select name="categoryID" id="categoryID">
+ <option value="0">{lang}wcf.acp.article.category{/lang}</option>
+
+ {foreach from=$categoryNodeList item=category}
+ <option value="{@$category->categoryID}"{if $category->categoryID == $categoryID} selected="selected"{/if}>{if $category->getDepth() > 1}{@" "|str_repeat:($category->getDepth() - 1)}{/if}{$category->getTitle()}</option>
+ {/foreach}
+ </select>
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <input type="text" id="pageTitle" name="title" value="{$title}" placeholder="{lang}wcf.acp.article.title{/lang}" class="long" />
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <input type="text" id="pageContent" name="content" value="{$content}" placeholder="{lang}wcf.acp.article.content{/lang}" class="long" />
+ </dd>
+ </dl>
+
+ <dl class="col-xs-12 col-md-4">
+ <dt></dt>
+ <dd>
+ <input type="text" id="username" name="username" value="{$username}" placeholder="{lang}wcf.acp.article.author{/lang}" class="long" />
+ </dd>
+ </dl>
+
+ {event name='filterFields'}
+ </div>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+ {@SECURITY_TOKEN_INPUT_TAG}
+ </div>
+ </section>
+</form>
+
+{hascontent}
+ <div class="paginationTop">
+ {content}
+ {assign var='linkParameters' value=''}
+ {if $categoryID}{capture append=linkParameters}&categoryID={@$categoryID}{/capture}{/if}
+ {if $title}{capture append=linkParameters}&title={@$title|rawurlencode}{/capture}{/if}
+ {if $content}{capture append=linkParameters}&content={@$content|rawurlencode}{/capture}{/if}
+ {if $username}{capture append=linkParameters}&username={@$username|rawurlencode}{/capture}{/if}
+
+ {pages print=true assign=pagesLinks controller="ArticleList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder$linkParameters"}
+ {/content}
+ </div>
+{/hascontent}
+
+{if $objects|count}
+ <div class="section tabularBox">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnArticleID{if $sortField == 'articleID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=articleID&sortOrder={if $sortField == 'articleID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnText columnArticleTitle{if $sortField == 'title'} active {@$sortOrder}{/if}"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=title&sortOrder={if $sortField == 'title' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.article.title{/lang}</a></th>
+ <th class="columnDigits columnComments{if $sortField == 'comments'} active {@$sortOrder}{/if}"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=comments&sortOrder={if $sortField == 'comments' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.article.comments{/lang}</a></th>
+ <th class="columnDigits columnViews{if $sortField == 'views'} active {@$sortOrder}{/if}"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=views&sortOrder={if $sortField == 'views' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.article.views{/lang}</a></th>
+ <th class="columnDate columnTime{if $sortField == 'time'} active {@$sortOrder}{/if}"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=time&sortOrder={if $sortField == 'time' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.article.time{/lang}</a></th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$objects item=article}
+ <tr class="jsArticleRow">
+ <td class="columnIcon">
+ <a href="{link controller='ArticleEdit' id=$article->articleID}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon24 fa-pencil"></span></a>
+ {if $article->canDelete()}
+ <span class="icon icon24 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$article->articleID}" data-confirm-message="{lang}wcf.acp.article.delete.confirmMessage{/lang}"></span>
+ {else}
+ <span class="icon icon24 fa-times disabled" title="{lang}wcf.global.button.delete{/lang}"></span>
+ {/if}
+
+ <a href="{$article->getLink()}" title="{lang}wcf.acp.article.button.viewArticle{/lang}" class="jsTooltip"><span class="icon icon24 fa-search"></span></a>
+
+ {event name='rowButtons'}
+ </td>
+ <td class="columnID columnArticleID">{@$article->articleID}</td>
+ <td class="columnText columnArticleTitle">
+ <div class="box48">
+ <span>
+ {if $article->getImage()}
+ {@$article->getImage()->getElementTag(48)}
+ {else}
+ <img src="{@$__wcf->getPath()}images/placeholderTiny.png" style="width: 48px; height: 48px" alt="" />
+ {/if}
+ </span>
+
+ <div class="containerHeadline">
+ <h3><a href="{link controller='ArticleEdit' id=$article->articleID}{/link}" title="{lang}wcf.acp.article.edit{/lang}" class="jsTooltip">{$article->title}</a></h3>
+ <ul class="inlineList dotSeparated">
+ {if $article->categoryID}
+ <li>{$article->getCategory()->getTitle()}</li>
+ {/if}
+
+ {if $article->username}
+ <li>
+ {if $article->userID}
+ <a href="{link controller='UserEdit' id=$article->userID}{/link}">{$article->username}</a>
+ {else}
+ {$article->username}
+ {/if}
+ </li>
+ {/if}
+ </ul>
+ </div>
+ </div>
+ </td>
+ <td class="columnDigits columnComments">{#$article->comments}</td>
+ <td class="columnDigits columnViews">{#$article->views}</td>
+ <td class="columnDate columnTime">{@$article->time|time}</td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ </div>
+
+ <footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ <nav class="contentFooterNavigation">
+ <ul>
+ <li><a href="#" class="button jsButtonArticleAdd"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.article.add{/lang}</span></a></li>
+
+ {event name='contentFooterNavigation'}
+ </ul>
+ </nav>
+ </footer>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='articleAddDialog'}
+
+{include file='footer'}
{/if}
<dl{if $errorField == 'title'} class="formError"{/if}>
- <dt><label for="title">{lang}wcf.acp.box.title{/lang}</label></dt>
+ <dt><label for="title0">{lang}wcf.acp.box.title{/lang}</label></dt>
<dd>
<input type="text" id="title0" name="title[0]" value="{if !$title[0]|empty}{$title[0]}{/if}" class="long" />
{if $errorField == 'title'}
</ul>
</nav>
</footer>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
{/if}
{include file='pageAddDialog'}
--- /dev/null
+/**
+ * Provides the dialog overlay to add a new article.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/Acp/Ui/Article/Add
+ */
+define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
+ "use strict";
+
+ var _link;
+
+ /**
+ * @exports WoltLab/WCF/Acp/Ui/Article/Add
+ */
+ return {
+ /**
+ * Initializes the article add handler.
+ *
+ * @param {string} link redirect URL
+ */
+ init: function(link) {
+ _link = link;
+
+ var buttons = elBySelAll('.jsButtonArticleAdd');
+ for (var i = 0, length = buttons.length; i < length; i++) {
+ buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
+ }
+ },
+
+ /**
+ * Opens the 'Add Article' dialog.
+ *
+ * @param {Event=} event event object
+ */
+ openDialog: function(event) {
+ if (event instanceof Event) {
+ event.preventDefault();
+ }
+
+ UiDialog.open(this);
+ },
+
+ _dialogSetup: function() {
+ return {
+ id: 'articleAddDialog',
+ options: {
+ onSetup: function(content) {
+ elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
+ event.preventDefault();
+
+ var isMultilingual = elBySel('input[name="isMultilingual"]:checked', content).value;
+
+ window.location = _link.replace(/{\$isMultilingual}/, isMultilingual);
+ });
+ },
+ title: Language.get('wcf.acp.article.add')
+ }
+ };
+ }
+ }
+});
_buildWidget: function(element, elementData) {
// build summary
if (this._options.canViewSummary) {
- var summary, summaryContainer = elBySel(this._options.summarySelector, element), summaryContent, summaryIcon;
+ var summary, summaryContent, summaryIcon;
+ var summaryContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element)
if (summaryContainer !== null) {
summary = elCreate('div');
summary.className = 'likesSummary';
}
// cumulative likes
- var badge, badgeContainer = elBySel(this._options.badgeContainerSelector, element), listItem;
+ var badge, listItem;
+ var badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
if (badgeContainer !== null) {
badge = elCreate('a');
badge.href = '#';
this._updateBadge(element);
}
- if (WCF.User.userID != elData(element, 'user-id') || this._options.canLikeOwnContent) {
- var appendTo = (this._options.buttonAppendToSelector) ? elBySel(this._options.buttonAppendToSelector, element) : null;
- var insertPosition = (this._options.buttonBeforeSelector) ? elBySel(this._options.buttonBeforeSelector, element) : null;
+ if (this._options.canLike && (WCF.User.userID != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
+ var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
+ var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
if (insertPosition === null && appendTo === null) {
throw new Error("Unable to find insert location for like/dislike buttons.");
}
button.className = 'jsTooltip' + (this._options.renderAsButton ? ' button' : '');
button.href = '#';
button.title = title;
- button.innerHTML = '<span class="icon icon16 fa-thumbs-o-' + (isLike ? 'up' : 'down') + '" /> <span class="invisible">' + title + '</span>';
+ button.innerHTML = '<span class="icon icon16 fa-thumbs-o-' + (isLike ? 'up' : 'down') + '"></span> <span class="invisible">' + title + '</span>';
button.addEventListener(WCF_CLICK_EVENT, this._like.bind(this, element));
elData(button, 'type', (isLike ? 'like' : 'dislike'));
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\article\ArticleAction;
+use wcf\data\article\category\ArticleCategory;
+use wcf\data\category\CategoryNodeTree;
+use wcf\data\language\Language;
+use wcf\data\media\Media;
+use wcf\data\media\ViewableMediaList;
+use wcf\data\user\User;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\DateUtil;
+use wcf\util\HeaderUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the article add form.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.form
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleAddForm extends AbstractForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.add';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.article.canManageArticle'];
+
+ /**
+ * true if created article is multi-lingual
+ * @var boolean
+ */
+ public $isMultilingual = 0;
+
+ /**
+ * category id
+ * @var integer
+ */
+ public $categoryID = 0;
+
+ /**
+ * author's username
+ * @var string
+ */
+ public $username = '';
+
+ /**
+ * author
+ * @var User
+ */
+ public $author;
+
+ /**
+ * article date (ISO 8601)
+ * @var string
+ */
+ public $time = '';
+
+ /**
+ * article date object
+ * @var \DateTime
+ */
+ public $timeObj;
+
+ /**
+ * publication status
+ * @var integer
+ */
+ public $publicationStatus = 1;
+
+ /**
+ * publication date (ISO 8601)
+ * @var string
+ */
+ public $publicationDate = '';
+
+ /**
+ * publication date object
+ * @var \DateTime
+ */
+ public $publicationDateObj;
+
+ /**
+ * enables the comment function
+ * @var boolean
+ */
+ public $enableComments = ARTICLE_ENABLE_COMMENTS_DEFAULT_VALUE;
+
+ /**
+ * article titles
+ * @var string[]
+ */
+ public $title = [];
+
+ /**
+ * tags
+ * @var string[][]
+ */
+ public $tags = [];
+
+ /**
+ * article teasers
+ * @var string[]
+ */
+ public $teaser = [];
+
+ /**
+ * article contents
+ * @var string[]
+ */
+ public $content = [];
+
+ /**
+ * image ids
+ * @var integer[]
+ */
+ public $imageID = [];
+
+ /**
+ * images
+ * @var Media[]
+ */
+ public $images = [];
+
+ /**
+ * list of available languages
+ * @var Language[]
+ */
+ public $availableLanguages = [];
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ $this->readMultilingualSetting();
+
+ // get available languages
+ $this->availableLanguages = LanguageFactory::getInstance()->getLanguages();
+ }
+
+ /**
+ * Reads basic article parameters controlling i18n.
+ */
+ protected function readMultilingualSetting() {
+ if (!empty($_REQUEST['isMultilingual'])) $this->isMultilingual = 1;
+
+ // work-around to force adding article via dialog overlay
+ if (empty($_POST) && !isset($_REQUEST['isMultilingual'])) {
+ HeaderUtil::redirect(LinkHandler::getInstance()->getLink('ArticleList', ['showArticleAddDialog' => 1]));
+ exit;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ $this->enableComments = 0;
+ if (isset($_POST['categoryID'])) $this->categoryID = intval($_POST['categoryID']);
+ if (isset($_POST['username'])) $this->username = StringUtil::trim($_POST['username']);
+ if (isset($_POST['time'])) {
+ $this->time = $_POST['time'];
+ $this->timeObj = \DateTime::createFromFormat('Y-m-d\TH:i:sP', $this->time);
+ }
+ if (!empty($_POST['enableComments'])) $this->enableComments = 1;
+ if (isset($_POST['publicationStatus'])) $this->publicationStatus = intval($_POST['publicationStatus']);
+ if ($this->publicationStatus == 2 && isset($_POST['publicationDate'])) {
+ $this->publicationDate = $_POST['publicationDate'];
+ $this->publicationDateObj = \DateTime::createFromFormat('Y-m-d\TH:i:sP', $this->publicationDate);
+ }
+ if (isset($_POST['title']) && is_array($_POST['title'])) $this->title = ArrayUtil::trim($_POST['title']);
+ if (MODULE_TAGGING && isset($_POST['tags']) && is_array($_POST['tags'])) $this->tags = ArrayUtil::trim($_POST['tags']);
+ if (isset($_POST['teaser']) && is_array($_POST['teaser'])) $this->teaser = ArrayUtil::trim($_POST['teaser']);
+ if (isset($_POST['content']) && is_array($_POST['content'])) $this->content = ArrayUtil::trim($_POST['content']);
+
+ if (WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
+ if (isset($_POST['imageID']) && is_array($_POST['imageID'])) $this->imageID = ArrayUtil::toIntegerArray($_POST['imageID']);
+
+ $this->readImages();
+ }
+ }
+
+ /**
+ * Reads the box images.
+ */
+ protected function readImages() {
+ if (!empty($this->imageID)) {
+ $mediaList = new ViewableMediaList();
+ $mediaList->setObjectIDs($this->imageID);
+ $mediaList->readObjects();
+
+ foreach ($this->imageID as $languageID => $imageID) {
+ $image = $mediaList->search($imageID);
+ if ($image !== null && $image->isImage) {
+ $this->images[$languageID] = $image;
+ }
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ // category
+ if (empty($this->categoryID)) {
+ throw new UserInputException('categoryID');
+ }
+ $category = ArticleCategory::getCategory($this->categoryID);
+ if ($category === null) {
+ throw new UserInputException('categoryID', 'invalid');
+ }
+
+ // author
+ if (empty($this->username)) {
+ throw new UserInputException('username');
+ }
+ $this->author = User::getUserByUsername($this->username);
+ if (!$this->author->userID) {
+ throw new UserInputException('username', 'notFound');
+ }
+
+ // article date
+ if (empty($this->time)) {
+ throw new UserInputException('time');
+ }
+ if (!$this->timeObj) {
+ throw new UserInputException('time', 'invalid');
+ }
+
+ // publication status
+ if ($this->publicationStatus < 0 || $this->publicationStatus > 2) {
+ throw new UserInputException('publicationStatus');
+ }
+ if ($this->publicationStatus == 2) {
+ if (empty($this->publicationDate)) {
+ throw new UserInputException('publicationDate');
+ }
+
+ if (!$this->publicationDateObj || $this->publicationDateObj->getTimestamp() < TIME_NOW) {
+ throw new UserInputException('publicationDate', 'invalid');
+ }
+ }
+
+ if ($this->isMultilingual) {
+ foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+ // title
+ if (empty($this->title[$language->languageID])) {
+ throw new UserInputException('title'.$language->languageID);
+ }
+ // teaser
+ if (empty($this->teaser[$language->languageID])) {
+ throw new UserInputException('teaser'.$language->languageID);
+ }
+ // content
+ if (empty($this->content[$language->languageID])) {
+ throw new UserInputException('content'.$language->languageID);
+ }
+ }
+ }
+ else {
+ // title
+ if (empty($this->title[0])) {
+ throw new UserInputException('title');
+ }
+ // teaser
+ if (empty($this->teaser[0])) {
+ throw new UserInputException('teaser');
+ }
+ // content
+ if (empty($this->content[0])) {
+ throw new UserInputException('content');
+ }
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ $content = [];
+ if ($this->isMultilingual) {
+ foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+ $content[$language->languageID] = [
+ 'title' => (!empty($this->title[$language->languageID]) ? $this->title[$language->languageID] : ''),
+ 'tags' => (!empty($this->tags[$language->languageID]) ? $this->tags[$language->languageID] : []),
+ 'teaser' => (!empty($this->teaser[$language->languageID]) ? $this->teaser[$language->languageID] : ''),
+ 'content' => (!empty($this->content[$language->languageID]) ? $this->content[$language->languageID] : ''),
+ 'imageID' => (!empty($this->imageID[$language->languageID]) ? $this->imageID[$language->languageID] : null)
+ ];
+ }
+ }
+ else {
+ $content[0] = [
+ 'title' => (!empty($this->title[0]) ? $this->title[0] : ''),
+ 'tags' => (!empty($this->tags[0]) ? $this->tags[0] : []),
+ 'teaser' => (!empty($this->teaser[0]) ? $this->teaser[0] : ''),
+ 'content' => (!empty($this->content[0]) ? $this->content[0] : ''),
+ 'imageID' => (!empty($this->imageID[0]) ? $this->imageID[0] : null)
+ ];
+ }
+
+ $data = [
+ 'time' => $this->timeObj->getTimestamp(),
+ 'categoryID' => $this->categoryID,
+ 'publicationStatus' => $this->publicationStatus,
+ 'publicationDate' => ($this->publicationStatus == 2 ? $this->publicationDateObj->getTimestamp() : 0),
+ 'enableComments' => $this->enableComments,
+ 'userID' => $this->author->userID,
+ 'username' => $this->author->username,
+ 'isMultilingual' => $this->isMultilingual
+ ];
+
+ $this->objectAction = new ArticleAction([], 'create', ['data' => array_merge($this->additionalFields, $data), 'content' => $content]);
+ $this->objectAction->executeAction();
+
+ // call saved event
+ $this->saved();
+
+ // show success
+ WCF::getTPL()->assign('success', true);
+
+ // reset variables
+ $this->username = $this->publicationDate = '';
+ $this->categoryID = 0;
+ $this->publicationStatus = $this->enableComments = 1;
+ $this->title = $this->teaser = $this->content = $this->images = $this->imageID = $this->tags = [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->username = WCF::getUser()->username;
+ $dateTime = DateUtil::getDateTimeByTimestamp(TIME_NOW);
+ $dateTime->setTimezone(WCF::getUser()->getTimeZone());
+ $this->time = $dateTime->format('c');
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'add',
+ 'isMultilingual' => $this->isMultilingual,
+ 'categoryID' => $this->categoryID,
+ 'username' => $this->username,
+ 'time' => $this->time,
+ 'enableComments' => $this->enableComments,
+ 'publicationStatus' => $this->publicationStatus,
+ 'publicationDate' => $this->publicationDate,
+ 'imageID' => $this->imageID,
+ 'images' => $this->images,
+ 'tags' => $this->tags,
+ 'title' => $this->title,
+ 'teaser' => $this->teaser,
+ 'content' => $this->content,
+ 'availableLanguages' => $this->availableLanguages,
+ 'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator()
+ ]);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\acp\form;
+
+/**
+ * Shows the article category add form.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.form
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleCategoryAddForm extends AbstractCategoryAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.category.add';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectTypeName = 'com.woltlab.wcf.article.category';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+
+/**
+ * Shows the article category edit form.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.form
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleCategoryEditForm extends AbstractCategoryEditForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.category.list';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectTypeName = 'com.woltlab.wcf.article.category';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\article\Article;
+use wcf\data\article\ArticleAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\tagging\TagEngine;
+use wcf\system\WCF;
+use wcf\util\DateUtil;
+
+/**
+ * Shows the article edit form.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.form
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleEditForm extends ArticleAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.list';
+
+ /**
+ * article id
+ * @var integer
+ */
+ public $articleID = 0;
+
+ /**
+ * article object
+ * @var Article
+ */
+ public $article = null;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->articleID = intval($_REQUEST['id']);
+ $this->article = new Article($this->articleID);
+ if (!$this->article->articleID) {
+ throw new IllegalLinkException();
+ }
+ if ($this->article->isMultilingual) $this->isMultilingual = 1;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function readMultilingualSetting() {
+ // not required for editing
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ AbstractForm::save();
+
+ $content = [];
+ if ($this->isMultilingual) {
+ foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+ $content[$language->languageID] = [
+ 'title' => (!empty($this->title[$language->languageID]) ? $this->title[$language->languageID] : ''),
+ 'tags' => (!empty($this->tags[$language->languageID]) ? $this->tags[$language->languageID] : []),
+ 'teaser' => (!empty($this->teaser[$language->languageID]) ? $this->teaser[$language->languageID] : ''),
+ 'content' => (!empty($this->content[$language->languageID]) ? $this->content[$language->languageID] : ''),
+ 'imageID' => (!empty($this->imageID[$language->languageID]) ? $this->imageID[$language->languageID] : null)
+ ];
+ }
+ }
+ else {
+ $content[0] = [
+ 'title' => (!empty($this->title[0]) ? $this->title[0] : ''),
+ 'tags' => (!empty($this->tags[0]) ? $this->tags[0] : []),
+ 'teaser' => (!empty($this->teaser[0]) ? $this->teaser[0] : ''),
+ 'content' => (!empty($this->content[0]) ? $this->content[0] : ''),
+ 'imageID' => (!empty($this->imageID[0]) ? $this->imageID[0] : null)
+ ];
+ }
+
+ $data = [
+ 'categoryID' => $this->categoryID,
+ 'publicationStatus' => $this->publicationStatus,
+ 'publicationDate' => ($this->publicationStatus == 2 ? $this->publicationDateObj->getTimestamp() : 0),
+ 'enableComments' => $this->enableComments,
+ 'userID' => $this->author->userID,
+ 'username' => $this->author->username,
+ 'time' => $this->timeObj->getTimestamp()
+ ];
+
+ $this->objectAction = new ArticleAction([$this->article], 'update', ['data' => array_merge($this->additionalFields, $data), 'content' => $content]);
+ $this->objectAction->executeAction();
+
+ // call saved event
+ $this->saved();
+
+ // show success
+ WCF::getTPL()->assign('success', true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ if (!empty($_POST) && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
+ foreach ($this->article->getArticleContent() as $languageID => $content) {
+ $this->imageID[$languageID] = $content['imageID'];
+ }
+
+ $this->readImages();
+ }
+
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->categoryID = $this->article->categoryID;
+ $this->publicationStatus = $this->article->publicationStatus;
+ $this->enableComments = $this->article->enableComments;
+ $this->username = $this->article->username;
+ $dateTime = DateUtil::getDateTimeByTimestamp($this->article->time);
+ $dateTime->setTimezone(WCF::getUser()->getTimeZone());
+ $this->time = $dateTime->format('c');
+ if ($this->article->publicationDate) {
+ $dateTime = DateUtil::getDateTimeByTimestamp($this->article->publicationDate);
+ $dateTime->setTimezone(WCF::getUser()->getTimeZone());
+ $this->publicationDate = $dateTime->format('c');
+ }
+
+ foreach ($this->article->getArticleContent() as $languageID => $content) {
+ $this->title[$languageID] = $content->title;
+ $this->teaser[$languageID] = $content->teaser;
+ $this->content[$languageID] = $content->content;
+ $this->imageID[$languageID] = $content->imageID;
+
+ // get tags
+ if (MODULE_TAGGING) {
+ $this->tags[$languageID] = TagEngine::getInstance()->getObjectTags(
+ 'com.woltlab.wcf.article',
+ $content->articleContentID,
+ [($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID())]
+ );
+ }
+ }
+
+ $this->readImages();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'edit',
+ 'articleID' => $this->articleID,
+ 'article' => $this->article
+ ]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\page;
+
+/**
+ * Shows the list article categories.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.page
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleCategoryListPage extends AbstractCategoryListPage {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.category.list';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectTypeName = 'com.woltlab.wcf.article.category';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+}
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\data\article\ArticleList;
+use wcf\data\article\ViewableArticleList;
+use wcf\data\category\CategoryNodeTree;
+use wcf\data\user\User;
+use wcf\page\SortablePage;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows a list of cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage acp.page
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @property ArticleList $objectList
+ */
+class ArticleListPage extends SortablePage {
+ /**
+ * @inheritdoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.list';
+
+ /**
+ * @inheritdoc
+ */
+ public $objectListClassName = ViewableArticleList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+
+ /**
+ * @inheritdoc
+ */
+ public $neededPermissions = ['admin.content.article.canManageArticle'];
+
+ /**
+ * @inheritdoc
+ */
+ public $defaultSortField = 'time';
+
+ /**
+ * @inheritdoc
+ */
+ public $defaultSortOrder = 'DESC';
+
+ /**
+ * @inheritdoc
+ */
+ public $validSortFields = ['articleID', 'title', 'time', 'views', 'comments'];
+
+ /**
+ * category id
+ * @var integer
+ */
+ public $categoryID = 0;
+
+ /**
+ * name
+ * @var string
+ */
+ public $username = '';
+
+ /**
+ * title
+ * @var string
+ */
+ public $title = '';
+
+ /**
+ * content
+ * @var string
+ */
+ public $content = '';
+
+ /**
+ * display 'Add Article' dialog on load
+ * @var integer
+ */
+ public $showArticleAddDialog = 0;
+
+ /**
+ * @inheritdoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_POST['categoryID'])) $this->categoryID = intval($_POST['categoryID']);
+ if (!empty($_REQUEST['username'])) $this->username = StringUtil::trim($_REQUEST['username']);
+ if (!empty($_REQUEST['title'])) $this->title = StringUtil::trim($_REQUEST['title']);
+ if (!empty($_REQUEST['content'])) $this->content = StringUtil::trim($_REQUEST['content']);
+ if (!empty($_REQUEST['showArticleAddDialog'])) $this->showArticleAddDialog = 1;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function initObjectList() {
+ parent::initObjectList();
+
+ if ($this->categoryID) {
+ $this->objectList->getConditionBuilder()->add('article.categoryID = ?', [$this->categoryID]);
+ }
+ if (!empty($this->username)) {
+ $user = User::getUserByUsername($this->username);
+ if ($user->userID) $this->objectList->getConditionBuilder()->add('article.userID = ?', [$user->userID]);
+ else $this->objectList->getConditionBuilder()->add('1=0');
+ }
+ if (!empty($this->title)) {
+ $this->objectList->getConditionBuilder()->add('article.articleID IN (SELECT articleID FROM wcf'.WCF_N.'_article_content WHERE title LIKE ?)', ['%'.$this->title.'%']);
+ }
+ if (!empty($this->content)) {
+ $this->objectList->getConditionBuilder()->add('article.articleID IN (SELECT articleID FROM wcf'.WCF_N.'_article_content WHERE content LIKE ?)', ['%'.$this->content.'%']);
+ }
+
+ $this->objectList->sqlSelects = "(SELECT title FROM wcf".WCF_N."_article_content WHERE articleID = article.articleID AND (languageID IS NULL OR languageID = ".WCF::getLanguage()->languageID.") LIMIT 1) AS title";
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'categoryID' => $this->categoryID,
+ 'username' => $this->username,
+ 'title' => $this->title,
+ 'content' => $this->content,
+ 'showArticleAddDialog' => $this->showArticleAddDialog,
+ 'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator()
+ ]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\article\category\ArticleCategory;
+
+/**
+ * Represents a list of accessible articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ */
+class AccessibleArticleList extends ViewableArticleList {
+ /**
+ * Creates a new AccessibleArticleList object.
+ */
+ public function __construct() {
+ parent::__construct();
+
+ // get accessible categories
+ $accessibleCategoryIDs = ArticleCategory::getAccessibleCategoryIDs();
+ if (empty($accessibleCategoryIDs)) {
+ $this->getConditionBuilder()->add('1=0');
+ }
+ else {
+ $this->getConditionBuilder()->add('article.categoryID IN (?)', [$accessibleCategoryIDs]);
+ $this->getConditionBuilder()->add('article.publicationStatus = ?', [1]);
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\article\category\ArticleCategory;
+use wcf\data\article\content\ArticleContent;
+use wcf\data\DatabaseObject;
+use wcf\data\ILinkableObject;
+use wcf\data\media\ViewableMedia;
+use wcf\system\WCF;
+
+/**
+ * Represents a cms article.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @property-read integer $articleID
+ * @property-read integer $userID
+ * @property-read string $username
+ * @property-read integer $time
+ * @property-read integer $categoryID
+ * @property-read integer $isMultilingual
+ * @property-read integer $publicationStatus
+ * @property-read integer $publicationDate
+ * @property-read integer $enableComments
+ * @property-read integer $comments
+ * @property-read integer $views
+ * @todo
+ */
+class Article extends DatabaseObject implements ILinkableObject {
+ /**
+ * @inheritDoc
+ */
+ protected static $databaseTableName = 'article';
+
+ /**
+ * @inheritDoc
+ */
+ protected static $databaseTableIndexName = 'articleID';
+
+ /**
+ * article content grouped by language id
+ * @var ArticleContent[]
+ */
+ public $articleContent;
+
+ /**
+ * article's category
+ * @var ArticleCategory
+ */
+ protected $category;
+
+ /**
+ * Returns true if the active user can delete this article.
+ *
+ * @return boolean
+ */
+ public function canDelete() {
+ if (WCF::getSession()->getPermission('admin.content.article.canManageArticle')) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the active user has access to this article.
+ *
+ * @return boolean
+ */
+ public function canRead() {
+ if ($this->getCategory()) {
+ return $this->getCategory()->isAccessible();
+ }
+
+ return WCF::getSession()->getPermission('user.article.canRead');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ $this->getArticleContent();
+ if ($this->isMultilingual) {
+ if (isset($this->articleContent[WCF::getLanguage()->languageID])) {
+ return $this->articleContent[WCF::getLanguage()->languageID]->getLink();
+ }
+ }
+ else {
+ if (isset($this->articleContent[0])) {
+ return $this->articleContent[0]->getLink();
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the article's title.
+ *
+ * @return string
+ */
+ public function getTitle() {
+ $this->getArticleContent();
+ if ($this->isMultilingual) {
+ if (isset($this->articleContent[WCF::getLanguage()->languageID])) {
+ return $this->articleContent[WCF::getLanguage()->languageID]->getTitle();
+ }
+ }
+ else {
+ if (isset($this->articleContent[0])) {
+ return $this->articleContent[0]->getTitle();
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the article's teaser.
+ *
+ * @return string
+ */
+ public function getTeaser() {
+ $this->getArticleContent();
+ if ($this->isMultilingual) {
+ if (isset($this->articleContent[WCF::getLanguage()->languageID])) {
+ return $this->articleContent[WCF::getLanguage()->languageID]->teaser;
+ }
+ }
+ else {
+ if (isset($this->articleContent[0])) {
+ return $this->articleContent[0]->teaser;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the article's formatted content.
+ *
+ * @return string
+ */
+ public function getFormattedContent() {
+ $this->getArticleContent();
+ if ($this->isMultilingual) {
+ if (isset($this->articleContent[WCF::getLanguage()->languageID])) {
+ return $this->articleContent[WCF::getLanguage()->languageID]->getFormattedContent();
+ }
+ }
+ else {
+ if (isset($this->articleContent[0])) {
+ return $this->articleContent[0]->getFormattedContent();
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the article's image.
+ *
+ * @return ViewableMedia
+ */
+ public function getImage() {
+ $this->getArticleContent();
+ if ($this->isMultilingual) {
+ if (isset($this->articleContent[WCF::getLanguage()->languageID])) {
+ return $this->articleContent[WCF::getLanguage()->languageID]->getImage();
+ }
+ }
+ else {
+ if (!empty($this->articleContent[0])) {
+ return $this->articleContent[0]->getImage();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the article's content.
+ *
+ * @return ArticleContent[]
+ */
+ public function getArticleContent() {
+ if ($this->articleContent === null) {
+ $this->articleContent = [];
+
+ $sql = "SELECT *
+ FROM wcf" . WCF_N . "_article_content
+ WHERE articleID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->articleID]);
+ while ($row = $statement->fetchArray()) {
+ $this->articleContent[($row['languageID'] ?: 0)] = new ArticleContent(null, $row);
+ }
+ }
+
+ return $this->articleContent;
+ }
+
+ /**
+ * Returns the category of the article.
+ *
+ * @return ArticleCategory
+ */
+ public function getCategory() {
+ if ($this->category === null && $this->categoryID) {
+ $this->category = ArticleCategory::getCategory($this->categoryID);
+ }
+
+ return $this->category;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\article\content\ArticleContent;
+use wcf\data\article\content\ArticleContentEditor;
+use wcf\system\language\LanguageFactory;
+use wcf\system\tagging\TagEngine;
+
+/**
+ * Executes article related actions.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method ArticleEditor[] getObjects()
+ * @method ArticleEditor getSingleObject()
+ */
+class ArticleAction extends AbstractDatabaseObjectAction {
+ /**
+ * @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', 'delete', 'update'];
+
+ /**
+ * @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) {
+ $articleContent = ArticleContentEditor::create([
+ 'articleID' => $article->articleID,
+ 'languageID' => ($languageID ?: null),
+ 'title' => $content['title'],
+ 'teaser' => $content['teaser'],
+ 'content' => $content['content'],
+ 'imageID' => $content['imageID']
+ ]);
+
+ // save tags
+ if (!empty($content['tags'])) {
+ TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()));
+ }
+ }
+ }
+
+ return $article;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function update() {
+ parent::update();
+
+ // update article content
+ if (!empty($this->parameters['content'])) {
+ foreach ($this->getObjects() as $article) {
+ foreach ($this->parameters['content'] as $languageID => $content) {
+ $articleContent = ArticleContent::getArticleContent($article->articleID, ($languageID ?: null));
+ if ($articleContent !== null) {
+ // update
+ $editor = new ArticleContentEditor($articleContent);
+ $editor->update([
+ 'title' => $content['title'],
+ 'teaser' => $content['teaser'],
+ 'content' => $content['content'],
+ 'imageID' => $content['imageID']
+
+ ]);
+
+ // delete tags
+ if (empty($content['tags'])) {
+ TagEngine::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, ($languageID ?: null));
+ }
+ }
+ else {
+ $articleContent = ArticleContentEditor::create([
+ 'articleID' => $article->articleID,
+ 'languageID' => ($languageID ?: null),
+ 'title' => $content['title'],
+ 'teaser' => $content['teaser'],
+ 'content' => $content['content'],
+ 'imageID' => $content['imageID']
+ ]);
+ }
+
+ // save tags
+ if (!empty($content['tags'])) {
+ TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()));
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method Article getDecoratedObject()
+ * @mixin Article
+ */
+class ArticleEditor extends DatabaseObjectEditor {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = Article::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method Article current()
+ * @method Article[] getObjects()
+ * @method Article|null search($objectID)
+ * @property Article[] $objects
+ */
+class ArticleList extends DatabaseObjectList {
+ /**
+ * @inheritDoc
+ */
+ public $className = Article::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\article\category\ArticleCategory;
+use wcf\system\exception\SystemException;
+
+/**
+ * Represents a list of articles in a specific category.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ */
+class CategoryArticleList extends AccessibleArticleList {
+ /** @noinspection PhpMissingParentConstructorInspection */
+ /**
+ * Creates a new CategoryArticleList object.
+ *
+ * @param integer $categoryID
+ * @param boolean $includeChildCategories
+ * @throws SystemException
+ */
+ public function __construct($categoryID, $includeChildCategories = false) {
+ ViewableArticleList::__construct();
+
+ $categoryIDs = [$categoryID];
+ if ($includeChildCategories) {
+ $category = ArticleCategory::getCategory($categoryID);
+ if ($category === null) {
+ throw new SystemException("invalid category id '".$categoryID."' given");
+ }
+ foreach ($category->getChildCategories() as $category) {
+ if ($category->isAccessible()) {
+ $categoryIDs[] = $category->categoryID;
+ }
+ }
+ }
+
+ $this->getConditionBuilder()->add('article.categoryID IN (?)', [$categoryIDs]);
+ $this->getConditionBuilder()->add('article.publicationStatus = ?', [1]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\IFeedEntry;
+use wcf\data\TUserContent;
+use wcf\system\request\LinkHandler;
+use wcf\util\StringUtil;
+
+/**
+ * Represents a viewable article for RSS feeds.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ */
+class FeedArticle extends ViewableArticle implements IFeedEntry {
+ use TUserContent;
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ return $this->getDecoratedObject()->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->getDecoratedObject()->getTitle();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getFormattedMessage() {
+ return $this->getExcerpt();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMessage() {
+ return $this->getDecoratedObject()->getTeaser();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getExcerpt($maxLength = 255) {
+ return StringUtil::encodeHTML($this->getDecoratedObject()->getTeaser());
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function __toString() {
+ return $this->getMessage();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getComments() {
+ return $this->comments;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getCategories() {
+ $categories = [];
+ $category = $this->getDecoratedObject()->getCategory();
+ if ($category !== null) {
+ $categories[] = $category->getTitle();
+ foreach ($category->getParentCategories() as $category) {
+ $categories[] = $category->getTitle();
+ }
+ }
+
+ return $categories;
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function isVisible() {
+ return $this->canRead();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+
+/**
+ * Represents a list of articles for RSS feeds.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method FeedArticle current()
+ * @method FeedArticle[] getObjects()
+ * @method FeedArticle|null search($objectID)
+ */
+class FeedArticleList extends CategoryArticleList {
+ /**
+ * @inheritDoc
+ */
+ public $decoratorClassName = FeedArticle::class;
+
+ /**
+ * Creates a new FeedArticleList object.
+ *
+ * @param integer $categoryID
+ */
+ public function __construct($categoryID = 0) {
+ if ($categoryID) {
+ parent::__construct($categoryID, true);
+ }
+ else {
+ AccessibleArticleList::__construct();
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\like\object\AbstractLikeObject;
+use wcf\data\like\Like;
+
+/**
+ * Likeable object implementation for cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method Article getDecoratedObject()
+ * @mixin Article
+ */
+class LikeableArticle extends AbstractLikeObject {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = Article::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->getDecoratedObject()->getTitle();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getURL() {
+ return $this->getDecoratedObject()->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getUserID() {
+ return $this->userID;
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function getObjectID() {
+ return $this->articleID;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function updateLikeCounter($cumulativeLikes) {
+ // update cumulative likes
+ $editor = new ArticleEditor($this->getDecoratedObject());
+ $editor->update(['cumulativeLikes' => $cumulativeLikes]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sendNotification(Like $like) {
+ /* @todo
+ if ($this->getDecoratedObject()->userID != WCF::getUser()->userID) {
+ $notificationObject = new LikeUserNotificationObject($like);
+ UserNotificationHandler::getInstance()->fireEvent('like', 'com.woltlab.wcf.likeableArticle.notification', $notificationObject, [$this->getDecoratedObject()->userID], [
+ 'objectID' => $this->getDecoratedObject()->articleID
+ ]);
+ }
+ */
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function getLanguageID() {
+ return null;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\like\object\ILikeObject;
+use wcf\data\like\ILikeObjectTypeProvider;
+use wcf\data\object\type\AbstractObjectTypeProvider;
+use wcf\system\like\IViewableLikeProvider;
+use wcf\system\WCF;
+
+/**
+ * Like Object type provider for cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method LikeableArticle getObjectByID($objectID)
+ * @method LikeableArticle[] getObjectsByIDs(array $objectIDs)
+ */
+class LikeableArticleProvider extends AbstractObjectTypeProvider implements ILikeObjectTypeProvider, IViewableLikeProvider {
+ /**
+ * @inheritDoc
+ */
+ public $className = Article::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $listClassName = ArticleList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $decoratorClassName = LikeableArticle::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function checkPermissions(ILikeObject $object) {
+ /** @var LikeableArticle $object */
+ return $object->articleID && $object->canRead();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function prepare(array $likes) {
+ $articleIDs = [];
+ foreach ($likes as $like) {
+ $articleIDs[] = $like->objectID;
+ }
+
+ // fetch articles
+ $articleList = new ViewableArticleList();
+ $articleList->setObjectIDs($articleIDs);
+ $articleList->readObjects();
+ $articles = $articleList->getObjects();
+
+ // set message
+ foreach ($likes as $like) {
+ if (isset($articles[$like->objectID])) {
+ $article = $articles[$like->objectID];
+
+ // check permissions
+ if (!$article->canRead()) {
+ continue;
+ }
+ $like->setIsAccessible();
+
+ // short output
+ $text = WCF::getLanguage()->getDynamicVariable('wcf.like.title.com.woltlab.wcf.likeableArticle', [
+ 'article' => $article,
+ 'like' => $like
+ ]);
+ $like->setTitle($text);
+
+ // output
+ $like->setDescription($article->getTeaser());
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\tag\Tag;
+use wcf\system\tagging\TagEngine;
+
+/**
+ * Represents a list of tagged articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ */
+class TaggedArticleList extends AccessibleArticleList {
+ /**
+ * Creates a new CategoryArticleList object.
+ *
+ * @param Tag $tag
+ */
+ public function __construct(Tag $tag) {
+ parent::__construct();
+
+ $this->getConditionBuilder()->add("article.articleID IN (SELECT articleID FROM wcf".WCF_N."_article_content WHERE articleContentID IN (SELECT objectID FROM wcf".WCF_N."_tag_to_object WHERE objectTypeID = ? AND languageID = ? AND tagID = ?))", [TagEngine::getInstance()->getObjectTypeID('com.woltlab.wcf.article'), $tag->languageID, $tag->tagID]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\article\content\ViewableArticleContent;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\data\user\User;
+use wcf\data\user\UserProfile;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+
+/**
+ * Represents a viewable article.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method Article getDecoratedObject()
+ * @mixin Article
+ */
+class ViewableArticle extends DatabaseObjectDecorator {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = Article::class;
+
+ /**
+ * user profile object
+ * @var UserProfile
+ */
+ protected $userProfile = null;
+
+ /**
+ * Gets a specific article decorated as viewable article.
+ *
+ * @param integer $articleID
+ * @return ViewableArticle
+ */
+ public static function getArticle($articleID) {
+ $list = new ViewableArticleList();
+ $list->setObjectIDs([$articleID]);
+ $list->readObjects();
+ $objects = $list->getObjects();
+ if (isset($objects[$articleID])) return $objects[$articleID];
+ return null;
+ }
+
+ /**
+ * Returns the user profile object.
+ *
+ * @return UserProfile
+ */
+ public function getUserProfile() {
+ if ($this->userProfile === null) {
+ if ($this->userID) {
+ $this->userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->userID);
+ }
+ else {
+ $this->userProfile = new UserProfile(new User(null, [
+ 'username' => $this->username
+ ]));
+ }
+ }
+
+ return $this->userProfile;
+ }
+
+ /**
+ * Sest the article's content.
+ *
+ * @param ViewableArticleContent $articleContent
+ */
+ public function setArticleContent(ViewableArticleContent $articleContent) {
+ if ($this->getDecoratedObject()->articleContent === null) {
+ $this->getDecoratedObject()->articleContent = [];
+ }
+
+ $this->getDecoratedObject()->articleContent[($articleContent->languageID ?: 0)] = $articleContent;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article;
+use wcf\data\article\content\ViewableArticleContentList;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+use wcf\system\like\LikeHandler;
+use wcf\system\WCF;
+
+/**
+ * Represents a list of articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ */
+class ViewableArticleList extends ArticleList {
+ /**
+ * @inheritDoc
+ */
+ public $decoratorClassName = ViewableArticle::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct() {
+ parent::__construct();
+
+ // get like status
+ if (!empty($this->sqlSelects)) $this->sqlSelects .= ',';
+ $this->sqlSelects .= "like_object.likes, like_object.dislikes";
+ $this->sqlJoins .= " LEFT JOIN wcf".WCF_N."_like_object like_object ON (like_object.objectTypeID = ".LikeHandler::getInstance()->getObjectType('com.woltlab.wcf.likeableArticle')->objectTypeID." AND like_object.objectID = article.articleID)";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readObjects() {
+ parent::readObjects();
+
+ $userIDs = [];
+ foreach ($this->getObjects() as $article) {
+ if ($article->userID) {
+ $userIDs[] = $article->userID;
+ }
+ }
+
+ // cache user profiles
+ if (!empty($userIDs)) {
+ UserProfileRuntimeCache::getInstance()->cacheObjectIDs($userIDs);
+ }
+
+ // get article content
+ if (!empty($this->objectIDs)) {
+ $contentList = new ViewableArticleContentList();
+ $contentList->getConditionBuilder()->add('article_content.articleID IN (?)', [$this->objectIDs]);
+ $contentList->getConditionBuilder()->add('(article_content.languageID IS NULL OR article_content.languageID = ?)', [WCF::getLanguage()->languageID]);
+ $contentList->readObjects();
+ foreach ($contentList as $articleContent) {
+ $this->objects[$articleContent->articleID]->setArticleContent($articleContent);
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article\category;
+use wcf\data\category\AbstractDecoratedCategory;
+use wcf\data\user\User;
+use wcf\data\ITitledLinkObject;
+use wcf\data\user\UserProfile;
+use wcf\system\category\CategoryHandler;
+use wcf\system\category\CategoryPermissionHandler;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+/**
+ * Represents an article category.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article.category
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method static ArticleCategory|null getCategory($categoryID)
+ */
+class ArticleCategory extends AbstractDecoratedCategory implements ITitledLinkObject {
+ /**
+ * object type name of the article categories
+ * @var string
+ */
+ const OBJECT_TYPE_NAME = 'com.woltlab.wcf.article.category';
+
+ /**
+ * acl permissions of this category grouped by the id of the user they
+ * belong to
+ * @var array
+ */
+ protected $userPermissions = [];
+
+ /**
+ * Returns true if the category is accessible for the active user.
+ *
+ * @param User $user
+ * @return boolean
+ */
+ public function isAccessible(User $user = null) {
+ if ($this->getObjectType()->objectType != self::OBJECT_TYPE_NAME) return false;
+
+ // check permissions
+ return $this->getPermission('canReadArticle', $user);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPermission($permission, User $user = null) {
+ if ($user === null) {
+ $user = WCF::getUser();
+ }
+
+ if (!isset($this->userPermissions[$user->userID])) {
+ $this->userPermissions[$user->userID] = CategoryPermissionHandler::getInstance()->getPermissions($this->getDecoratedObject(), $user);
+ }
+
+ if (isset($this->userPermissions[$user->userID][$permission])) {
+ return $this->userPermissions[$user->userID][$permission];
+ }
+
+ if ($this->getParentCategory()) {
+ return $this->getParentCategory()->getPermission($permission, $user);
+ }
+
+ if ($user->userID === WCF::getSession()->getUser()->userID) {
+ return WCF::getSession()->getPermission('user.article.'.$permission);
+ }
+ else {
+ $userProfile = new UserProfile($user);
+ return $userProfile->getPermission('user.article.'.$permission);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ return LinkHandler::getInstance()->getLink('CategoryArticleList', [
+ 'object' => $this->getDecoratedObject()
+ ]);
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return WCF::getLanguage()->get($this->title);
+ }
+
+ /**
+ * Returns a list with ids of accessible categories.
+ *
+ * @param string[] $permissions
+ * @return integer[]
+ */
+ public static function getAccessibleCategoryIDs(array $permissions = ['canReadArticle']) {
+ $categoryIDs = [];
+ foreach (CategoryHandler::getInstance()->getCategories(self::OBJECT_TYPE_NAME) as $category) {
+ $result = true;
+ $category = new ArticleCategory($category);
+ foreach ($permissions as $permission) {
+ $result = $result && $category->getPermission($permission);
+ }
+
+ if ($result) {
+ $categoryIDs[] = $category->categoryID;
+ }
+ }
+
+ return $categoryIDs;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article\content;
+use wcf\data\article\Article;
+use wcf\data\DatabaseObject;
+use wcf\data\ILinkableObject;
+use wcf\system\html\output\HtmlOutputProcessor;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\request\IRouteController;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+/**
+ * Represents an article content.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article.content
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @property-read integer $articleContentID
+ * @property-read integer $articleID
+ * @property-read integer $languageID
+ * @property-read string $title
+ * @property-read string $content
+ * @property-read string $teaser
+ * @property-read integer $imageID
+ */
+class ArticleContent extends DatabaseObject implements ILinkableObject, IRouteController {
+ /**
+ * @inheritDoc
+ */
+ protected static $databaseTableName = 'article_content';
+
+ /**
+ * @inheritDoc
+ */
+ protected static $databaseTableIndexName = 'articleContentID';
+
+ /**
+ * article object
+ * @var Article
+ */
+ protected $article;
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ return LinkHandler::getInstance()->getLink('Article', [
+ 'object' => $this,
+ 'forceFrontend' => true
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Returns the article's formatted content.
+ *
+ * @return string
+ */
+ public function getFormattedContent() {
+ // assign embedded objects
+ MessageEmbeddedObjectManager::getInstance()->setActiveMessage('com.woltlab.wcf.article', $this->articleContentID);
+
+ // TODO
+ return (new HtmlOutputProcessor())->process($this->content);
+ }
+
+ /**
+ * Returns article object.
+ *
+ * @return Article
+ */
+ public function getArticle() {
+ if ($this->article === null) {
+ $this->article = new Article($this->articleID);
+ }
+
+ return $this->article;
+ }
+
+ public static function getArticleContent($articleID, $languageID) {
+ if ($languageID !== null) {
+ $sql = "SELECT *
+ FROM wcf" . WCF_N . "_article_content
+ WHERE articleID = ?
+ AND languageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$articleID, $languageID]);
+ }
+ else {
+ $sql = "SELECT *
+ FROM wcf" . WCF_N . "_article_content
+ WHERE articleID = ?
+ AND languageID IS NULL";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$articleID]);
+ }
+
+ if (($row = $statement->fetchSingleRow()) !== false) {
+ return new ArticleContent(null, $row);
+ }
+
+ return null;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article\content;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes article content related actions.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article.content
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method ArticleContentEditor[] getObjects()
+ * @method ArticleContentEditor getSingleObject()
+ */
+class ArticleContentAction extends AbstractDatabaseObjectAction {
+ /**
+ * @inheritDoc
+ */
+ protected $className = ArticleContentEditor::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\article\content;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit article content.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article.content
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method ArticleContent getDecoratedObject()
+ * @mixin ArticleContent
+ */
+class ArticleContentEditor extends DatabaseObjectEditor {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = ArticleContent::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\article\content;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of article content.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article.content
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method ArticleContent current()
+ * @method ArticleContent[] getObjects()
+ * @method ArticleContent|null search($objectID)
+ * @property ArticleContent[] $objects
+ */
+class ArticleContentList extends DatabaseObjectList {
+ /**
+ * @inheritDoc
+ */
+ public $className = ArticleContent::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\article\content;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\data\media\ViewableMedia;
+
+/**
+ * Represents a viewable article content.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method ArticleContent getDecoratedObject()
+ * @mixin ArticleContent
+ */
+class ViewableArticleContent extends DatabaseObjectDecorator {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = ArticleContent::class;
+
+ /**
+ * article image
+ * @var ViewableMedia
+ */
+ protected $image;
+
+ /**
+ * Gets a specific article content decorated as viewable article content.
+ *
+ * @param integer $articleContentID
+ * @return ViewableArticleContent
+ */
+ public static function getArticleContent($articleContentID) {
+ $list = new ViewableArticleContentList();
+ $list->setObjectIDs([$articleContentID]);
+ $list->readObjects();
+ $objects = $list->getObjects();
+ if (isset($objects[$articleContentID])) return $objects[$articleContentID];
+ return null;
+ }
+
+ /**
+ * Returns the article's image.
+ *
+ * @return ViewableMedia
+ */
+ public function getImage() {
+ if ($this->image === null) {
+ if ($this->imageID) {
+ $this->image = ViewableMedia::getMedia($this->imageID);
+ }
+ }
+
+ return $this->image;
+ }
+
+ /**
+ * Sets the article's image.
+ *
+ * @param ViewableMedia $image
+ */
+ public function setImage(ViewableMedia $image) {
+ $this->image = $image;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\article\content;
+use wcf\data\media\ViewableMediaList;
+
+/**
+ * Represents a list of viewable article contents.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.article
+ * @category Community Framework
+ * @since 2.2
+ */
+class ViewableArticleContentList extends ArticleContentList {
+ /**
+ * @inheritDoc
+ */
+ public $decoratorClassName = ViewableArticleContent::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function readObjects() {
+ parent::readObjects();
+
+ $imageIDs = [];
+ foreach ($this->getObjects() as $articleContent) {
+ if ($articleContent->imageID) {
+ $imageIDs[] = $articleContent->imageID;
+ }
+ }
+
+ // cache images
+ if (!empty($imageIDs)) {
+ $mediaList = new ViewableMediaList();
+ $mediaList->setObjectIDs($imageIDs);
+ $mediaList->readObjects();
+ $images = $mediaList->getObjects();
+
+ foreach ($this->getObjects() as $articleContent) {
+ if ($articleContent->imageID && isset($images[$articleContent->imageID])) {
+ $articleContent->setImage($images[$articleContent->imageID]);
+ }
+ }
+ }
+ }
+}
return $i;
}
+ /**
+ * Returns node depth.
+ *
+ * @return integer
+ */
+ public function getDepth() {
+ $element = $this;
+ $depth = 1;
+
+ while ($element->parentNode->parentNode != null) {
+ $depth++;
+ $element = $element->parentNode;
+ }
+
+ return $depth;
+ }
+
/**
* @inheritDoc
*/
*/
public function __toString() {
if ($this->isImage) {
- return '<img src="'.$this->getLink().'" alt="'.StringUtil::encodeHTML($this->altText).'" />';
+ return '<img src="'.$this->getLink().'" alt="'.StringUtil::encodeHTML($this->altText).'" '.($this->title ? 'title="'.StringUtil::encodeHTML($this->title).'" ' : '').'/>';
}
return '<a href="'.$this->getLink().'>'.$this->getTitle().'</a>';
if ($this->isImage && $this->tinyThumbnailType) {
$tinyThumbnail = Media::getThumbnailSizes()['tiny'];
if ($size <= $tinyThumbnail['width'] && $size <= $tinyThumbnail['height']) {
- return '<img src="' . $this->getThumbnailLink('tiny') . '" alt="' . StringUtil::encodeHTML($this->altText) . '" style="width: ' . $size . 'px; height: ' . $size . 'px;" />';
+ return '<img src="' . $this->getThumbnailLink('tiny') . '" alt="' . StringUtil::encodeHTML($this->altText) . '" '.($this->title ? 'title="'.StringUtil::encodeHTML($this->title).'" ' : '').'style="width: ' . $size . 'px; height: ' . $size . 'px;" />';
}
}
* @return string
* @throws SystemException
*/
- public function getThumbnailTag($size = '') {
+ public function getThumbnailTag($size = 'tiny') {
if (!isset(Media::getThumbnailSizes()[$size])) {
throw new SystemException("Unknown thumbnail size '".$size."'");
}
- return '<img src="'.$this->getThumbnailLink($size).'" alt="'.StringUtil::encodeHTML($this->altText).'" />';
+ return '<img src="'.$this->getThumbnailLink($size).'" alt="'.StringUtil::encodeHTML($this->altText).'" '.($this->title ? 'title="'.StringUtil::encodeHTML($this->title).'" ' : '').'/>';
}
/**
use wcf\system\WCF;
/**
- * Represents a list of viewable madia files.
+ * Represents a list of viewable media files.
*
* @author Matthias Schmidt
* @copyright 2001-2016 WoltLab GmbH
* @return \wcf\system\page\handler\IMenuPageHandler|null
*/
public function getHandler() {
- if ($this->handler) {
+ if ($this->pageHandler === null && $this->handler) {
$this->pageHandler = new $this->handler();
}
// assign parameters
WCF::getTPL()->assign([
'action' => $this->action,
- 'templateName' => $this->templateName
+ 'templateName' => $this->templateName,
+ 'canonicalURL' => $this->canonicalURL
]);
}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\article\category\ArticleCategory;
+use wcf\data\article\FeedArticleList;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\WCF;
+
+/**
+ * Shows a list of cms articles in a certain category in feed.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage page
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleFeedPage extends AbstractFeedPage {
+ /**
+ * category the listed articles belong to
+ * @var ArticleCategory
+ */
+ public $category;
+
+ /**
+ * id of the category the listed articles belong to
+ * @var integer
+ */
+ public $categoryID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) {
+ $this->categoryID = intval($_REQUEST['id']);
+ $this->category = ArticleCategory::getCategory($this->categoryID);
+ if ($this->category === null) {
+ throw new IllegalLinkException();
+ }
+ if (!$this->category->isAccessible()) {
+ throw new PermissionDeniedException();
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ // read the articles
+ $this->items = new FeedArticleList($this->categoryID);
+ $this->items->sqlLimit = 20;
+ $this->items->readObjects();
+
+ // set title
+ if ($this->category !== null) {
+ $this->title = $this->category->getTitle();
+ }
+ else {
+ $this->title = WCF::getLanguage()->get('wcf.article.articles');
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\article\AccessibleArticleList;
+use wcf\system\request\LinkHandler;
+
+/**
+ * Shows a list of cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage page
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleListPage extends MultipleLinkPage {
+ /**
+ * @inheritDoc
+ */
+ public $itemsPerPage = ARTICLES_PER_PAGE;
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+
+ /**
+ * @inheritDoc
+ */
+ public $sqlOrderBy = 'article.time DESC';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = AccessibleArticleList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ $this->canonicalURL = LinkHandler::getInstance()->getLink('ArticleList', [], ($this->pageNo > 1 ? 'pageNo=' . $this->pageNo : ''));
+ }
+}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\article\AccessibleArticleList;
+use wcf\data\article\ArticleEditor;
+use wcf\data\article\CategoryArticleList;
+use wcf\data\article\content\ViewableArticleContent;
+use wcf\data\article\ViewableArticle;
+use wcf\data\comment\StructuredCommentList;
+use wcf\data\like\object\LikeObject;
+use wcf\data\tag\Tag;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\like\LikeHandler;
+use wcf\system\MetaTagHandler;
+use wcf\system\page\PageLocationManager;
+use wcf\system\request\LinkHandler;
+use wcf\system\tagging\TagEngine;
+use wcf\system\WCF;
+
+/**
+ * Shows a cms article.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage page
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticlePage extends AbstractPage {
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['MODULE_ARTICLE'];
+
+ /**
+ * article content id
+ * @var integer
+ */
+ public $articleContentID = 0;
+
+ /**
+ * article content object
+ * @var ViewableArticleContent
+ */
+ public $articleContent;
+
+ /**
+ * article object
+ * @var ViewableArticle
+ */
+ public $article;
+
+ /**
+ * next article in this category
+ * @var ViewableArticle
+ */
+ public $nextArticle;
+
+ /**
+ * previous article in this category
+ * @var ViewableArticle
+ */
+ public $previousArticle;
+
+ /**
+ * comment object type id
+ * @var integer
+ */
+ public $commentObjectTypeID = 0;
+
+ /**
+ * comment manager object
+ * @var ICommentManager
+ */
+ public $commentManager;
+
+ /**
+ * list of comments
+ * @var StructuredCommentList
+ */
+ public $commentList;
+
+ /**
+ * list of related articles
+ * @var AccessibleArticleList
+ */
+ public $relatedArticles;
+
+ /**
+ * list of tags
+ * @var Tag[]
+ */
+ public $tags = [];
+
+ /**
+ * like data for the article
+ * @var LikeObject[]
+ */
+ public $articleLikeData = [];
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->articleContentID = intval($_REQUEST['id']);
+ $this->articleContent = ViewableArticleContent::getArticleContent($this->articleContentID);
+ if ($this->articleContent === null) {
+ throw new IllegalLinkException();
+ }
+ $this->article = ViewableArticle::getArticle($this->articleContent->articleID);
+ $this->canonicalURL = $this->articleContent->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function checkPermissions() {
+ parent::checkPermissions();
+
+ if (!$this->article->canRead()) {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ // update view count
+ $articleEditor = new ArticleEditor($this->article->getDecoratedObject());
+ $articleEditor->updateCounters([
+ 'views' => 1
+ ]);
+
+ // get comments
+ if ($this->article->enableComments) {
+ $this->commentObjectTypeID = CommentHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article');
+ $this->commentManager = CommentHandler::getInstance()->getObjectType($this->commentObjectTypeID)->getProcessor();
+ $this->commentList = CommentHandler::getInstance()->getCommentList($this->commentManager, $this->commentObjectTypeID, $this->articleContent->articleContentID);
+ }
+
+ // get next entry
+ $articleList = new CategoryArticleList($this->article->categoryID);
+ $articleList->getConditionBuilder()->add('article.time > ?', [$this->article->time]);
+ $articleList->sqlOrderBy = 'article.time';
+ $articleList->sqlLimit = 1;
+ $articleList->readObjects();
+ foreach ($articleList as $article) $this->nextArticle = $article;
+
+ // get previous entry
+ $articleList = new CategoryArticleList($this->article->categoryID);
+ $articleList->getConditionBuilder()->add('article.time < ?', [$this->article->time]);
+ $articleList->sqlOrderBy = 'article.time';
+ $articleList->sqlLimit = 1;
+ $articleList->readObjects();
+ foreach ($articleList as $article) $this->previousArticle = $article;
+
+ // get tags
+ if (MODULE_TAGGING && WCF::getSession()->getPermission('user.tag.canViewTag')) {
+ $this->tags = TagEngine::getInstance()->getObjectTags(
+ 'com.woltlab.wcf.article',
+ $this->articleContent->articleContentID,
+ [($this->articleContent->languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID())]
+ );
+ }
+
+ // get related articles
+ if (MODULE_TAGGING && ARTICLE_RELATED_ARTICLES) {
+ if (!empty($this->tags)) {
+ $conditionBuilder = new PreparedStatementConditionBuilder();
+ $conditionBuilder->add('objectTypeID = ?', [TagEngine::getInstance()->getObjectTypeID('com.woltlab.wcf.article')]);
+ $conditionBuilder->add('tagID IN (?)', [array_keys($this->tags)]);
+ $conditionBuilder->add('objectID <> ?', [$this->articleContentID]);
+ $sql = "SELECT objectID, COUNT(*) AS count
+ FROM wcf" . WCF_N . "_tag_to_object
+ " . $conditionBuilder . "
+ GROUP BY objectID
+ HAVING count > " . (round(count($this->tags) * (ARTICLE_RELATED_ARTICLES_MATCH_THRESHOLD / 100))) . "
+ ORDER BY count DESC";
+ $statement = WCF::getDB()->prepareStatement($sql, ARTICLE_RELATED_ARTICLES);
+ $statement->execute($conditionBuilder->getParameters());
+ $articleContentIDs = [];
+ while ($row = $statement->fetchArray()) {
+ $articleContentIDs[] = $row['objectID'];
+ }
+
+ if (!empty($articleContentIDs)) {
+ $conditionBuilder = new PreparedStatementConditionBuilder();
+ $conditionBuilder->add('articleContentID IN (?)', [$articleContentIDs]);
+ $sql = "SELECT articleID
+ FROM wcf" . WCF_N . "_article_content
+ " . $conditionBuilder;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditionBuilder->getParameters());
+ $articleIDs = [];
+ while ($row = $statement->fetchArray()) {
+ $articleIDs[] = $row['articleID'];
+ }
+
+ $this->relatedArticles = new AccessibleArticleList();
+ $this->relatedArticles->getConditionBuilder()->add('article.articleID IN (?)', [$articleIDs]);
+ $this->relatedArticles->sqlOrderBy = 'article.time';
+ $this->relatedArticles->readObjects();
+ }
+ }
+ }
+
+ // fetch likes
+ if (MODULE_LIKE) {
+ $objectType = LikeHandler::getInstance()->getObjectType('com.woltlab.wcf.likeableArticle');
+ LikeHandler::getInstance()->loadLikeObjects($objectType, [$this->article->articleID]);
+ $this->articleLikeData = LikeHandler::getInstance()->getLikeObjects($objectType);
+ }
+
+ // set location
+ PageLocationManager::getInstance()->addParentLocation('com.woltlab.wcf.CategoryArticleList', $this->article->categoryID, $this->article->getCategory());
+ foreach ($this->article->getCategory()->getParentCategories() as $parentCategory) {
+ PageLocationManager::getInstance()->addParentLocation('com.woltlab.wcf.CategoryArticleList', $parentCategory->categoryID, $parentCategory);
+ }
+ PageLocationManager::getInstance()->addParentLocation('com.woltlab.wcf.ArticleList');
+
+ // add meta/og tags
+ MetaTagHandler::getInstance()->addTag('og:title', 'og:title', $this->articleContent->getTitle() . ' - ' . WCF::getLanguage()->get(PAGE_TITLE), true);
+ MetaTagHandler::getInstance()->addTag('og:url', 'og:url', LinkHandler::getInstance()->getLink('Article', ['object' => $this->articleContent, 'appendSession' => false]), true);
+ MetaTagHandler::getInstance()->addTag('og:type', 'og:type', 'article', true);
+ MetaTagHandler::getInstance()->addTag('og:description', 'og:description', $this->articleContent->teaser, true);
+
+ if ($this->articleContent->getImage()) {
+ MetaTagHandler::getInstance()->addTag('og:image', 'og:image', $this->articleContent->getImage()->getLink(), true);
+ MetaTagHandler::getInstance()->addTag('og:image:width', 'og:image:width', $this->articleContent->getImage()->width, true);
+ MetaTagHandler::getInstance()->addTag('og:image:height', 'og:image:height', $this->articleContent->getImage()->height, true);
+ }
+
+ // add tags as keywords
+ if (!empty($this->tags)) {
+ $keywords = '';
+ foreach ($this->tags as $tag) {
+ if (!empty($keywords)) $keywords .= ', ';
+ $keywords .= $tag->name;
+ }
+ MetaTagHandler::getInstance()->addTag('keywords', 'keywords', $keywords);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'articleContentID' => $this->articleContentID,
+ 'articleContent' => $this->articleContent,
+ 'article' => $this->article,
+ 'previousArticle' => $this->previousArticle,
+ 'nextArticle' => $this->nextArticle,
+ 'commentCanAdd' => WCF::getSession()->getPermission('user.article.canAddComment'),
+ 'commentList' => $this->commentList,
+ 'commentObjectTypeID' => $this->commentObjectTypeID,
+ 'lastCommentTime' => ($this->commentList ? $this->commentList->getMinCommentTime() : 0),
+ 'likeData' => ((MODULE_LIKE && $this->commentList) ? $this->commentList->getLikeData() : []),
+ 'relatedArticles' => $this->relatedArticles,
+ 'tags' => $this->tags,
+ 'articleLikeData' => $this->articleLikeData
+ ]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\article\category\ArticleCategory;
+use wcf\data\article\CategoryArticleList;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\page\PageLocationManager;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+/**
+ * Shows a list of cms articles in a certain category.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage page
+ * @category Community Framework
+ * @since 2.2
+ */
+class CategoryArticleListPage extends ArticleListPage {
+ /**
+ * category the listed articles belong to
+ * @var ArticleCategory
+ */
+ public $category;
+
+ /**
+ * id of the category the listed articles belong to
+ * @var integer
+ */
+ public $categoryID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->categoryID = intval($_REQUEST['id']);
+ $this->category = ArticleCategory::getCategory($this->categoryID);
+ if ($this->category === null) {
+ throw new IllegalLinkException();
+ }
+
+ $this->canonicalURL = LinkHandler::getInstance()->getLink('CategoryArticleList', [
+ 'object' => $this->category
+ ], ($this->pageNo > 1 ? 'pageNo=' . $this->pageNo : ''));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function checkPermissions() {
+ parent::checkPermissions();
+
+ if (!$this->category->isAccessible()) {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ protected function initObjectList() {
+ $this->objectList = new CategoryArticleList($this->categoryID, true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ // set location
+ foreach ($this->category->getParentCategories() as $parentCategory) {
+ PageLocationManager::getInstance()->addParentLocation('com.woltlab.wcf.CategoryArticleList', $parentCategory->categoryID, $parentCategory);
+ }
+ PageLocationManager::getInstance()->addParentLocation('com.woltlab.wcf.ArticleList');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'categoryID' => $this->categoryID,
+ 'category' => $this->category
+ ]);
+ }
+}
\ No newline at end of file
// cache all necessary data for showing locations
foreach ($this->objectList as $userOnline) {
- if ($userOnline->controller) {
- $page = PageCache::getInstance()->getPage($userOnline->pageID);
- if ($page !== null && $page->getHandler() !== null && $page->getHandler() instanceof IOnlineLocationPageHandler) {
- /** @noinspection PhpUndefinedMethodInspection */
- $page->getHandler()->prepareOnlineLocation($page, $userOnline);
- }
+ $page = PageCache::getInstance()->getPage($userOnline->pageID);
+ if ($page !== null && $page->getHandler() !== null && $page->getHandler() instanceof IOnlineLocationPageHandler) {
+ /** @noinspection PhpUndefinedMethodInspection */
+ $page->getHandler()->prepareOnlineLocation($page, $userOnline);
}
}
--- /dev/null
+<?php
+namespace wcf\system\cache\runtime;
+use wcf\data\article\ViewableArticle;
+use wcf\data\article\ViewableArticleList;
+
+/**
+ * Runtime cache implementation for viewable articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.cache.runtime
+ * @category Community Framework
+ * @since 2.2
+ *
+ * @method ViewableArticle[] getCachedObjects()
+ * @method ViewableArticle getObject($objectID)
+ * @method ViewableArticle[] getObjects(array $objectIDs)
+ */
+class ViewableArticleRuntimeCache extends AbstractRuntimeCache {
+ /**
+ * @inheritDoc
+ */
+ protected $listClassName = ViewableArticleList::class;
+}
--- /dev/null
+<?php
+namespace wcf\system\category;
+use wcf\system\WCF;
+
+/**
+ * Category type implementation for article categories.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.category
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticleCategoryType extends AbstractCategoryType {
+ /**
+ * @inheritDoc
+ */
+ protected $langVarPrefix = 'wcf.article.category';
+
+ /**
+ * @inheritDoc
+ */
+ protected $hasDescription = false;
+
+ /**
+ * @inheritDoc
+ */
+ protected $objectTypes = ['com.woltlab.wcf.acl' => 'com.woltlab.wcf.article.category'];
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function canAddCategory() {
+ return $this->canEditCategory();
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function canDeleteCategory() {
+ return $this->canEditCategory();
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function canEditCategory() {
+ return WCF::getSession()->getPermission('admin.content.article.canManageCategory');
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\comment\manager;
+use wcf\data\article\ArticleEditor;
+use wcf\data\article\content\ArticleContent;
+use wcf\system\WCF;
+
+/**
+ * Article comment manager implementation.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.comment.manager
+ * @category Community Framework
+ */
+class ArticleCommentManager extends AbstractCommentManager {
+ /**
+ * @inheritDoc
+ */
+ protected $permissionAdd = 'user.article.canAddComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionDelete = 'user.article.canDeleteComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionEdit = 'user.article.canEditComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionModDelete = 'mod.article.canDeleteComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionModEdit = 'mod.article.canEditComment';
+
+ /**
+ * @inheritDoc
+ */
+ public function isAccessible($objectID, $validateWritePermission = false) {
+ // check object id
+ $articleContent = new ArticleContent($objectID);
+ if (!$articleContent->articleContentID || !$articleContent->getArticle()->canRead()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink($objectTypeID, $objectID) {
+ return (new ArticleContent($objectID))->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle($objectTypeID, $objectID, $isResponse = false) {
+ if ($isResponse) return WCF::getLanguage()->get('wcf.article.commentResponse');
+
+ return WCF::getLanguage()->getDynamicVariable('wcf.article.comment');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function updateCounter($objectID, $value) {
+ $articleContent = new ArticleContent($objectID);
+ $editor = new ArticleEditor($articleContent->getArticle());
+ $editor->updateCounters([
+ 'comments' => $value
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsLike() {
+ // @todo
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsReport() {
+ // @todo
+ return false;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\page\handler;
+use wcf\data\page\Page;
+use wcf\data\user\online\UserOnline;
+use wcf\system\cache\runtime\ViewableArticleRuntimeCache;
+use wcf\system\WCF;
+
+/**
+ * Menu page handler for the article page.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.page.handler
+ * @category Community Framework
+ * @since 2.2
+ */
+class ArticlePageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler {
+ use TOnlineLocationPageHandler;
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink($objectID) {
+ return ViewableArticleRuntimeCache::getInstance()->getObject($objectID)->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isValid($objectID) {
+ return ViewableArticleRuntimeCache::getInstance()->getObject($objectID) !== null;
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function isVisible($objectID = null) {
+ return ViewableArticleRuntimeCache::getInstance()->getObject($objectID)->canRead();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function lookup($searchString) {
+ // @todo
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getOnlineLocation(Page $page, UserOnline $user) {
+ if ($user->pageObjectID === null) {
+ return '';
+ }
+
+ $article = ViewableArticleRuntimeCache::getInstance()->getObject($user->pageObjectID);
+ if ($article === null || !$article->canRead()) {
+ return '';
+ }
+
+ return WCF::getLanguage()->getDynamicVariable('wcf.page.onlineLocation.'.$page->identifier, ['article' => $article]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function prepareOnlineLocation(/** @noinspection PhpUnusedParameterInspection */Page $page, UserOnline $user) {
+ if ($user->pageObjectID !== null) {
+ ViewableArticleRuntimeCache::getInstance()->cacheObjectID($user->pageObjectID);
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\page\handler;
+use wcf\data\article\category\ArticleCategory;
+use wcf\data\page\Page;
+use wcf\data\user\online\UserOnline;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+/**
+ * Menu page handler for the category article list page.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.page.handler
+ * @category Community Framework
+ * @since 2.2
+ */
+class CategoryArticleListPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler {
+ use TOnlineLocationPageHandler;
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink($objectID) {
+ return LinkHandler::getInstance()->getLink('CategoryArticleList', [
+ 'object' => ArticleCategory::getCategory($objectID),
+ 'forceFrontend' => true
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isValid($objectID) {
+ return ArticleCategory::getCategory($objectID) !== null;
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function isVisible($objectID = null) {
+ return ArticleCategory::getCategory($objectID)->isAccessible();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function lookup($searchString) {
+ // @todo
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getOnlineLocation(Page $page, UserOnline $user) {
+ if ($user->pageObjectID === null) {
+ return '';
+ }
+
+ $category = ArticleCategory::getCategory($user->pageObjectID);
+ if ($category === null || !$category->isAccessible()) {
+ return '';
+ }
+
+ return WCF::getLanguage()->getDynamicVariable('wcf.page.onlineLocation.'.$page->identifier, ['category' => $category]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\tagging;
+use wcf\data\article\TaggedArticleList;
+use wcf\data\tag\Tag;
+
+/**
+ * Implementation of ITaggable for tagging of cms articles.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.tagging
+ * @category Community Framework
+ * @since 2.2
+ */
+class TaggableArticle extends AbstractTaggable {
+ /**
+ * @inheritDoc
+ */
+ public function getObjectList(Tag $tag) {
+ return new TaggedArticleList($tag);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTemplateName() {
+ return 'articleListItems';
+ }
+}
--- /dev/null
+.articleContentHeader {
+ .contentHeaderTitle {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .contentTitle,
+ .contentHeaderDescription {
+ width: 100%;
+ }
+
+ .contentTitle {
+ margin-top: 5px;
+ }
+
+ .contentHeaderDescription {
+ color: $wcfContentText;
+ margin-top: 0;
+ order: -1;
+ text-transform: uppercase;
+ }
+}
+
+.articleImage {
+ .articleImageWrapper {
+ align-items: center;
+ display: flex;
+ height: 300px;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ }
+ }
+
+ figcaption {
+ color: $wcfContentDimmedText;
+ margin-top: 5px;
+ text-align: center;
+
+ @include wcfFontSmall;
+ }
+}
+
+.articleContent {
+ .articleTeaser {
+ @include wcfFontBold;
+ }
+
+ .articleTagList {
+ margin-top: 20px;
+
+ > li {
+ padding-left: 11px;
+ }
+
+ .articleTag {
+ background-color: $wcfButtonBackground;
+ color: $wcfButtonText;
+ display: inline-block;
+ padding: 3px 6px 2px 2px;
+ position: relative;
+ text-transform: uppercase;
+
+ @include wcfFontSmall;
+ @include wcfFontBold;
+
+ &::before {
+ border: 11px solid transparent;
+ border-left-width: 0;
+ border-right-color: $wcfButtonBackground;
+ content: "";
+ display: block;
+ left: -11px;
+ position: absolute;
+ top: 0;
+ }
+
+ &:hover {
+ background-color: $wcfButtonPrimaryBackgroundActive;
+ color: $wcfButtonPrimaryTextActive;
+
+ &::before {
+ border-right-color: $wcfButtonPrimaryBackgroundActive;
+ }
+ }
+ }
+ }
+
+ .articleLikesSummery:not(:empty) {
+ margin-top: 20px;
+ }
+
+ .articleLikeButtons:not(:empty) {
+ margin-top: 20px;
+ }
+}
+
+.articleAboutAuthor {
+ .articleAboutAuthorText {
+ font-style: italic;
+ }
+
+ .articleAboutAuthorUsername {
+ margin-top: 5px;
+
+ .username {
+ @include wcfFontHeadline;
+ }
+
+ .userTitleBadge {
+ top: -2px;
+ }
+ }
+}
+
+.articleNavigation {
+ @include screen-md-up {
+ > nav > ul {
+ display: flex;
+ }
+ }
+
+ .previousArticleButton,
+ .nextArticleButton {
+ @include screen-md-up {
+ width: 50%;
+ }
+
+ > a {
+ color: $wcfContentText;
+ display: block;
+
+ &::before {
+ font-family: FontAwesome;
+ font-size: 36px;
+ display: block;
+ margin-top: 20px;
+ }
+
+ .articleNavigationEntityName,
+ .articleNavigationArticleTitle {
+ display: block;
+ }
+
+ .articleNavigationEntityName {
+ text-transform: uppercase;
+ }
+
+ .articleNavigationArticleTitle {
+ margin-top: 3px;
+
+ @include wcfFontHeadline;
+ }
+
+ .articleNavigationArticleImage {
+ > img {
+ border-radius: 2px;
+ opacity: .85;
+ transition: .2s ease opacity;
+ }
+ }
+
+ &:hover {
+ &::before {
+ color: $wcfContentLinkActive;
+ }
+
+ .articleNavigationArticleTitle {
+ color: $wcfContentLinkActive;
+ }
+
+ .articleNavigationArticleImage {
+ > img {
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+
+ .previousArticleButton {
+ @include screen-md-up {
+ padding-right: 10px;
+ }
+
+ > a {
+ &::before {
+ float: left;
+ content: $fa-var-chevron-left;
+ }
+
+ > div {
+ margin-left: 36px;
+ }
+ }
+ }
+
+ .nextArticleButton {
+ text-align: right;
+
+ @include screen-md-up {
+ margin-left: 50%;
+ padding-left: 10px;
+ }
+
+ .articleNavigationArticleImage {
+ order: 1;
+ margin-left: 15px;
+ margin-right: 0;
+ }
+
+ > a {
+ &::before {
+ float: right;
+ content: $fa-var-chevron-right;
+ }
+
+ > div {
+ margin-right: 36px;
+ }
+ }
+ }
+
+ .previousArticleButton + .nextArticleButton {
+ @include screen-sm-down {
+ margin-top: 20px;
+ }
+
+ @include screen-md-up {
+ margin-left: 0;
+ }
+ }
+}
+
+.articleList {
+ .articleListMetaData {
+ color: $wcfContentDimmedText;
+ margin-top: 2px;
+
+ .icon {
+ color: inherit;
+ }
+ }
+
+ a {
+ color: $wcfContentText;
+
+ &:hover {
+ .articleListImage > img {
+ opacity: 1;
+ }
+
+ .articleListTitle {
+ color: $wcfContentLinkActive;
+ }
+ }
+ }
+
+ .articleListImage > img {
+ border-radius: 2px;
+ opacity: .85;
+ transition: .2s ease opacity;
+ }
+
+ &:not(.rowColGap) {
+ > li:not(:first-child) {
+ margin-top: 30px;
+ }
+ }
+}
\ No newline at end of file
cookiePath VARCHAR(255) NOT NULL DEFAULT '/'
);
+DROP TABLE IF EXISTS wcf1_article;
+CREATE TABLE wcf1_article (
+ articleID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ userID INT(10),
+ username VARCHAR(255) NOT NULL DEFAULT '',
+ time INT(10) NOT NULL DEFAULT 0,
+ categoryID INT(10),
+ isMultilingual TINYINT(1) NOT NULL DEFAULT 0,
+ publicationStatus TINYINT(1) NOT NULL DEFAULT 1,
+ publicationDate INT(10) NOT NULL DEFAULT 0,
+ enableComments TINYINT(1) NOT NULL DEFAULT 1,
+ comments SMALLINT(5) NOT NULL DEFAULT 0,
+ views MEDIUMINT(7) NOT NULL DEFAULT 0,
+ cumulativeLikes MEDIUMINT(7) NOT NULL DEFAULT 0,
+ hasEmbeddedObjects TINYINT(1) NOT NULL DEFAULT 0,
+
+ KEY (time)
+);
+
+DROP TABLE IF EXISTS wcf1_article_content;
+CREATE TABLE wcf1_article_content (
+ articleContentID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ articleID INT(10) NOT NULL,
+ languageID INT(10),
+ title VARCHAR(255) NOT NULL,
+ teaser TEXT,
+ content MEDIUMTEXT,
+ imageID INT(10),
+
+ UNIQUE KEY (articleID, languageID)
+);
+
DROP TABLE IF EXISTS wcf1_attachment;
CREATE TABLE wcf1_attachment (
attachmentID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
ALTER TABLE wcf1_application ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_article ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
+ALTER TABLE wcf1_article ADD FOREIGN KEY (categoryID) REFERENCES wcf1_category (categoryID) ON DELETE SET NULL;
+
+ALTER TABLE wcf1_article_content ADD FOREIGN KEY (articleID) REFERENCES wcf1_article (articleID) ON DELETE CASCADE;
+ALTER TABLE wcf1_article_content ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE SET NULL;
+ALTER TABLE wcf1_article_content ADD FOREIGN KEY (imageID) REFERENCES wcf1_media (mediaID) ON DELETE SET NULL;
+
ALTER TABLE wcf1_attachment ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
ALTER TABLE wcf1_attachment ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;