Added article system
authorMarcel Werk <burntime@woltlab.com>
Mon, 6 Jun 2016 15:02:45 +0000 (17:02 +0200)
committerMarcel Werk <burntime@woltlab.com>
Mon, 6 Jun 2016 15:02:57 +0000 (17:02 +0200)
66 files changed:
com.woltlab.wcf/aclOption.xml
com.woltlab.wcf/acpMenu.xml
com.woltlab.wcf/menuItem.xml
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/option.xml
com.woltlab.wcf/page.xml
com.woltlab.wcf/templates/article.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/articleList.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/articleListItems.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/categoryArticleList.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/header.tpl
com.woltlab.wcf/templates/membersList.tpl
com.woltlab.wcf/userGroupOption.xml
constants.php
wcfsetup/install/files/acp/templates/articleAdd.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/articleAddDialog.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/articleList.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/boxAdd.tpl
wcfsetup/install/files/acp/templates/pageList.tpl
wcfsetup/install/files/images/placeholderTiny.png [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Article/Add.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Like/Handler.js
wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/ArticleCategoryAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/ArticleCategoryEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/ArticleEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/ArticleCategoryListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/ArticleListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/AccessibleArticleList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/Article.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/ArticleAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/ArticleEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/ArticleList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/CategoryArticleList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/FeedArticle.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/FeedArticleList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/LikeableArticle.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/LikeableArticleProvider.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/ViewableArticle.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/category/ArticleCategory.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/content/ArticleContent.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/content/ArticleContentAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/content/ArticleContentEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/content/ArticleContentList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/content/ViewableArticleContent.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/content/ViewableArticleContentList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryNode.class.php
wcfsetup/install/files/lib/data/media/ViewableMedia.class.php
wcfsetup/install/files/lib/data/media/ViewableMediaList.class.php
wcfsetup/install/files/lib/data/page/Page.class.php
wcfsetup/install/files/lib/page/AbstractPage.class.php
wcfsetup/install/files/lib/page/ArticleFeedPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/ArticleListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/ArticlePage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/CategoryArticleListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/UsersOnlineListPage.class.php
wcfsetup/install/files/lib/system/cache/runtime/ViewableArticleRuntimeCache.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/manager/ArticleCommentManager.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/page/handler/ArticlePageHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/page/handler/CategoryArticleListPageHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php [new file with mode: 0644]
wcfsetup/install/files/style/ui/article.scss [new file with mode: 0644]
wcfsetup/setup/db/install.sql

index a608f243f677f25483e046526252b7bfb28b3e99..2091a61b8d15ee72c87b9824169b648aee46d0dc 100644 (file)
@@ -8,6 +8,10 @@
                        <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
index 94d96a8f89bed3821a5d6587ecdac9c0f75f8bca..71375715401357ea4d53b1c3cc414b6d482a2aa6 100644 (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">
index 2edf3055064389cbbed1afde21f5cabee83d42a8..379242d721fff5759a490a090bdf022dd223182d 100644 (file)
@@ -7,6 +7,12 @@
                        <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>
index a5203a3771e3b644533faa0b48098d7bb662f76c..d058b18728b3619678da99ba76cfc985ef0475f3 100644 (file)
                        <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 -->
index 75cc0ca064053612b81a9cf3548676d8e556e7d2..bb046a9e287344c9f69cdf0dc8ec904ac9c1cc9f 100644 (file)
                        <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>
@@ -1427,7 +1436,7 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                        </option>
                        <!-- /message.general.poll -->
                        
-                       <!-- cms.media.thumnail -->
+                       <!-- cms.media.thumbnail -->
                        <option name="media_small_thumbnail_width">
                                <categoryname>cms.media.thumbnail</categoryname>
                                <optiontype>integer</optiontype>
@@ -1489,7 +1498,51 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <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>
        
index de65cf06adf52f96fd6268f75362a92463d788bc..b727d692b8487321e55a371b1497fc7789524c8e 100644 (file)
                        <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">
diff --git a/com.woltlab.wcf/templates/article.tpl b/com.woltlab.wcf/templates/article.tpl
new file mode 100644 (file)
index 0000000..df85e5a
--- /dev/null
@@ -0,0 +1,276 @@
+{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}&plusmn;{/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'}
diff --git a/com.woltlab.wcf/templates/articleList.tpl b/com.woltlab.wcf/templates/articleList.tpl
new file mode 100644 (file)
index 0000000..a4313f5
--- /dev/null
@@ -0,0 +1,54 @@
+{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'}
diff --git a/com.woltlab.wcf/templates/articleListItems.tpl b/com.woltlab.wcf/templates/articleListItems.tpl
new file mode 100644 (file)
index 0000000..1791531
--- /dev/null
@@ -0,0 +1,46 @@
+<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}&plusmn;{/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
diff --git a/com.woltlab.wcf/templates/categoryArticleList.tpl b/com.woltlab.wcf/templates/categoryArticleList.tpl
new file mode 100644 (file)
index 0000000..a70db0d
--- /dev/null
@@ -0,0 +1,58 @@
+{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'}
index 0f4ab999aa22551f5f2efb5add259ef8e26d97fe..1acf6bf1daee1e299a7a586eb0a04c327c193f2f 100644 (file)
        
        {include file='headInclude'}
        
+       {if !$canonicalURL|empty}
+               <link rel="canonical" href="{$canonicalURL}">
+       {/if}
+       
        {if !$headContent|empty}
                {@$headContent}
        {/if}
index a4867ed2e37eec17a61fae876282c42f63f8df6e..8380bedae1e123384d14bdab4ddb8b246d44a1fe 100644 (file)
@@ -12,7 +12,7 @@
                <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}
index fdb84aa4a441c57eb07f2f9b15a3c496485e450a..292309df29f3a3f8c0ef13a4f5ffe5b2e15e4f7b 100644 (file)
@@ -32,6 +32,9 @@
                        <category name="user.pageComment">
                                <parent>user</parent>
                        </category>
+                       <category name="user.article">
+                               <parent>user</parent>
+                       </category>
                        
                        <category name="mod" />
                        <category name="mod.general">
@@ -43,6 +46,9 @@
                        <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>
@@ -719,6 +740,30 @@ png]]></defaultvalue>
                                <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>
@@ -787,6 +832,28 @@ png]]></defaultvalue>
                                <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>
index 923ecd7a3ebe69180dac547c56894e935ce9a2db..a43b2cbfdfbbd99d59e59c7491c0d0a98ff68d91 100644 (file)
@@ -211,3 +211,10 @@ define('MEDIA_MEDIUM_THUMBNAIL_RETAIN_DIMENSIONS', 1);
 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);
diff --git a/wcfsetup/install/files/acp/templates/articleAdd.tpl b/wcfsetup/install/files/acp/templates/articleAdd.tpl
new file mode 100644 (file)
index 0000000..6ceb45f
--- /dev/null
@@ -0,0 +1,380 @@
+{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}{@"&nbsp;&nbsp;&nbsp;&nbsp;"|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'}
diff --git a/wcfsetup/install/files/acp/templates/articleAddDialog.tpl b/wcfsetup/install/files/acp/templates/articleAddDialog.tpl
new file mode 100644 (file)
index 0000000..f8f8e61
--- /dev/null
@@ -0,0 +1,28 @@
+<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>
diff --git a/wcfsetup/install/files/acp/templates/articleList.tpl b/wcfsetup/install/files/acp/templates/articleList.tpl
new file mode 100644 (file)
index 0000000..9daf33c
--- /dev/null
@@ -0,0 +1,188 @@
+{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}{@"&nbsp;&nbsp;&nbsp;&nbsp;"|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'}
index 13e07bf18ebd03b6d6a48f0f2066c45202579a5b..cdb201fe0425c3877822d06b23fcafc08df05eb6 100644 (file)
                                        {/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'}
index 3f39c05e0d79bbf1d6a0e3f727775040e255c4f9..052231b7547c4ed3f8c8a15170512ae0e843e6b1 100644 (file)
                        </ul>
                </nav>
        </footer>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
 {/if}
 
 {include file='pageAddDialog'}
diff --git a/wcfsetup/install/files/images/placeholderTiny.png b/wcfsetup/install/files/images/placeholderTiny.png
new file mode 100644 (file)
index 0000000..5177e27
Binary files /dev/null and b/wcfsetup/install/files/images/placeholderTiny.png differ
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Article/Add.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Article/Add.js
new file mode 100644 (file)
index 0000000..85aa9e2
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * 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')
+                               }
+                       };
+               }
+       }
+});
index a3d4a401949a098310f08a68aca0624bac1574f0..695bbe5f30a34148e9923753c57c9bd0221a6355 100644 (file)
@@ -113,7 +113,8 @@ define(
                _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';
@@ -143,7 +144,8 @@ define(
                        }
                        
                        // 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 = '#';
@@ -164,9 +166,9 @@ define(
                                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.");
                                }
@@ -203,7 +205,7 @@ define(
                        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'));
                        
diff --git a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php
new file mode 100644 (file)
index 0000000..391ac96
--- /dev/null
@@ -0,0 +1,396 @@
+<?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
diff --git a/wcfsetup/install/files/lib/acp/form/ArticleCategoryAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleCategoryAddForm.class.php
new file mode 100644 (file)
index 0000000..1c58a64
--- /dev/null
@@ -0,0 +1,30 @@
+<?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'];
+}
diff --git a/wcfsetup/install/files/lib/acp/form/ArticleCategoryEditForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleCategoryEditForm.class.php
new file mode 100644 (file)
index 0000000..3e6f4e3
--- /dev/null
@@ -0,0 +1,30 @@
+<?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'];
+}
diff --git a/wcfsetup/install/files/lib/acp/form/ArticleEditForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleEditForm.class.php
new file mode 100644 (file)
index 0000000..437ad04
--- /dev/null
@@ -0,0 +1,170 @@
+<?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
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/ArticleCategoryListPage.class.php b/wcfsetup/install/files/lib/acp/page/ArticleCategoryListPage.class.php
new file mode 100644 (file)
index 0000000..15250bd
--- /dev/null
@@ -0,0 +1,30 @@
+<?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'];
+}
diff --git a/wcfsetup/install/files/lib/acp/page/ArticleListPage.class.php b/wcfsetup/install/files/lib/acp/page/ArticleListPage.class.php
new file mode 100644 (file)
index 0000000..20f8a43
--- /dev/null
@@ -0,0 +1,142 @@
+<?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()
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/AccessibleArticleList.class.php b/wcfsetup/install/files/lib/data/article/AccessibleArticleList.class.php
new file mode 100644 (file)
index 0000000..147ff13
--- /dev/null
@@ -0,0 +1,33 @@
+<?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]);
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/Article.class.php b/wcfsetup/install/files/lib/data/article/Article.class.php
new file mode 100644 (file)
index 0000000..1d0bfe3
--- /dev/null
@@ -0,0 +1,220 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php
new file mode 100644 (file)
index 0000000..9a849ff
--- /dev/null
@@ -0,0 +1,125 @@
+<?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()));
+                                       }
+                               }
+                       }
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/ArticleEditor.class.php b/wcfsetup/install/files/lib/data/article/ArticleEditor.class.php
new file mode 100644 (file)
index 0000000..7a2fd0f
--- /dev/null
@@ -0,0 +1,24 @@
+<?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;
+}
diff --git a/wcfsetup/install/files/lib/data/article/ArticleList.class.php b/wcfsetup/install/files/lib/data/article/ArticleList.class.php
new file mode 100644 (file)
index 0000000..1196cac
--- /dev/null
@@ -0,0 +1,26 @@
+<?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;
+}
diff --git a/wcfsetup/install/files/lib/data/article/CategoryArticleList.class.php b/wcfsetup/install/files/lib/data/article/CategoryArticleList.class.php
new file mode 100644 (file)
index 0000000..4df347d
--- /dev/null
@@ -0,0 +1,45 @@
+<?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]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/FeedArticle.class.php b/wcfsetup/install/files/lib/data/article/FeedArticle.class.php
new file mode 100644 (file)
index 0000000..fa90c72
--- /dev/null
@@ -0,0 +1,95 @@
+<?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();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/FeedArticleList.class.php b/wcfsetup/install/files/lib/data/article/FeedArticleList.class.php
new file mode 100644 (file)
index 0000000..57590d0
--- /dev/null
@@ -0,0 +1,38 @@
+<?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();
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/LikeableArticle.class.php b/wcfsetup/install/files/lib/data/article/LikeableArticle.class.php
new file mode 100644 (file)
index 0000000..e049144
--- /dev/null
@@ -0,0 +1,85 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/LikeableArticleProvider.class.php b/wcfsetup/install/files/lib/data/article/LikeableArticleProvider.class.php
new file mode 100644 (file)
index 0000000..c80b955
--- /dev/null
@@ -0,0 +1,85 @@
+<?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());
+                       }
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php b/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php
new file mode 100644 (file)
index 0000000..d55680e
--- /dev/null
@@ -0,0 +1,28 @@
+<?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]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/ViewableArticle.class.php b/wcfsetup/install/files/lib/data/article/ViewableArticle.class.php
new file mode 100644 (file)
index 0000000..c84f222
--- /dev/null
@@ -0,0 +1,82 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php b/wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php
new file mode 100644 (file)
index 0000000..3bbf272
--- /dev/null
@@ -0,0 +1,66 @@
+<?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);
+                       }
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/category/ArticleCategory.class.php b/wcfsetup/install/files/lib/data/article/category/ArticleCategory.class.php
new file mode 100644 (file)
index 0000000..78b1b47
--- /dev/null
@@ -0,0 +1,120 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/content/ArticleContent.class.php b/wcfsetup/install/files/lib/data/article/content/ArticleContent.class.php
new file mode 100644 (file)
index 0000000..126d88d
--- /dev/null
@@ -0,0 +1,115 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/content/ArticleContentAction.class.php b/wcfsetup/install/files/lib/data/article/content/ArticleContentAction.class.php
new file mode 100644 (file)
index 0000000..55789e8
--- /dev/null
@@ -0,0 +1,24 @@
+<?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;
+}
diff --git a/wcfsetup/install/files/lib/data/article/content/ArticleContentEditor.class.php b/wcfsetup/install/files/lib/data/article/content/ArticleContentEditor.class.php
new file mode 100644 (file)
index 0000000..27cba08
--- /dev/null
@@ -0,0 +1,24 @@
+<?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;
+}
diff --git a/wcfsetup/install/files/lib/data/article/content/ArticleContentList.class.php b/wcfsetup/install/files/lib/data/article/content/ArticleContentList.class.php
new file mode 100644 (file)
index 0000000..db52691
--- /dev/null
@@ -0,0 +1,26 @@
+<?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;
+}
diff --git a/wcfsetup/install/files/lib/data/article/content/ViewableArticleContent.class.php b/wcfsetup/install/files/lib/data/article/content/ViewableArticleContent.class.php
new file mode 100644 (file)
index 0000000..d4d0ff4
--- /dev/null
@@ -0,0 +1,70 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/article/content/ViewableArticleContentList.class.php b/wcfsetup/install/files/lib/data/article/content/ViewableArticleContentList.class.php
new file mode 100644 (file)
index 0000000..278e9db
--- /dev/null
@@ -0,0 +1,49 @@
+<?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]);
+                               }
+                       }
+               }
+       }
+}
index 9b0fd2afac09a153f4f7f0a6f1f45f7274315d88..c4c86d2a8b607d5debc6d5e28640cf05e25e296f 100644 (file)
@@ -90,6 +90,23 @@ class CategoryNode extends DatabaseObjectDecorator implements \RecursiveIterator
                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
         */
index c1337634cda5f713f5e268780377f840c5621060..0c7939bf90e14032e63fa6ea1895f8c90b61b1d8 100644 (file)
@@ -35,7 +35,7 @@ class ViewableMedia extends DatabaseObjectDecorator {
         */
        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>';
@@ -51,7 +51,7 @@ class ViewableMedia extends DatabaseObjectDecorator {
                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;" />';
                        }
                }
                
@@ -65,12 +65,12 @@ class ViewableMedia extends DatabaseObjectDecorator {
         * @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).'" ' : '').'/>';
        }
        
        /**
index 09f6c342546bae55fd1459a1fbc7824bcda89198..077fb04be26e4db7c040d4501806f002971f8f8b 100644 (file)
@@ -3,7 +3,7 @@ namespace wcf\data\media;
 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
index e30a33d1d551fb31fd73d0ca9d929c90985c92c0..8ed5c581e4113659e9c2855b1b05191d96f3e994 100644 (file)
@@ -200,7 +200,7 @@ class Page extends DatabaseObject implements ILinkableObject, ITitledObject {
         * @return      \wcf\system\page\handler\IMenuPageHandler|null
         */
        public function getHandler() {
-               if ($this->handler) {
+               if ($this->pageHandler === null && $this->handler) {
                        $this->pageHandler = new $this->handler();
                }
                
index 7ebd0f9419df4b4b737d90da77f69cf9bd445806..e09ff45eccd30f7a1733762d81261aac881bc69d 100644 (file)
@@ -128,7 +128,8 @@ abstract class AbstractPage implements IPage {
                // assign parameters
                WCF::getTPL()->assign([
                        'action' => $this->action,
-                       'templateName' => $this->templateName
+                       'templateName' => $this->templateName,
+                       'canonicalURL' => $this->canonicalURL
                ]);
        }
        
diff --git a/wcfsetup/install/files/lib/page/ArticleFeedPage.class.php b/wcfsetup/install/files/lib/page/ArticleFeedPage.class.php
new file mode 100644 (file)
index 0000000..a340a43
--- /dev/null
@@ -0,0 +1,70 @@
+<?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');
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/page/ArticleListPage.class.php b/wcfsetup/install/files/lib/page/ArticleListPage.class.php
new file mode 100644 (file)
index 0000000..329a93e
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 : ''));
+       }
+}
diff --git a/wcfsetup/install/files/lib/page/ArticlePage.class.php b/wcfsetup/install/files/lib/page/ArticlePage.class.php
new file mode 100644 (file)
index 0000000..797ca30
--- /dev/null
@@ -0,0 +1,277 @@
+<?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
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/page/CategoryArticleListPage.class.php b/wcfsetup/install/files/lib/page/CategoryArticleListPage.class.php
new file mode 100644 (file)
index 0000000..551820d
--- /dev/null
@@ -0,0 +1,95 @@
+<?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
index acf15d7034a77ef458aa2c4df77b5f13e5a755b7..5b07402d8b960aff56902e8328426931bb9f3024 100644 (file)
@@ -107,12 +107,10 @@ class UsersOnlineListPage extends SortablePage {
                
                // 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);
                        }
                }
                
diff --git a/wcfsetup/install/files/lib/system/cache/runtime/ViewableArticleRuntimeCache.class.php b/wcfsetup/install/files/lib/system/cache/runtime/ViewableArticleRuntimeCache.class.php
new file mode 100644 (file)
index 0000000..70813d7
--- /dev/null
@@ -0,0 +1,26 @@
+<?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;
+}
diff --git a/wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php b/wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php
new file mode 100644 (file)
index 0000000..ed21728
--- /dev/null
@@ -0,0 +1,55 @@
+<?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');
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/manager/ArticleCommentManager.class.php b/wcfsetup/install/files/lib/system/comment/manager/ArticleCommentManager.class.php
new file mode 100644 (file)
index 0000000..bc9f5a0
--- /dev/null
@@ -0,0 +1,98 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/page/handler/ArticlePageHandler.class.php b/wcfsetup/install/files/lib/system/page/handler/ArticlePageHandler.class.php
new file mode 100644 (file)
index 0000000..0aa7a3a
--- /dev/null
@@ -0,0 +1,76 @@
+<?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);
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/page/handler/CategoryArticleListPageHandler.class.php b/wcfsetup/install/files/lib/system/page/handler/CategoryArticleListPageHandler.class.php
new file mode 100644 (file)
index 0000000..032c98c
--- /dev/null
@@ -0,0 +1,71 @@
+<?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]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php b/wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php
new file mode 100644 (file)
index 0000000..407abe4
--- /dev/null
@@ -0,0 +1,31 @@
+<?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';
+       }
+}
diff --git a/wcfsetup/install/files/style/ui/article.scss b/wcfsetup/install/files/style/ui/article.scss
new file mode 100644 (file)
index 0000000..f74ac02
--- /dev/null
@@ -0,0 +1,271 @@
+.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
index e2d1802c3d0450777cbd39739bf21d2e3d0fbb65..2f02131c9cc9661c9d44e5bb012ecc9ca1ae7090 100644 (file)
@@ -136,6 +136,38 @@ CREATE TABLE wcf1_application (
        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,
@@ -1619,6 +1651,13 @@ ALTER TABLE wcf1_ad ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (
 
 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;