Merged com.woltlab.wcf.message into WCF
authorMarcel Werk <burntime@woltlab.com>
Mon, 20 May 2013 20:44:03 +0000 (22:44 +0200)
committerMarcel Werk <burntime@woltlab.com>
Mon, 20 May 2013 20:44:03 +0000 (22:44 +0200)
44 files changed:
com.woltlab.wcf/bbcode.xml
com.woltlab.wcf/objectTypeDefinition.xml
com.woltlab.wcf/option.xml
com.woltlab.wcf/template/__messageFormSmilies.tpl [new file with mode: 0644]
com.woltlab.wcf/template/__messageQuoteManager.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageFormAttachments.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageFormMultilingualism.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageFormPreviewButton.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageFormSettings.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageFormSmilies.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageFormTabs.tpl [new file with mode: 0644]
com.woltlab.wcf/template/messageQuoteList.tpl [new file with mode: 0644]
com.woltlab.wcf/template/rssFeed.tpl [new file with mode: 0644]
com.woltlab.wcf/template/shareButtons.tpl [new file with mode: 0644]
com.woltlab.wcf/template/wysiwyg.tpl [new file with mode: 0644]
com.woltlab.wcf/userGroupOption.xml
wcfsetup/install/files/icon/reddit.png [new file with mode: 0644]
wcfsetup/install/files/js/3rdParty/ckeditor/plugins/wbbcode/plugin.js [new file with mode: 0644]
wcfsetup/install/files/js/3rdParty/ckeditor/plugins/wbutton/plugin.js [new file with mode: 0644]
wcfsetup/install/files/js/WCF.Message.js [new file with mode: 0644]
wcfsetup/install/files/js/WCF.Message.min.js [new file with mode: 0644]
wcfsetup/install/files/lib/action/MessageQuoteAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IExtendedMessageQuickReplyAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IFeedEntry.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IMessage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IMessageInlineEditorAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IMessageQuickReplyAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/bbcode/MessagePreviewAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/smiley/category/SmileyCategoryAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/form/MessageForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/AbstractFeedPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/bbcode/AttachmentBBCode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/censorship/Censorship.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/util/MessageUtil.class.php [new file with mode: 0644]
wcfsetup/install/files/style/message.less [new file with mode: 0644]
wcfsetup/install/files/style/poll.less [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 503a40233019800351f7933f769e61643802298b..dd0079e4e6894513340508c2e879878c02b5c8fd 100644 (file)
                        <allowedchildren>none</allowedchildren>
                        <sourcecode>1</sourcecode>
                </bbcode>
+               
+               <bbcode name="attach">
+                       <classname>wcf\system\bbcode\AttachmentBBCode</classname>
+                       <attributes>
+                               <attribute name="0">
+                                       <validationpattern>^\d+$</validationpattern>
+                                       <required>1</required>
+                                       <usetext>1</usetext>
+                               </attribute>
+                               <attribute name="1">
+                                       <validationpattern>^(left|right)$</validationpattern>
+                               </attribute>
+                       </attributes>
+               </bbcode>
        </import>
 </data>
\ No newline at end of file
index 383800688c47a93a414e01cf5b590e562bcb88e4..5ab6bf6097d219f61d3b85ec14b9aefeb77c8369 100644 (file)
                <definition>
                        <name>com.woltlab.wcf.attachment.objectType</name>
                        <interfacename>wcf\system\attachment\IAttachmentObjectType</interfacename>
-               </definition>   
+               </definition>
+               
+               <definition>
+                       <name>com.woltlab.wcf.message.quote</name>
+                       <interfacename>wcf\system\message\IMessageQuoteHandler</interfacename>
+               </definition>
        </import>
-</data>
\ No newline at end of file
+</data>
index e69fab03f18e7ad243bfcb17d69709f96d046ea4..5740bf1fee8804abe26c2180ad4651612fe574d2 100644 (file)
                                <category name="security.censorship">
                                        <parent>security</parent>
                                </category>
+                                       <category name="message.censorship">
+                                               <parent>security.censorship</parent>
+                                       </category>
                        <!-- /security -->
                        
                        <!-- message -->
                                <category name="message.general">
                                        <parent>message</parent>
                                </category>
+                                       <category name="message.general.share">
+                                               <parent>message.general</parent>
+                                       </category>
                                
                                <category name="message.attachment">
                                        <parent>message</parent>
@@ -490,6 +496,60 @@ no:!cache_source_memcached_host]]></enableoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue><![CDATA[280]]></defaultvalue>
                        </option>
+                       
+                       <!-- message.general -->
+                       <option name="enable_bbcodes_default_value">
+                               <categoryname>message.general</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="enable_html_default_value">
+                               <categoryname>message.general</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                       </option>
+                       <option name="enable_smilies_default_value">
+                               <categoryname>message.general</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="pre_parse_default_value">
+                               <categoryname>message.general</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="show_signature_default_value">
+                               <categoryname>message.general</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <!-- /message.general -->
+                       
+                       <!-- message.general.share -->
+                       <option name="enable_share_buttons">
+                               <categoryname>message.general.share</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                               <enableoptions><![CDATA[share_buttons_show_count]]></enableoptions>
+                       </option>
+                       <option name="share_buttons_show_count">
+                               <categoryname>message.general.share</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <!-- /message.general.share -->
+                       
+                       <!-- message.censorship -->
+                       <option name="enable_censorship">
+                               <categoryname>message.censorship</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <enableoptions><![CDATA[censored_words]]></enableoptions>
+                       </option>
+                       <option name="censored_words">
+                               <categoryname>message.censorship</categoryname>
+                               <optiontype>textarea</optiontype>
+                       </option>
+                       <!-- /message.censorship -->
                </options>
        </import>
 </data>
diff --git a/com.woltlab.wcf/template/__messageFormSmilies.tpl b/com.woltlab.wcf/template/__messageFormSmilies.tpl
new file mode 100644 (file)
index 0000000..913276e
--- /dev/null
@@ -0,0 +1,5 @@
+<ul class="smileyList">
+       {foreach from=$smilies item=smiley}
+               <li><a title="{lang}{$smiley->smileyTitle}{/lang}" class="jsTooltip jsSmiley" data-smiley-code="{$smiley->smileyCode}"><img src="{$smiley->getURL()}" alt="{$smiley->smileyCode}" class="icon24" /></a></li>
+       {/foreach}
+</ul>
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/__messageQuoteManager.tpl b/com.woltlab.wcf/template/__messageQuoteManager.tpl
new file mode 100644 (file)
index 0000000..62059c5
--- /dev/null
@@ -0,0 +1,13 @@
+WCF.Language.addObject({
+       'wcf.message.quote.insertAllQuotes': '{lang}wcf.message.quote.insertAllQuotes{/lang}',
+       'wcf.message.quote.insertSelectedQuotes': '{lang}wcf.message.quote.insertSelectedQuotes{/lang}',
+       'wcf.message.quote.manageQuotes': '{lang}wcf.message.quote.manageQuotes{/lang}',
+       'wcf.message.quote.quoteSelected': '{lang}wcf.message.quote.quoteSelected{/lang}',
+       'wcf.message.quote.removeAllQuotes': '{lang}wcf.message.quote.removeAllQuotes{/lang}',
+       'wcf.message.quote.removeSelectedQuotes': '{lang}wcf.message.quote.removeSelectedQuotes{/lang}',
+       'wcf.message.quote.showQuotes': '{lang}wcf.message.quote.showQuotes{/lang}'
+});
+
+{if !$wysiwygSelector|isset}{assign var=wysiwygSelector value=''}{/if}
+{if !$supportPaste|isset}{assign var=supportPaste value=false}{/if}
+var $quoteManager = new WCF.Message.Quote.Manager({@$__quoteCount}, '{$wysiwygSelector|encodeJS}', {if $supportPaste}true{else}false{/if}, [ {implode from=$__quoteRemove item=quoteID}'{$quoteID}'{/implode} ]);
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/messageFormAttachments.tpl b/com.woltlab.wcf/template/messageFormAttachments.tpl
new file mode 100644 (file)
index 0000000..65a5508
--- /dev/null
@@ -0,0 +1,57 @@
+<div id="attachments" class="jsOnly formAttachmentContent tabMenuContent container containerPadding">
+       <ul class="formAttachmentList clearfix"{if !$attachmentHandler->getAttachmentList()|count} style="display: none"{/if}>
+               {foreach from=$attachmentHandler->getAttachmentList() item=$attachment}
+                       <li class="box48">
+                               {if $attachment->tinyThumbnailType}
+                                       <img src="{link controller='Attachment' object=$attachment}tiny=1{/link}" alt="" class="attachmentTinyThumbnail" />
+                               {else}
+                                       <span class="icon icon48 icon-paper-clip"></span>
+                               {/if}
+                               
+                               <div>
+                                       <div>
+                                               <p><a href="{link controller='Attachment' object=$attachment}{/link}"{if $attachment->isImage} title="{$attachment->filename}" class="jsImageViewer"{/if}>{$attachment->filename}</a></p>
+                                               <small>{@$attachment->filesize|filesize}</small>
+                                       </div>
+                                       
+                                       <ul>
+                                               <li><span class="icon icon16 icon-remove pointer jsTooltip jsDeleteButton " title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$attachment->attachmentID}" data-confirm-message="{lang}wcf.attachment.delete.sure{/lang}"></span></li>
+                                               <li><span class="icon icon16 icon-paste pointer jsTooltip jsButtonInsertAttachment" title="{lang}wcf.attachment.insert{/lang}" data-object-id="{@$attachment->attachmentID}" /></li>
+                                       </ul>
+                               </div>
+                       </li>
+               {/foreach}
+       </ul>
+       
+       <dl class="wide">
+               <dd>
+                       <div></div>
+                       <small>{lang}wcf.attachment.upload.limits{/lang}</small>
+               </dd>
+       </dl>
+       
+       {event name='fields'}
+</div>
+
+<script type="text/javascript" src="{@$__wcf->getPath()}js/WCF.Attachment{if !ENABLE_DEBUG_MODE}.min{/if}.js"></script>
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               WCF.Language.addObject({
+                       'wcf.attachment.upload.error.invalidExtension': '{lang}wcf.attachment.upload.error.invalidExtension{/lang}',
+                       'wcf.attachment.upload.error.tooLarge': '{lang}wcf.attachment.upload.error.tooLarge{/lang}',
+                       'wcf.attachment.upload.error.reachedLimit': '{lang}wcf.attachment.upload.error.reachedLimit{/lang}',
+                       'wcf.attachment.upload.error.reachedRemainingLimit': '{lang}wcf.attachment.upload.error.reachedRemainingLimit{/lang}',
+                       'wcf.attachment.upload.error.uploadFailed': '{lang}wcf.attachment.upload.error.uploadFailed{/lang}',
+                       'wcf.global.button.upload': '{lang}wcf.global.button.upload{/lang}',
+                       'wcf.attachment.insert': '{lang}wcf.attachment.insert{/lang}',
+                       'wcf.attachment.delete.sure': '{lang}wcf.attachment.delete.sure{/lang}'
+               });
+               
+               new WCF.Attachment.Upload($('#attachments > dl > dd > div'), $('#attachments > ul'), '{@$attachmentObjectType}', '{@$attachmentObjectID}', '{$tmpHash|encodeJS}', '{@$attachmentParentObjectID}', {@$attachmentHandler->getMaxCount()}, '{@$wysiwygContainerID}');
+               new WCF.Action.Delete('wcf\\data\\attachment\\AttachmentAction', '.formAttachmentList > li');
+       });
+       //]]>
+</script>
+
+<input type="hidden" name="tmpHash" value="{$tmpHash}" />
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/messageFormMultilingualism.tpl b/com.woltlab.wcf/template/messageFormMultilingualism.tpl
new file mode 100644 (file)
index 0000000..bcfa568
--- /dev/null
@@ -0,0 +1,31 @@
+{if $availableContentLanguages|count}
+       <dl{if $errorField == 'languageID'} class="formError"{/if}>
+               <dt>{lang}wcf.user.language{/lang}</dt>
+               <dd id="languageIDContainer">
+                       <noscript>
+                               <select name="languageID" id="languageID">
+                                       {foreach from=$availableContentLanguages item=contentLanguage}
+                                               <option value="{@$contentLanguage->languageID}">{$contentLanguage}</option>
+                                       {/foreach}
+                               </select>
+                       </noscript>
+               </dd>
+       </dl>
+       
+       <script type="text/javascript">
+               //<![CDATA[
+               $(function() {
+                       var $languages = {
+                               {implode from=$availableContentLanguages item=contentLanguage}
+                                       '{@$contentLanguage->languageID}': {
+                                               iconPath: '{@$contentLanguage->getIconPath()}',
+                                               languageName: '{$contentLanguage}'
+                                       }
+                               {/implode}
+                       };
+                       
+                       new WCF.Language.Chooser('languageIDContainer', 'languageID', {$languageID}, $languages);
+               });
+               //]]>
+       </script>
+{/if}
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/messageFormPreviewButton.tpl b/com.woltlab.wcf/template/messageFormPreviewButton.tpl
new file mode 100644 (file)
index 0000000..b175ced
--- /dev/null
@@ -0,0 +1,13 @@
+<button id="previewButton" class="jsOnly" accesskey="p">{lang}wcf.global.button.preview{/lang}</button>
+
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               WCF.Language.addObject({
+                       'wcf.global.preview': '{lang}wcf.global.preview{/lang}' 
+               });
+               
+               new WCF.Message.DefaultPreview({if MODULE_ATTACHMENT && $attachmentHandler !== null}'{@$attachmentObjectType}', '{@$attachmentObjectID}', '{$tmpHash|encodeJS}'{/if});
+       });
+       //]]>
+</script>
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/messageFormSettings.tpl b/com.woltlab.wcf/template/messageFormSettings.tpl
new file mode 100644 (file)
index 0000000..5c4f523
--- /dev/null
@@ -0,0 +1,36 @@
+<fieldset id="settings" class="settingsContent tabMenuContent container containerPadding">
+       <dl class="wide">
+               {if $__wcf->getSession()->getPermission('user.message.canUseBBCodes')}
+                       <dd>
+                               <label><input id="preParse" name="preParse" type="checkbox" value="1"{if $preParse} checked="checked"{/if} /> {lang}wcf.message.settings.preParse{/lang}</label>
+                               <small>{lang}wcf.message.settings.preParse.description{/lang}</small>
+                       </dd>
+               {/if}
+               {if $__wcf->getSession()->getPermission('user.message.canUseSmilies')}
+                       <dd>
+                               <label><input id="enableSmilies" name="enableSmilies" type="checkbox" value="1"{if $enableSmilies} checked="checked"{/if} /> {lang}wcf.message.settings.enableSmilies{/lang}</label>
+                               <small>{lang}wcf.message.settings.enableSmilies.description{/lang}</small>
+                       </dd>
+               {/if}
+               {if $__wcf->getSession()->getPermission('user.message.canUseBBCodes')}
+                       <dd>
+                               <label><input id="enableBBCodes" name="enableBBCodes" type="checkbox" value="1"{if $enableBBCodes} checked="checked"{/if} /> {lang}wcf.message.settings.enableBBCodes{/lang}</label>
+                               <small>{lang}wcf.message.settings.enableBBCodes.description{/lang}</small>
+                       </dd>
+               {/if}
+               {if $__wcf->getSession()->getPermission('user.message.canUseHtml')}
+                       <dd>
+                               <label><input id="enableHtml" name="enableHtml" type="checkbox" value="1"{if $enableHtml} checked="checked"{/if} /> {lang}wcf.message.settings.enableHtml{/lang}</label>
+                               <small>{lang}wcf.message.settings.enableHtml.description{/lang}</small>
+                       </dd>
+               {/if}
+               {if 'MODULE_USER_SIGNATURE'|defined && MODULE_USER_SIGNATURE && $showSignatureSetting && $__wcf->user->userID}
+                       <dd>
+                               <label><input id="showSignature" name="showSignature" type="checkbox" value="1"{if $showSignature} checked="checked"{/if} /> {lang}wcf.message.settings.showSignature{/lang}</label>
+                               <small>{lang}wcf.message.settings.showSignature.description{/lang}</small>
+                       </dd>
+               {/if}
+               
+               {event name='settings'}
+       </dl>
+</fieldset>
diff --git a/com.woltlab.wcf/template/messageFormSmilies.tpl b/com.woltlab.wcf/template/messageFormSmilies.tpl
new file mode 100644 (file)
index 0000000..d7c7e7c
--- /dev/null
@@ -0,0 +1,50 @@
+{assign var=__tabCount value=0}
+{capture assign=__categoryTabs}
+       {foreach from=$smileyCategories item=smileyCategory}
+               {assign var=__tabCount value=$__tabCount + 1}
+               {assign var='__smileyAnchor' value='smilies-'|concat:$smileyCategory->categoryID}
+               <li><a href="{$__wcf->getAnchor($__smileyAnchor)}" data-smiley-category-id="{@$smileyCategory->categoryID}">{$smileyCategory->title|language}</a></li>
+       {/foreach}
+{/capture}
+
+<div id="smilies" class="jsOnly smiliesContent tabMenuContent container containerPadding{if $__tabCount} tabMenuContainer{/if}">
+       {capture assign=__defaultSmilies}
+               {include file='__messageFormSmilies' smilies=$defaultSmilies}
+       {/capture}
+       
+       {if $__tabCount > 1}
+               <nav class="menu">
+                       <ul>
+                               {@$__categoryTabs}
+                       </ul>
+               </nav>
+               
+               {foreach from=$smileyCategories item=smileyCategory}
+                       {if !$smileyCategory->isDisabled}
+                               <div id="smilies-{@$smileyCategory->categoryID}" class="hidden">
+                                       {if !$smileyCategory->categoryID}{@$__defaultSmilies}{/if}
+                               </div>
+                       {/if}
+               {/foreach}
+               
+               <script type="text/javascript">
+                       //<![CDATA[
+                       $(function() {
+                               new WCF.Message.SmileyCategories();
+                       });
+                       //]]>
+               </script>
+       {else}
+               {@$__defaultSmilies}
+       {/if}
+       
+       {event name='fields'}
+       
+       <script type="text/javascript">
+               //<![CDATA[
+               $(function() {
+                       new WCF.Message.Smilies('{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}');
+               });
+               //]]>
+       </script>
+</div>
diff --git a/com.woltlab.wcf/template/messageFormTabs.tpl b/com.woltlab.wcf/template/messageFormTabs.tpl
new file mode 100644 (file)
index 0000000..81e0ffb
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="tabMenuContainer" data-active="{$activeTabMenuItem}" data-store="activeTabMenuItem">
+       <nav class="tabMenu jsOnly">
+               <ul>
+                       {if MODULE_SMILEY && $smileyCategories|count}<li id="smiliesTab"><a href="{@$__wcf->getAnchor('smilies')}" title="{lang}wcf.message.smilies{/lang}">{lang}wcf.message.smilies{/lang}</a></li>{/if}
+                       {if MODULE_ATTACHMENT && $attachmentHandler !== null && $attachmentHandler->canUpload()}<li id="attachmentsTab"><a href="{@$__wcf->getAnchor('attachments')}" title="{lang}wcf.attachment.attachments{/lang}">{lang}wcf.attachment.attachments{/lang}</a></li>{/if}
+                       <li><a href="{@$__wcf->getAnchor('settings')}" title="{lang}wcf.message.settings{/lang}">{lang}wcf.message.settings{/lang}</a></li>
+                       {event name='tabMenuTabs'}
+               </ul>
+       </nav>
+       
+       {if MODULE_SMILEY && $smileyCategories|count}{include file='messageFormSmilies'}{/if}
+       {if MODULE_ATTACHMENT && $attachmentHandler !== null && $attachmentHandler->canUpload()}{include file='messageFormAttachments'}{/if}
+       
+       {include file='messageFormSettings'}
+       
+       {event name='tabMenuContents'}
+</div>
+
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               if (jQuery.browser.mobile) $('#smiliesTab, #smilies').remove();
+               WCF.TabMenu.init();
+       });
+       //]]>
+</script>
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/messageQuoteList.tpl b/com.woltlab.wcf/template/messageQuoteList.tpl
new file mode 100644 (file)
index 0000000..8f1a113
--- /dev/null
@@ -0,0 +1,50 @@
+{if !$supportPaste|isset}{assign var=supportPaste value=false}{/if}
+{foreach from=$messages item=message}
+       <article class="message messageReduced marginTop jsInvalidQuoteTarget" data-link="{@$message->getLink()}" data-username="{$message->getUsername()}">
+               <div>
+                       <section class="messageContent">
+                               <div>
+                                       <header class="messageHeader">
+                                               <div class="box32">
+                                                       {if $userProfiles[$message->getUserID()]|isset}
+                                                               <a href="{link controller='User' object=$userProfiles[$message->getUserID()]}{/link}" class="framed">{@$userProfiles[$message->getUserID()]->getAvatar()->getImageTag(32)}</a>
+                                                       {/if}
+                                                       
+                                                       <div class="messageHeadline">
+                                                               <h1><a href="{@$message->getLink()}">{$message->getTitle()}</a></h1>
+                                                               <p>
+                                                                       <span class="username">{if $userProfiles[$message->getUserID()]|isset}<a href="{link controller='User' object=$userProfiles[$message->getUserID()]}{/link}">{$message->getUsername()}</a>{else}{$message->getUsername()}{/if}</span>
+                                                                       {@$message->getTime()|time}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       </header>
+                                       
+                                       <div class="messageBody">
+                                               <div>
+                                                       <div class="messageText">
+                                                               <ul>
+                                                                       {foreach from=$message key=quoteID item=quote}
+                                                                               <li data-quote-id="{@$quoteID}">
+                                                                                       <span>
+                                                                                               <input type="checkbox" value="1" id="quote_{@$quoteID}" class="jsCheckbox" />
+                                                                                               {if $supportPaste}<span class="icon icon16 icon-plus jsTooltip jsInsertQuote" title="{lang}wcf.message.quote.insertQuote{/lang}"></span>{/if}
+                                                                                       </span>
+                                                                                       
+                                                                                       <div class="jsQuote">
+                                                                                               <label for="quote_{@$quoteID}">{@$quote}</label>
+                                                                                       </div>
+                                                                                       <div class="jsFullQuote">
+                                                                                               {$message->getFullQuote($quoteID)}
+                                                                                       </div>
+                                                                               </li>
+                                                                       {/foreach}
+                                                               </ul>
+                                                       </div>
+                                               </div>
+                                       </div>
+                               </div>
+                       </section>
+               </div>
+       </article>
+{/foreach}
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/rssFeed.tpl b/com.woltlab.wcf/template/rssFeed.tpl
new file mode 100644 (file)
index 0000000..11242c0
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0"
+       xmlns:atom="http://www.w3.org/2005/Atom"
+       xmlns:content="http://purl.org/rss/1.0/modules/content/"
+       xmlns:dc="http://purl.org/dc/elements/1.1/"
+       xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
+>
+       <channel>
+               <title><![CDATA[{if $title}{$title} - {/if}{@PAGE_TITLE|language|escapeCDATA}]]></title>
+               <link><![CDATA[{@$baseHref|escapeCDATA}]]></link>
+               <description><![CDATA[{@PAGE_DESCRIPTION|escapeCDATA}]]></description>
+               <language>{@$__wcf->language->getFixedLanguageCode()}</language>
+               <pubDate>{'r'|gmdate:TIME_NOW}</pubDate>
+{assign var='dummy' value=$items->rewind()}
+               <lastBuildDate>{if $items->valid()}{'r'|gmdate:$items->current()->getTime()}{else}{'r'|gmdate:TIME_NOW}{/if}</lastBuildDate>
+               <ttl>60</ttl>
+               <generator><![CDATA[WoltLab Community Framework {@WCF_VERSION}]]></generator>
+               <atom:link href="{$__wcf->getRequestURI()}" rel="self" type="application/rss+xml" />
+{*             *}{foreach from=$items item='item'}
+               <item>
+                       <title><![CDATA[{@$item->getTitle()|escapeCDATA}]]></title>
+                       <link><![CDATA[{@$item->getLink()|escapeCDATA}]]></link>
+                       {hascontent}<description><![CDATA[{content}{@$item->getExcerpt()|escapeCDATA}{/content}]]></description>{/hascontent}
+                       <pubDate>{'r'|gmdate:$item->getTime()}</pubDate>
+                       <dc:creator>{@$item->getUsername()|escapeCDATA}</dc:creator>
+                       <guid><![CDATA[{@$item->getLink()|escapeCDATA}]]></guid>
+                       {foreach from=$item->getCategories() item='category'}
+                               <category><![CDATA[{@$category|escapeCDATA}]]></category>
+                       {/foreach}
+                       {hascontent}<content:encoded><![CDATA[{content}{@$item->getFormattedMessage()|escapeCDATA}{/content}]]></content:encoded>{/hascontent}
+                       <slash:comments>{@$item->getComments()|escapeCDATA}</slash:comments>
+               </item>
+{*             *}{/foreach}
+       </channel>
+{if ENABLE_BENCHMARK}
+       <!-- 
+               Execution time: {@$__wcf->getBenchmark()->getExecutionTime()}s ({#($__wcf->getBenchmark()->getExecutionTime()-$__wcf->getBenchmark()->getQueryExecutionTime())/$__wcf->getBenchmark()->getExecutionTime()*100}% PHP, {#$__wcf->getBenchmark()->getQueryExecutionTime()/$__wcf->getBenchmark()->getExecutionTime()*100}% SQL) | SQL queries: {#$__wcf->getBenchmark()->getQueryCount()} | Memory-Usage: {$__wcf->getBenchmark()->getMemoryUsage()}
+       
+{*     *}{if ENABLE_DEBUG_MODE}
+{*             *}{foreach from=$__wcf->getBenchmark()->getItems() item=item}
+{*     *}                      {if $item.type == 1}(SQL Query) {/if}{$item.text} ({@$item.use}s)
+{*             *}{/foreach}
+{*     *}{/if}
+       -->
+{/if}
+</rss>
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/shareButtons.tpl b/com.woltlab.wcf/template/shareButtons.tpl
new file mode 100644 (file)
index 0000000..83514dc
--- /dev/null
@@ -0,0 +1,49 @@
+<div class="messageShareButtons jsOnly">
+       <ul>
+               <li class="jsShareFacebook">
+                       <a>
+                               <span class="icon icon32 icon-facebook-sign jsTooltip" title="{lang}wcf.message.share.facebook{/lang}"></span>
+                               <span class="invisible">{lang}wcf.message.share.facebook{/lang}</span>
+                       </a>
+                       <span class="badge" style="display: none">0</span>
+               </li>
+               <li class="jsShareTwitter">
+                       <a>
+                               <span class="icon icon32 icon-twitter-sign jsTooltip" title="{lang}wcf.message.share.twitter{/lang}"></span>
+                               <span class="invisible">{lang}wcf.message.share.twitter{/lang}</span>
+                       </a>
+                       <span class="badge" style="display: none">0</span>
+               </li>
+               <li class="jsShareGoogle">
+                       <a>
+                               <span class="icon icon32 icon-google-plus-sign jsTooltip" title="{lang}wcf.message.share.google{/lang}"></span>
+                               <span class="invisible">{lang}wcf.message.share.google{/lang}</span>
+                       </a>
+                       <span class="badge" style="display: none">0</span>
+               </li>
+               <li class="jsShareReddit">
+                       <a>
+                               <img class="jsTooltip" src="{$__wcf->getPath()}icon/reddit.png" alt="{lang}wcf.message.share.reddit{/lang}" title="{lang}wcf.message.share.reddit{/lang}" />
+                               <span class="invisible">{lang}wcf.message.share.reddit{/lang}</span>
+                       </a>
+                       <span class="badge" style="display: none">0</span>
+               </li>
+               
+               {event name='buttons'}
+       </ul>
+       
+       <script type="text/javascript">
+               //<![CDATA[
+               $(function() {
+                       WCF.Language.addObject({
+                               'wcf.message.share.facebook': '{lang}wcf.message.share.facebook{/lang}',
+                               'wcf.message.share.google': '{lang}wcf.message.share.google{/lang}',
+                               'wcf.message.share.reddit': '{lang}wcf.message.share.reddit{/lang}',
+                               'wcf.message.share.twitter': '{lang}wcf.message.share.twitter{/lang}'
+                       });
+                       
+                       new WCF.Message.Share.Page({if SHARE_BUTTONS_SHOW_COUNT}true{else}false{/if});
+               });
+               //]]>
+       </script>
+</div>
diff --git a/com.woltlab.wcf/template/wysiwyg.tpl b/com.woltlab.wcf/template/wysiwyg.tpl
new file mode 100644 (file)
index 0000000..e7a768b
--- /dev/null
@@ -0,0 +1,65 @@
+<script type="text/javascript">
+//<![CDATA[
+       var CKEDITOR_BASEPATH = '{@$__wcf->getPath()}js/3rdParty/ckeditor/';
+       var __CKEDITOR_BUTTONS = [ {implode from=$__wcf->getBBCodeHandler()->getButtonBBCodes() item=__bbcode}{ icon: '{$__bbcode->wysiwygIcon}', label: '{$__bbcode->buttonLabel|language}', name: '{$__bbcode->bbcodeTag}' }{/implode} ];
+//]]>
+</script>
+<script type="text/javascript" src="{@$__wcf->getPath()}js/3rdParty/ckeditor/ckeditor.js"></script>
+<script type="text/javascript" src="{@$__wcf->getPath()}js/3rdParty/ckeditor/adapters/jquery.js"></script>
+{event name='javascriptIncludes'}
+
+<script type="text/javascript">
+//<![CDATA[
+$(function() {
+       if ($.browser.mobile) {
+               return;
+       }
+       
+       var __CKEDITOR_TOOLBAR = [
+               ['Source', '-', 'Undo', 'Redo'],
+               ['Bold', 'Italic', 'Underline', '-', 'Strike', 'Subscript','Superscript'],
+               ['NumberedList', 'BulletedList', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
+               '/',
+               ['Font', 'FontSize', 'TextColor'],
+               ['Link', 'Unlink', 'Image', 'Table', 'Smiley'],
+               ['Maximize']
+       ];
+       if (__CKEDITOR_BUTTONS.length) {
+               var $buttons = [ ];
+               
+               for (var $i = 0, $length = __CKEDITOR_BUTTONS.length; $i < $length; $i++) {
+                       $buttons.push('__wcf_' + __CKEDITOR_BUTTONS[$i].name);
+               }
+               
+               __CKEDITOR_TOOLBAR.push($buttons);
+       }
+       
+       var $config = {
+               smiley_path: '{@$__wcf->getPath()|encodeJS}',
+               extraPlugins: 'wbbcode,wbutton',
+               removePlugins: 'contextmenu,tabletools,liststyle,elementspath,menubutton,forms,scayt',
+               language: '{@$__wcf->language->getFixedLanguageCode()}',
+               fontSize_sizes: '8/8pt;10/10pt;12/12pt;14/14pt;18/18pt;24/24pt;36/36pt;',
+               disableObjectResizing: true,
+               disableNativeSpellChecker: false,
+               toolbarCanCollapse: false,
+               enterMode: CKEDITOR.ENTER_BR,
+               minHeight: 200,
+               toolbar: __CKEDITOR_TOOLBAR,
+               smiley_images: [
+                       {implode from=$defaultSmilies item=smiley}'{@$smiley->smileyPath|encodeJS}'{/implode}
+               ],
+               smiley_descriptions: [
+                       {implode from=$defaultSmilies item=smiley}'{@$smiley->smileyCode|encodeJS}'{/implode}
+               ]
+       };
+       
+       {event name='javascriptInit'}
+       
+       var $editor = CKEDITOR.instances['{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}'];
+       if ($editor) $editor.destroy(true);
+       
+       $('{if $wysiwygSelector|isset}#{$wysiwygSelector|encodeJS}{else}#text{/if}').ckeditor($config);
+});
+//]]>
+</script>
index a9bc1f2dd884e4fa9bb86b38e380574f1c72ad93..37d4267105e59d4c698d9218438aa4fab30c966c 100644 (file)
@@ -285,6 +285,30 @@ pdf]]></defaultvalue>
                                <defaultvalue>0</defaultvalue>
                                <admindefaultvalue>1</admindefaultvalue>
                        </option>
+                       
+                       <!-- user.message -->
+                       <option name="user.message.canUseSmilies">
+                               <categoryname>user.message</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="user.message.canUseHtml">
+                               <categoryname>user.message</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                       </option>
+                       <option name="user.message.canUseBBCodes">
+                               <categoryname>user.message</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="user.message.allowedBBCodes">
+                               <categoryname>user.message</categoryname>
+                               <optiontype>BBCodeSelect</optiontype>
+                               <defaultvalue>all</defaultvalue>
+                       </option>
+                       <!-- /user.message -->
                </options>
        </import>
 </data>
diff --git a/wcfsetup/install/files/icon/reddit.png b/wcfsetup/install/files/icon/reddit.png
new file mode 100644 (file)
index 0000000..e1bcd73
Binary files /dev/null and b/wcfsetup/install/files/icon/reddit.png differ
diff --git a/wcfsetup/install/files/js/3rdParty/ckeditor/plugins/wbbcode/plugin.js b/wcfsetup/install/files/js/3rdParty/ckeditor/plugins/wbbcode/plugin.js
new file mode 100644 (file)
index 0000000..df3b71b
--- /dev/null
@@ -0,0 +1,410 @@
+/*
+ * BBCode Plugin v1.0 for CKEditor - http://www.site-top.com/
+ * Copyright (c) 2010, PitBult.
+ * - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
+ */
+
+(function() {
+       var $pasted = false;
+       var $insertedText = null;
+       
+       CKEDITOR.on('instanceReady', function(event) {
+               /**
+                * Fixes issues with pasted html.
+                */
+               event.editor.on('paste', function(ev) {
+                       if (ev.data.type == 'html') {
+                               var $value = ev.data.dataValue;
+                               
+                               // Convert <br> to line breaks.
+                               $value = $value.replace(/<br><\/p>/gi,"\n\n");
+                               $value = $value.replace(/<br>/gi, "\n");
+                               $value = $value.replace(/<\/p>/gi,"\n\n");
+                               $value = $value.replace(/&nbsp;/gi," ");
+                               
+                               // remove html tags
+                               $value = $value.replace(/<[^>]+>/g, '');
+
+                               // fix multiple new lines
+                               $value = $value.replace(/\n{3,}/gi,"\n\n");
+                               
+                               ev.data.dataValue = $value;
+                               
+                               $pasted = true;
+                       }
+               }, null, null, 9);
+               
+               event.editor.on('insertText', function(ev) {
+                       $insertedText = ev.data;
+               }, null, null, 1);
+               event.editor.on('mode', function(ev) {
+                       ev.editor.focus();
+                       
+                       insertFakeSubmitButton(ev);
+               });
+               event.editor.on('afterSetData', function(ev) {
+                       insertFakeSubmitButton(ev);
+               });
+               
+               event.editor.on('key', function(ev) {
+                       if (ev.data.keyCode == CKEDITOR.ALT + 83) { // [Alt] + [S]
+                               WCF.Message.Submit.execute(ev.editor.name);
+                       }
+               });
+               
+               insertFakeSubmitButton(event);
+       });
+       
+       /**
+        * Inserts a fake submit button, Chrome only.
+        * 
+        * @param       object          event
+        */
+       function insertFakeSubmitButton(event) {
+               if (event.editor.mode === 'source' || !WCF.Browser.isChrome()) {
+                       return;
+               }
+               
+               // place button outside of <body> to prevent it being removed once deleting content
+               $('<button accesskey="s" />').hide().appendTo($(event.editor.document.$).find('html'));
+               
+       }
+       
+       /**
+        * Removes obsolete dialog elements.
+        */
+       CKEDITOR.on('dialogDefinition', function(event) {
+               var $tab;
+               var $name = event.data.name;
+               var $definition = event.data.definition;
+
+               if ($name == 'link') {
+                       $definition.removeContents('target');
+                       $definition.removeContents('upload');
+                       $definition.removeContents('advanced');
+                       $tab = $definition.getContents('info');
+                       $tab.remove('emailSubject');
+                       $tab.remove('emailBody');
+               }
+               else if ($name == 'image') {
+                       $definition.removeContents('advanced');
+                       $tab = $definition.getContents('Link');
+                       $tab.remove('cmbTarget');
+                       $tab = $definition.getContents('info');
+                       $tab.remove('txtAlt');
+                       $tab.remove('basic');
+               }
+               else if ($name == 'table') {
+                       $definition.removeContents('advanced');
+                       $definition.width = 210;
+                       $definition.height = 1;
+                       
+                       $tab = $definition.getContents('info');
+                       
+                       $tab.remove('selHeaders');
+                       $tab.remove('cmbAlign');
+                       $tab.remove('txtHeight');
+                       $tab.remove('txtCaption');
+                       $tab.remove('txtSummary');
+                       
+                       // don't remove these fields as we need their default values
+                       $tab.get('txtBorder').style = 'display: none';
+                       $tab.get('txtWidth').style = 'display: none';
+                       $tab.get('txtCellSpace').style = 'display: none';
+                       $tab.get('txtCellPad').style = 'display: none';
+               }
+               else if ($name == 'smiley') {
+                       $definition.contents[0].elements[0].onClick = function(ev) {
+                               var $target = ev.data.getTarget();
+                               var $targetName = $target.getName();
+                               
+                               if ($targetName == 'a') {
+                                       $target = $target.getChild( 0 );
+                               }
+                               else if ($targetName != 'img') {
+                                       return;
+                               }
+                               
+                               var $src = $target.getAttribute('cke_src');
+                               var $title = $target.getAttribute('title');
+                               
+                               event.editor.insertText(' ' + $title + ' ');
+                               
+                               $definition.dialog.hide();
+                               ev.data.preventDefault();
+                       };
+               }
+       });
+       
+       /**
+        * Enables this plugin.
+        */
+       CKEDITOR.plugins.add('wbbcode', {
+               requires: ['htmlwriter'],
+               init: function(editor) {
+                       editor.dataProcessor = new CKEDITOR.htmlDataProcessor(editor);
+                       editor.dataProcessor.toHtml = toHtml;
+                       editor.dataProcessor.toDataFormat = toDataFormat;
+               }
+       });
+       
+       /**
+        * Removes the unicode zero width space (0x200B).
+        * 
+        * @param       string          string
+        * @return      string
+        */
+       var removeCrap = function(string) {
+               var $string = '';
+               
+               for (var $i = 0, $length = string.length; $i < $length; $i++) {
+                       var $byte = string.charCodeAt($i).toString(16);
+                       if ($byte != '200b') {
+                               $string += string[$i];
+                       }
+               }
+               
+               return $string;
+       }
+
+       /**
+        * Converts bbcodes to html.
+        */
+       var toHtml = function(data, fixForBody) {
+               // remove 0x200B (unicode zero width space)
+               data = removeCrap(data);
+               
+               if ($insertedText !== null) {
+                       data = $insertedText;
+                       $insertedText = null;
+                       
+                       if (data == ' ') return '&nbsp;';
+               }
+               
+               if (!$pasted) {
+                       // Convert & to its HTML entity.
+                       data = data.replace(/&/g, '&amp;');
+                       
+                       // Convert < and > to their HTML entities.
+                       data = data.replace(/</g, '&lt;');
+                       data = data.replace(/>/g, '&gt;');
+               }
+               
+               // Convert line breaks to <br>.
+               data = data.replace(/(?:\r\n|\n|\r)/g, '<br>');
+               
+               if ($pasted) {
+                       $pasted = false;
+                       // skip
+                       return data;
+               }
+               
+               // cache code tags
+               var $cachedCodes = { };
+               data = data.replace(/\[code(.+?)\[\/code]/gi, function(match) {
+                       var $key = match.hashCode();
+                       $cachedCodes[$key] = match;
+                       return '@@' + $key + '@@';
+               });
+               
+               // [url]
+               data = data.replace(/\[url\]([^"]+?)\[\/url]/gi, '<a href="$1">$1</a>');
+               data = data.replace(/\[url\='?([^'"\]]+)'?](.+?)\[\/url]/gi, '<a href="$1">$2</a>');
+               
+               // [email]
+               data = data.replace(/\[email\]([^"]+?)\[\/email]/gi, '<a href="mailto:$1">$1</a>');
+               data = data.replace(/\[email\=([^"\]]+)](.+?)\[\/email]/gi, '<a href="mailto:$1">$2</a>');
+               
+               // [b]
+               data = data.replace(/\[b\](.*?)\[\/b]/gi, '<b>$1</b>');
+               
+               // [i]
+               data = data.replace(/\[i\](.*?)\[\/i]/gi, '<i>$1</i>');
+               
+               // [u]
+               data = data.replace(/\[u\](.*?)\[\/u]/gi, '<u>$1</u>');
+               
+               // [s]
+               data = data.replace(/\[s\](.*?)\[\/s]/gi, '<strike>$1</strike>');
+               
+               // [sub]
+               data = data.replace(/\[sub\](.*?)\[\/sub]/gi, '<sub>$1</sub>');
+               
+               // [sup]
+               data = data.replace(/\[sup\](.*?)\[\/sup]/gi, '<sup>$1</sup>');
+                       
+               // [img]
+               data = data.replace(/\[img\]([^"]+?)\[\/img\]/gi,'<img src="$1" />');
+               data = data.replace(/\[img='?([^"]*?)'?,(left|right)\]\[\/img\]/gi,'<img src="$1" style="float: $2" />');
+               data = data.replace(/\[img='?([^"]*?)'?\]\[\/img\]/gi,'<img src="$1" />');
+               
+               // [quote]
+               // data = data.replace(/\[quote\]/gi, '<blockquote>');
+               // data = data.replace(/\[\/quote\]/gi, '</blockquote>');
+               
+               // [size]
+               data = data.replace(/\[size=(\d+)\](.*?)\[\/size\]/gi,'<span style="font-size: $1pt">$2</span>');
+               
+               // [color]
+               data = data.replace(/\[color=([#a-z0-9]*?)\](.*?)\[\/color\]/gi,'<span style="color: $1">$2</span>');
+               
+               // [font]
+               data = data.replace(/\[font='?([a-z,\- ]*?)'?\](.*?)\[\/font\]/gi,'<span style="font-family: $1">$2</span>');
+               
+               // [align]
+               data = data.replace(/\[align=(left|right|center|justify)\](.*?)\[\/align\]/gi,'<div style="text-align: $1">$2</div>');
+               
+               // [*]
+               data = data.replace(/\[\*\](.*?)(?=\[\*\]|\[\/list\])/gi,'<li>$1</li>');
+               
+               // [list]
+               data = data.replace(/\[list\]/gi, '<ul>');
+               data = data.replace(/\[list=1\]/gi, '<ul style="list-style-type: decimal">');
+               data = data.replace(/\[\/list]/gi, '</ul>');
+               
+               // [table]
+               data = data.replace(/\[table\]/gi, '<table border="1" cellspacing="1" cellpadding="1" style="width: 500px;">');
+               data = data.replace(/\[\/table\]/gi, '</table>');
+               // [tr]
+               data = data.replace(/\[tr\]/gi, '<tr>');
+               data = data.replace(/\[\/tr\]/gi, '</tr>');
+               // [td]
+               data = data.replace(/\[td\]/gi, '<td>');
+               data = data.replace(/\[\/td\]/gi, '</td>');
+               
+               // smileys
+               for (var i = 0; i < this.editor.config.smiley_descriptions.length; i++) {
+                       var smileyCode = this.editor.config.smiley_descriptions[i].replace(/</g, '&lt;').replace(/>/g, '&gt;');
+                       var regExp = new RegExp('(\\s|>|^)'+WCF.String.escapeRegExp(smileyCode)+'(?=\\s|<|$)', 'gi');
+                       data = data.replace(regExp, '$1<img src="' + this.editor.config.smiley_path + this.editor.config.smiley_images[i] + '" class="smiley" alt="'+smileyCode+'" />');
+               }
+               
+               // remove "javascript:"
+               data = data.replace(/(javascript):/gi, '$1<span></span>:');
+               
+               // insert codes
+               if ($.getLength($cachedCodes)) {
+                       for (var $key in $cachedCodes) {
+                               var $regex = new RegExp('@@' + $key + '@@', 'g');
+                               data = data.replace($regex, $cachedCodes[$key]);
+                       }
+               }
+               
+               return data;
+       };
+       
+       /**
+        * Converts html to bbcodes.
+        */
+       var toDataFormat = function(html, fixForBody) {
+               if (html == '<br>' || html == '<p><br></p>') {
+                       return "";
+               }
+               
+               // Convert <br> to line breaks.
+               html = html.replace(/<br><\/p>/gi,"\n");
+               html = html.replace(/<br(?=[ \/>]).*?>/gi, '\r\n');
+               html = html.replace(/<p>/gi,"");
+               html = html.replace(/<\/p>/gi,"\n");
+               html = html.replace(/&nbsp;/gi," ");
+               
+               // [email]
+               html = html.replace(/<a .*?href=(["'])mailto:(.+?)\1.*?>([\s\S]+?)<\/a>/gi, '[email=$2]$3[/email]');
+               
+               // [url]
+               html = html.replace(/<a .*?href=(["'])(.+?)\1.*?>([\s\S]+?)<\/a>/gi, '[url=\'$2\']$3[/url]');
+               
+               // [b]
+               html = html.replace(/<(?:b|strong)>/gi, '[b]');
+               html = html.replace(/<\/(?:b|strong)>/gi, '[/b]');
+               
+               // [i]
+               html = html.replace(/<(?:i|em)>/gi, '[i]');
+               html = html.replace(/<\/(?:i|em)>/gi, '[/i]');
+               
+               // [u]
+               html = html.replace(/<u>/gi, '[u]');
+               html = html.replace(/<\/u>/gi, '[/u]');
+               
+               // [s]
+               html = html.replace(/<strike>/gi, '[s]');
+               html = html.replace(/<\/strike>/gi, '[/s]');
+               
+               // [sub
+               html = html.replace(/<sub>/gi, '[sub]');
+               html = html.replace(/<\/sub>/gi, '[/sub]');
+               
+               // [sup]
+               html = html.replace(/<sup>/gi, '[sup]');
+               html = html.replace(/<\/sup>/gi, '[/sup]');
+               
+               // smileys
+               html = html.replace(/<img [^>]*?alt="([^"]+?)" class="smiley".*?>/gi, '$1'); // firefox
+               html = html.replace(/<img [^>]*?class="smiley" alt="([^"]+?)".*?>/gi, '$1'); // chrome, ie
+               
+               // [img]
+               html = html.replace(/<img [^>]*?src=(["'])([^"']+?)\1 style="float: (left|right)".*?>/gi, "[img='$2',$3][/img]");
+               html = html.replace(/<img [^>]*?src=(["'])([^"']+?)\1.*?>/gi, '[img]$2[/img]');
+               
+               // [quote]
+               // html = html.replace(/<blockquote>/gi, '[quote]');
+               // html = html.replace(/\n*<\/blockquote>/gi, '[/quote]');
+               
+               // [color]
+               html = html.replace(/<span style="color: ?rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\);?">([\s\S]*?)<\/span>/gi, function(match, r, g, b, text) {
+                       var $hex = ("0123456789ABCDEF".charAt((r - r % 16) / 16) + '' + "0123456789ABCDEF".charAt(r % 16)) + '' + ("0123456789ABCDEF".charAt((g - g % 16) / 16) + '' + "0123456789ABCDEF".charAt(g % 16)) + '' + ("0123456789ABCDEF".charAt((b - b % 16) / 16) + '' + "0123456789ABCDEF".charAt(b % 16));
+                       
+                       return "[color=#" + $hex + "]" + text + "[/color]";
+               });
+               html = html.replace(/<span style="color: ?(.*?);?">([\s\S]*?)<\/span>/gi, "[color=$1]$2[/color]");
+               
+               // [size]
+               html = html.replace(/<span style="font-size: ?(\d+)pt;?">([\s\S]*?)<\/span>/gi, "[size=$1]$2[/size]");
+               
+               // [font]
+               html = html.replace(/<span style="font-family: ?(.*?);?">([\s\S]*?)<\/span>/gi, "[font='$1']$2[/font]");
+               
+               // [align]
+               html = html.replace(/<div style="text-align: ?(left|center|right|justify);? ?">([\s\S]*?)<\/div>/gi, "[align=$1]$2[/align]");
+               
+               // [*]
+               html = html.replace(/<li>/gi, '[*]');
+               html = html.replace(/<\/li>/gi, '');
+               
+               // [list]
+               html = html.replace(/<ul>/gi, '[list]');
+               html = html.replace(/<(ol|ul style="list-style-type: decimal")>/gi, '[list=1]');
+               html = html.replace(/<\/(ul|ol)>/gi, '[/list]');
+               
+               // [table]
+               html = html.replace(/<table[^>]*>/gi, '[table]');
+               html = html.replace(/<\/table>/gi, '[/table]');
+               
+               // remove empty <tr>s
+               html = html.replace(/<tr><\/tr>/gi, '');
+               // [tr]
+               html = html.replace(/<tr>/gi, '[tr]');
+               html = html.replace(/<\/tr>/gi, '[/tr]');
+               
+               // [td]
+               html = html.replace(/<td>/gi, '[td]');
+               html = html.replace(/<\/td>/gi, '[/td]');
+               
+               // Remove remaining tags.
+               html = html.replace(/<[^>]+>/g, '');
+               
+               // Restore <, > and &
+               html = html.replace(/&lt;/g, '<');
+               html = html.replace(/&gt;/g, '>');
+               html = html.replace(/&amp;/g, '&');
+               
+               // Restore (and )
+               html = html.replace(/%28/g, '(');
+               html = html.replace(/%29/g, ')');
+               
+               // Restore %20
+               html = html.replace(/%20/g, ' ');
+               
+               return html;
+       }
+})();
diff --git a/wcfsetup/install/files/js/3rdParty/ckeditor/plugins/wbutton/plugin.js b/wcfsetup/install/files/js/3rdParty/ckeditor/plugins/wbutton/plugin.js
new file mode 100644 (file)
index 0000000..c26f152
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * Provides custom buttons for CKEditor.
+ * 
+ * In short we're applying a style element on the current selection which will be replaced
+ * with the plain BBCode tag (e.g. [tt]) afterwards. Using insertText() or insertHtml() does
+ * not work here as it discards the inline styles set for the selection.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+(function() {
+       /**
+        * Transforms the BBCode span-element into a plain BBCode.
+        * 
+        * @param       CKEDITOR        editor
+        */
+       function transformBBCode(editor) {
+               $(editor.document.$).find('span.wcfBBCode').replaceWith(function() {
+                       var $bbcode = $(this).data('bbcode');
+                       return '[' + $bbcode + ']' + $(this).html() + '[/' + $bbcode + ']';
+               });
+       }
+       
+       // listens for 'afterCommandExec' to transform BBCodes into plain text
+       CKEDITOR.on('instanceReady', function(event) {
+               event.editor.on('afterCommandExec', function(ev) {
+                       if (ev.data.name.indexOf('__wcf_') == 0) {
+                               transformBBCode(ev.editor);
+                       }
+               });
+       });
+       
+       /**
+        * Enables this plugin.
+        */
+       CKEDITOR.plugins.add('wbutton', {
+               /**
+                * list of required plugins
+                * @var array<string>
+                */
+               requires: [ 'button' ],
+               
+               /**
+                * Initializes the 'wbutton' plugin.
+                * 
+                * @param       CKEDITOR        editor
+                */
+               init: function(editor) {
+                       if (!__CKEDITOR_BUTTONS.length) {
+                               return;
+                       }
+                       
+                       for (var $i = 0, $length = __CKEDITOR_BUTTONS.length; $i < $length; $i++) {
+                               this._wcfAddButton(editor, __CKEDITOR_BUTTONS[$i]);
+                       }
+               },
+               
+               /**
+                * Adds command and button for given BBCode.
+                * 
+                * @param       CKEDITOR        editor
+                * @param       object          button
+                */
+               _wcfAddButton: function(editor, button) {
+                       var $style = new CKEDITOR.style({
+                               element: 'span',
+                               attributes: {
+                                       'class': 'wcfBBCode',
+                                       'data-bbcode': button.name
+                               }
+                       });
+                       editor.addCommand('__wcf_' + button.name, new CKEDITOR.styleCommand($style));
+                       editor.ui.addButton('__wcf_' + button.name, {
+                               command: '__wcf_' + button.name,
+                               icon: button.icon,
+                               label: button.label
+                       });
+               }
+       });
+})();
diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js
new file mode 100644 (file)
index 0000000..29bd943
--- /dev/null
@@ -0,0 +1,2686 @@
+/**
+ * Message related classes for WCF
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+WCF.Message = { };
+
+/**
+ * Namespace for BBCode related classes.
+ */
+WCF.Message.BBCode = { };
+
+/**
+ * BBCode Viewer for WCF.
+ */
+WCF.Message.BBCode.CodeViewer = Class.extend({
+       /**
+        * dialog overlay
+        * @var jQuery
+        */
+       _dialog: null,
+       
+       /**
+        * Initializes the WCF.Message.BBCode.CodeViewer class.
+        */
+       init: function() {
+               this._dialog = null;
+               
+               this._initCodeBoxes();
+               
+               WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.BBCode.CodeViewer', $.proxy(this._initCodeBoxes, this));
+               WCF.DOMNodeInsertedHandler.forceExecution();
+       },
+       
+       /**
+        * Initializes available code boxes.
+        */
+       _initCodeBoxes: function() {
+               $('.codeBox:not(.jsCodeViewer)').each($.proxy(function(index, codeBox) {
+                       var $codeBox = $(codeBox).addClass('jsCodeViewer');
+                       
+                       $('<span class="icon icon16 icon-copy pointer jsTooltip" title="' + WCF.Language.get('wcf.message.bbcode.code.copy') + '" />').appendTo($codeBox.find('div > h3')).click($.proxy(this._click, this));
+               }, this));
+       },
+       
+       /**
+        * Shows a code viewer for a specific code box.
+        * 
+        * @param       object          event
+        */
+       _click: function(event) {
+               var $content = '';
+               $(event.currentTarget).parents('div').next('ol').children('li').each(function(index, listItem) {
+                       if ($content) {
+                               $content += "\n";
+                       }
+                       
+                       // do *not* use $.trim here, as we want to preserve whitespaces whitespaces
+                       $content += $(listItem).text().replace(/\n+$/, '');
+               });
+               
+               
+               if (this._dialog === null) {
+                       this._dialog = $('<div><textarea cols="60" rows="12" readonly="readonly" /></div>').hide().appendTo(document.body);
+                       this._dialog.children('textarea').val($content);
+                       this._dialog.wcfDialog({
+                               title: WCF.Language.get('wcf.message.bbcode.code.copy')
+                       });
+               }
+               else {
+                       this._dialog.children('textarea').val($content);
+                       this._dialog.wcfDialog('open');
+               }
+               
+               this._dialog.children('textarea').select();
+       }
+});
+
+/**
+ * Prevents multiple submits of the same form by disabling the submit button.
+ */
+WCF.Message.FormGuard = Class.extend({
+       /**
+        * Initializes the WCF.Message.FormGuard class.
+        */
+       init: function() {
+               var $forms = $('form.jsFormGuard').removeClass('jsFormGuard').submit(function() {
+                       $(this).find('.formSubmit input[type=submit]').disable();
+               });
+               
+               // restore buttons, prevents disabled buttons on back navigation in Opera
+               $(window).unload(function() {
+                       $forms.find('.formSubmit input[type=submit]').enable();
+               });
+       }
+});
+
+/**
+ * Provides previews for ckEditor message fields.
+ * 
+ * @param      string          className
+ * @param      string          messageFieldID
+ * @param      string          previewButtonID
+ */
+WCF.Message.Preview = Class.extend({
+       /**
+        * class name
+        * @var string
+        */
+       _className: '',
+       
+       /**
+        * message field id
+        * @var string
+        */
+       _messageFieldID: '',
+       
+       /**
+        * message field
+        * @var jQuery
+        */
+       _messageField: null,
+       
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * preview button
+        * @var jQuery
+        */
+       _previewButton: null,
+       
+       /**
+        * previous button label
+        * @var string
+        */
+       _previewButtonLabel: '',
+       
+       /**
+        * Initializes a new WCF.Message.Preview object.
+        * 
+        * @param       string          className
+        * @param       string          messageFieldID
+        * @param       string          previewButtonID
+        */
+       init: function(className, messageFieldID, previewButtonID) {
+               this._className = className;
+               
+               // validate message field
+               this._messageFieldID = $.wcfEscapeID(messageFieldID);
+               this._messageField = $('#' + this._messageFieldID);
+               if (!this._messageField.length) {
+                       console.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID + "'");
+                       return;
+               }
+               
+               // validate preview button
+               previewButtonID = $.wcfEscapeID(previewButtonID);
+               this._previewButton = $('#' + previewButtonID);
+               if (!this._previewButton.length) {
+                       console.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID + "'");
+                       return;
+               }
+               
+               this._previewButton.click($.proxy(this._click, this));
+               this._proxy = new WCF.Action.Proxy({
+                       success: $.proxy(this._success, this)
+               });
+       },
+       
+       /**
+        * Reads message field input and triggers an AJAX request.
+        */
+       _click: function(event) {
+               var $message = this._getMessage();
+               if ($message === null) {
+                       console.debug("[WCF.Message.Preview] Unable to access ckEditor instance of '" + this._messageFieldID + "'");
+                       return;
+               }
+               
+               this._proxy.setOption('data', {
+                       actionName: 'getMessagePreview',
+                       className: this._className,
+                       parameters: this._getParameters($message)
+               });
+               this._proxy.sendRequest();
+               
+               // update button label
+               this._previewButtonLabel = this._previewButton.html();
+               this._previewButton.html(WCF.Language.get('wcf.global.loading')).disable();
+               
+               // poke event
+               event.stopPropagation();
+               return false;
+       },
+       
+       /**
+        * Returns request parameters.
+        * 
+        * @param       string          message
+        * @return      object
+        */
+       _getParameters: function(message) {
+               // collect message form options
+               var $options = { };
+               $('#settings').find('input[type=checkbox]').each(function(index, checkbox) {
+                       var $checkbox = $(checkbox);
+                       if ($checkbox.is(':checked')) {
+                               $options[$checkbox.prop('name')] = $checkbox.prop('value');
+                       }
+               });
+               
+               // build parameters
+               return {
+                       data: {
+                               message: message
+                       },
+                       options: $options
+               };
+       },
+       
+       /**
+        * Returns parsed message from ckEditor or null if editor was not accessible.
+        * 
+        * @return      string
+        */
+       _getMessage: function() {
+               if ($.browser.mobile) {
+                       return this._messageField.val();
+               }
+               else if (this._messageField.data('ckeditorInstance')) {
+                       var $ckEditor = this._messageField.ckeditorGet();
+                       return $ckEditor.getData();
+               }
+               
+               return null;
+       },
+       
+       /**
+        * Handles successful AJAX requests.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               // restore preview button
+               this._previewButton.html(this._previewButtonLabel).enable();
+               
+               // evaluate message
+               this._handleResponse(data);
+       },
+       
+       /**
+        * Evaluates response data.
+        * 
+        * @param       object          data
+        */
+       _handleResponse: function(data) { }
+});
+
+/**
+ * Default implementation for message previews.
+ * 
+ * @see        WCF.Message.Preview
+ */
+WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
+       _attachmentObjectType: null,
+       _attachmentObjectID: null,
+       _tmpHash: null,
+       
+       /**
+        * @see WCF.Message.Preview.init()
+        */
+       init: function(attachmentObjectType, attachmentObjectID, tmpHash) {
+               this._super('wcf\\data\\bbcode\\MessagePreviewAction', 'text', 'previewButton');
+               
+               this._attachmentObjectType = attachmentObjectType || null;
+               this._attachmentObjectID = attachmentObjectID || null;
+               this._tmpHash = tmpHash || null;
+       },
+       
+       /**
+        * @see WCF.Message.Preview._handleResponse()
+        */
+       _handleResponse: function(data) {
+               var $preview = $('#previewContainer');
+               if (!$preview.length) {
+                       $preview = $('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>' + WCF.Language.get('wcf.global.preview') + '</legend><div></div></fieldset>').prependTo($('#messageContainer')).wcfFadeIn();
+               }
+               
+               $preview.find('div:eq(0)').html(data.returnValues.message);
+       },
+       
+       /**
+        * @see WCF.Message.Preview._getParameters()
+        */
+       _getParameters: function(message) {
+               var $parameters = this._super(message);
+               
+               if (this._attachmentObjectType != null) {
+                       $parameters.attachmentObjectType = this._attachmentObjectType;
+                       $parameters.attachmentObjectID = this._attachmentObjectID;
+                       $parameters.tmpHash = this._tmpHash;
+               }
+               
+               return $parameters;
+       }
+});
+
+/**
+ * Handles multilingualism for messages.
+ * 
+ * @param      integer         languageID
+ * @param      object          availableLanguages
+ * @param      boolean         forceSelection
+ */
+WCF.Message.Multilingualism = Class.extend({
+       /**
+        * list of available languages
+        * @var object
+        */
+       _availableLanguages: { },
+       
+       /**
+        * language id
+        * @var integer
+        */
+       _languageID: 0,
+       
+       /**
+        * language input element
+        * @var jQuery
+        */
+       _languageInput: null,
+       
+       /**
+        * Initializes WCF.Message.Multilingualism
+        * 
+        * @param       integer         languageID
+        * @param       object          availableLanguages
+        * @param       boolean         forceSelection
+        */
+       init: function(languageID, availableLanguages, forceSelection) {
+               this._availableLanguages = availableLanguages;
+               this._languageID = languageID || 0;
+               
+               this._languageInput = $('#languageID');
+               
+               // preselect current language id
+               this._updateLabel();
+               
+               // register event listener
+               this._languageInput.find('.dropdownMenu > li').click($.proxy(this._click, this));
+               
+               // add element to disable multilingualism
+               if (!forceSelection) {
+                       var $dropdownMenu = this._languageInput.find('.dropdownMenu');
+                       $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
+                       $('<li><span><span class="badge">' + this._availableLanguages[0] + '</span></span></li>').click($.proxy(this._disable, this)).appendTo($dropdownMenu);
+               }
+               
+               // bind submit event
+               this._languageInput.parents('form').submit($.proxy(this._submit, this));
+       },
+       
+       /**
+        * Handles language selections.
+        * 
+        * @param       object          event
+        */
+       _click: function(event) {
+               this._languageID = $(event.currentTarget).data('languageID');
+               this._updateLabel();
+       },
+       
+       /**
+        * Disables language selection.
+        */
+       _disable: function() {
+               this._languageID = 0;
+               this._updateLabel();
+       },
+       
+       /**
+        * Updates selected language.
+        */
+       _updateLabel: function() {
+               this._languageInput.find('.dropdownToggle > span').text(this._availableLanguages[this._languageID]);
+       },
+       
+       /**
+        * Sets language id upon submit.
+        */
+       _submit: function() {
+               this._languageInput.next('input[name=languageID]').prop('value', this._languageID);
+       }
+});
+
+/**
+ * Loads smiley categories upon user request.
+ */
+WCF.Message.SmileyCategories = Class.extend({
+       /**
+        * list of already loaded category ids
+        * @var array<integer>
+        */
+       _cache: [ ],
+       
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * ckEditor element
+        * @var jQuery
+        */
+       _ckEditor: null,
+       
+       /**
+        * Initializes the smiley loader.
+        * 
+        * @param       string          ckEditorID
+        */
+       init: function() {
+               this._cache = [ ];
+               this._proxy = new WCF.Action.Proxy({
+                       success: $.proxy(this._success, this)
+               });
+               
+               $('#smilies').on('wcftabsbeforeactivate', $.proxy(this._click, this));
+               
+               // handle onload
+               var self = this;
+               new WCF.PeriodicalExecuter(function(pe) {
+                       pe.stop();
+                       
+                       self._click({ }, { newTab: $('#smilies > .menu li.ui-state-active') });
+               }, 100);
+       },
+       
+       /**
+        * Handles tab menu clicks.
+        * 
+        * @param       object          event
+        * @param       object          ui
+        */
+       _click: function(event, ui) {
+               var $categoryID = parseInt($(ui.newTab).children('a').data('smileyCategoryID'));
+               
+               if ($categoryID && !WCF.inArray($categoryID, this._cache)) {
+                       this._proxy.setOption('data', {
+                               actionName: 'getSmilies',
+                               className: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
+                               objectIDs: [ $categoryID ]
+                       });
+                       this._proxy.sendRequest();
+               }
+       },
+       
+       /**
+        * Handles successful AJAX requests.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               var $categoryID = parseInt(data.returnValues.smileyCategoryID);
+               this._cache.push($categoryID);
+               
+               $('#smilies-' + $categoryID).html(data.returnValues.template);
+       }
+});
+
+/**
+ * Handles smiley clicks.
+ */
+WCF.Message.Smilies = Class.extend({
+       /**
+        * ckEditor element
+        * @var jQuery
+        */
+       _ckEditor: null,
+       
+       /**
+        * Initializes the smiley handler.
+        * 
+        * @param       string          ckEditorID
+        */
+       init: function(ckEditorID) {
+               // get ck editor
+               if (ckEditorID) {
+                       this._ckEditor = $('#' + ckEditorID);
+                       
+                       // add smiley click handler
+                       $(document).on('click', '.jsSmiley', $.proxy(this._smileyClick, this));
+               }
+       },
+       
+       /**
+        * Handles tab smiley clicks.
+        * 
+        * @param       object          event
+        */
+       _smileyClick: function(event) {
+               var $target = $(event.currentTarget);
+               var $smileyCode = $target.data('smileyCode');
+               
+               // get ckEditor
+               var $ckEditor = this._ckEditor.ckeditorGet();
+               // get smiley path
+               var $smileyPath = $target.find('img').attr('src');
+               
+               // add smiley to config
+               if (!WCF.inArray($smileyCode, $ckEditor.config.smiley_descriptions)) {
+                       $ckEditor.config.smiley_descriptions.push($smileyCode);
+                       $ckEditor.config.smiley_images.push($smileyPath);
+               }
+               
+               if ($ckEditor.mode === 'wysiwyg') {
+                       // in design mode
+                       var $img = $ckEditor.document.createElement('img', {
+                               attributes: {
+                                       src: $smileyPath,
+                                       'class': 'smiley',
+                                       alt: $smileyCode
+                               }
+                       });
+                       $ckEditor.insertText(' ');
+                       $ckEditor.insertElement($img);
+                       $ckEditor.insertText(' ');
+               }
+               else {
+                       // in source mode
+                       var $textarea = this._ckEditor.next('.cke_editor_text').find('textarea');
+                       var $value = $textarea.val();
+                       if ($value.length == 0) {
+                               $textarea.val($smileyCode);
+                               $textarea.setCaret($smileyCode.length);
+                       }
+                       else {
+                               var $position = $textarea.getCaret();
+                               var $string = (($value.substr($position - 1, 1) !== ' ') ? ' ' : '') + $smileyCode + ' ';
+                               $textarea.val( $value.substr(0, $position) + $string + $value.substr($position) );
+                               $textarea.setCaret($position + $string.length);
+                       }
+               }
+       }
+});
+
+/**
+ * Provides an AJAX-based quick reply for messages.
+ */
+WCF.Message.QuickReply = Class.extend({
+       /**
+        * quick reply container
+        * @var jQuery
+        */
+       _container: null,
+       
+       /**
+        * message field
+        * @var jQuery
+        */
+       _messageField: null,
+       
+       /**
+        * notification object
+        * @var WCF.System.Notification
+        */
+       _notification: null,
+       
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * quote manager object
+        * @var WCF.Message.Quote.Manager
+        */
+       _quoteManager: null,
+       
+       /**
+        * scroll handler
+        * @var WCF.Effect.Scroll
+        */
+       _scrollHandler: null,
+       
+       /**
+        * success message for created but invisible messages
+        * @var string
+        */
+       _successMessageNonVisible: '',
+       
+       /**
+        * Initializes a new WCF.Message.QuickReply object.
+        * 
+        * @param       boolean                         supportExtendedForm
+        * @param       WCF.Message.Quote.Manager       quoteManager
+        */
+       init: function(supportExtendedForm, quoteManager) {
+               this._container = $('#messageQuickReply');
+               this._messageField = $('#text');
+               if (!this._container || !this._messageField) {
+                       return;
+               }
+               
+               // button actions
+               var $formSubmit = this._container.find('.formSubmit');
+               $formSubmit.find('button[data-type=save]').click($.proxy(this._save, this));
+               if (supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
+               $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
+               
+               if (quoteManager) this._quoteManager = quoteManager;
+               
+               $('.jsQuickReply').data('__api', this).click($.proxy(this.click, this));
+               
+               this._proxy = new WCF.Action.Proxy({
+                       failure: $.proxy(this._failure, this),
+                       showLoadingOverlay: false,
+                       success: $.proxy(this._success, this)
+               });
+               this._scroll = new WCF.Effect.Scroll();
+               this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.add'));
+               this._successMessageNonVisible = '';
+       },
+       
+       /**
+        * Handles clicks on reply button.
+        * 
+        * @param       object          event
+        */
+       click: function(event) {
+               this._container.toggle();
+               
+               if (this._container.is(':visible')) {
+                       this._scroll.scrollTo(this._container, true);
+                       
+                       WCF.Message.Submit.registerButton('text', this._container.find('.formSubmit button[data-type=save]'));
+                       
+                       if (this._quoteManager) {
+                               // check if message field is empty
+                               var $empty = true;
+                               if ($.browser.touch) {
+                                       $empty = (!this._messageField.val().length);
+                               }
+                               else {
+                                       $empty = (!this._messageField.ckeditorGet().getData().length);
+                               }
+                               
+                               if ($empty) {
+                                       this._quoteManager.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes, this));
+                               }
+                       }
+                       
+                       new WCF.PeriodicalExecuter($.proxy(function(pe) {
+                               pe.stop();
+                               
+                               if ($.browser.mobile) {
+                                       this._messageField.focus();
+                               }
+                               else {
+                                       this._messageField.ckeditorGet().ui.editor.focus();
+                               }
+                       }, this), 250);
+               }
+               
+               // discard event
+               if (event !== null) {
+                       event.stopPropagation();
+                       return false;
+               }
+       },
+       
+       /**
+        * Returns container element.
+        *
+        * @return      jQuery
+        */
+       getContainer: function() {
+               return this._container;
+       },
+       
+       /**
+        * Insertes quotes into the quick reply editor.
+        * 
+        * @param       object          data
+        */
+       _insertQuotes: function(data) {
+               if (!data.returnValues.template) {
+                       return;
+               }
+               
+               if ($.browser.mobile) {
+                       this._messageField.val(data.returnValues.template);
+               }
+               else {
+                       this._messageField.ckeditorGet().insertText(data.returnValues.template);
+               }
+       },
+       
+       /**
+        * Saves message.
+        */
+       _save: function() {
+               var $message = '';
+               
+               if ($.browser.mobile) {
+                       $message = $.trim(this._messageField.val());
+               }
+               else {
+                       var $ckEditor = this._messageField.ckeditorGet();
+                       $message = $.trim($ckEditor.getData());
+               }
+               
+               // check if message is empty
+               var $innerError = this._messageField.parent().find('small.innerError');
+               if ($message === '') {
+                       if (!$innerError.length) {
+                               $innerError = $('<small class="innerError" />').appendTo(this._messageField.parent());
+                       }
+                       
+                       $innerError.html(WCF.Language.get('wcf.global.form.error.empty'));
+                       return;
+               }
+               else {
+                       $innerError.remove();
+               }
+               
+               this._proxy.setOption('data', {
+                       actionName: 'quickReply',
+                       className: this._getClassName(),
+                       interfaceName: 'wcf\\data\\IMessageQuickReplyAction',
+                       parameters: this._getParameters($message)
+               });
+               this._proxy.sendRequest();
+               
+               // show spinner and hide CKEditor
+               var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
+               $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
+               $messageBody.children('#cke_text').hide().end().next().hide();
+       },
+       
+       /**
+        * Returns the parameters for the save request.
+        * 
+        * @param       string          message
+        * @return      object
+        */
+       _getParameters: function(message) {
+               var $parameters = {
+                       objectID: this._getObjectID(),
+                       data: {
+                               message: message
+                       },
+                       lastPostTime: this._container.data('lastPostTime'),
+                       pageNo: this._container.data('pageNo'),
+                       removeQuoteIDs: (this._quoteManager === null ? [ ] : this._quoteManager.getQuotesMarkedForRemoval())
+               };
+               if (this._container.data('anchor')) {
+                       $parameters.anchor = this._container.data('anchor');
+               }
+               
+               return $parameters;
+       },
+       
+       /**
+        * Cancels quick reply.
+        */
+       _cancel: function() {
+               this._revertQuickReply(true);
+               
+               if ($.browser.mobile) {
+                       this._messageField.val('');
+               }
+               else {
+                       // revert CKEditor
+                       this._messageField.ckeditorGet().setData('');
+               }
+       },
+       
+       /**
+        * Reverts quick reply to original state and optionally hiding it.
+        * 
+        * @param       boolean         hide
+        */
+       _revertQuickReply: function(hide) {
+               var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
+               
+               if (hide) {
+                       this._container.hide();
+                       
+                       // remove previous error messages
+                       $messageBody.children('small.innerError').remove();
+               }
+               
+               // display CKEditor
+               $messageBody.children('.icon-spinner').remove();
+               $messageBody.children('#cke_text').show();
+               
+               // display form submit
+               $messageBody.next().show();
+       },
+       
+       /**
+        * Prepares jump to extended message add form.
+        */
+       _prepareExtended: function() {
+               // mark quotes for removal
+               if (this._quoteManager !== null) {
+                       this._quoteManager.markQuotesForRemoval();
+               }
+               
+               var $message = '';
+               
+               if ($.browser.mobile) {
+                       $message = this._messageField.val();
+               }
+               else {
+                       var $ckEditor = this._messageField.ckeditorGet();
+                       $message = $ckEditor.getData();
+               }
+               
+               new WCF.Action.Proxy({
+                       autoSend: true,
+                       data: {
+                               actionName: 'jumpToExtended',
+                               className: this._getClassName(),
+                               interfaceName: 'wcf\\data\\IExtendedMessageQuickReplyAction',
+                               parameters: {
+                                       containerID: this._getObjectID(),
+                                       message: $message
+                               }
+                       },
+                       success: function(data, textStatus, jqXHR) {
+                               window.location = data.returnValues.url;
+                       }
+               });
+       },
+       
+       /**
+        * Handles successful AJAX calls.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               // redirect to new page
+               if (data.returnValues.url) {
+                       window.location = data.returnValues.url;
+               }
+               else {
+                       if (data.returnValues.template) {
+                               // insert HTML
+                               var $message = $('' + data.returnValues.template);
+                               $message.insertBefore(this._container);
+                               
+                               // update last post time
+                               this._container.data('lastPostTime', data.returnValues.lastPostTime);
+                               
+                               // show notification
+                               this._notification.show(undefined, undefined, WCF.Language.get('wcf.global.success.add'));
+                               
+                               this._updateHistory($message.wcfIdentify());
+                       }
+                       else {
+                               // show notification
+                               var $message = (this._successMessageNonVisible) ? this._successMessageNonVisible : 'wcf.global.success.add';
+                               this._notification.show(undefined, 5000, WCF.Language.get($message));
+                       }
+                       
+                       if ($.browser.mobile) {
+                               this._messageField.val('');
+                       }
+                       else {
+                               // remove CKEditor contents
+                               this._messageField.ckeditorGet().setData('');
+                       }
+                       
+                       // hide quick reply and revert it
+                       this._revertQuickReply(true);
+                       
+                       // count stored quotes
+                       if (this._quoteManager !== null) {
+                               this._quoteManager.countQuotes();
+                       }
+               }
+       },
+       
+       /**
+        * Reverts quick reply on failure to preserve entered message.
+        */
+       _failure: function(data) {
+               this._revertQuickReply(false);
+               
+               if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+                       return true;
+               }
+               
+               var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
+               var $innerError = $messageBody.children('small.innerError').empty();
+               if (!$innerError.length) {
+                       $innerError = $('<small class="innerError" />').appendTo($messageBody);
+               }
+               
+               $innerError.html(data.returnValues.errorType);
+               
+               return false;
+       },
+       
+       /**
+        * Returns action class name.
+        * 
+        * @return      string
+        */
+       _getClassName: function() {
+               return '';
+       },
+       
+       /**
+        * Returns object id.
+        * 
+        * @return      integer
+        */
+       _getObjectID: function() {
+               return 0;
+       },
+       
+       /**
+        * Updates the history to avoid old content when going back in the browser
+        * history.
+        * 
+        * @param       hash
+        */
+       _updateHistory: function(hash) {
+               window.location.hash = hash;
+       }
+});
+
+/**
+ * Provides an inline message editor.
+ * 
+ * @param      integer         containerID
+ */
+WCF.Message.InlineEditor = Class.extend({
+       /**
+        * currently active message
+        * @var string
+        */
+       _activeElementID: '',
+       
+       /**
+        * message cache
+        * @var string
+        */
+       _cache: '',
+       
+       /**
+        * list of messages
+        * @var object
+        */
+       _container: { },
+       
+       /**
+        * container id
+        * @var integer
+        */
+       _containerID: 0,
+       
+       /**
+        * list of dropdowns
+        * @var object
+        */
+       _dropdowns: { },
+       
+       /**
+        * CSS selector for the message container
+        * @var string
+        */
+       _messageContainerSelector: '.jsMessage',
+       
+       /**
+        * prefix of the message editor CSS id
+        * @var string
+        */
+       _messageEditorIDPrefix: 'messageEditor',
+       
+       /**
+        * notification object
+        * @var WCF.System.Notification
+        */
+       _notification: null,
+       
+       /**
+        * proxy object
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * support for extended editing form
+        * @var boolean
+        */
+       _supportExtendedForm: false,
+       
+       /**
+        * Initializes a new WCF.Message.InlineEditor object.
+        * 
+        * @param       integer         containerID
+        * @param       boolean         supportExtendedForm
+        */
+       init: function(containerID, supportExtendedForm) {
+               this._activeElementID = '';
+               this._cache = '';
+               this._container = { };
+               this._containerID = parseInt(containerID);
+               this._dropdowns = { };
+               this._supportExtendedForm = (supportExtendedForm) ? true : false;
+               this._proxy = new WCF.Action.Proxy({
+                       failure: $.proxy(this._failure, this),
+                       showLoadingOverlay: false,
+                       success: $.proxy(this._success, this)
+               });
+               this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit'));
+               
+               this.initContainers();
+               
+               WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.InlineEditor', $.proxy(this.initContainers, this));
+       },
+       
+       /**
+        * Initializes editing capability for all messages.
+        */
+       initContainers: function() {
+               $(this._messageContainerSelector).each($.proxy(function(index, container) {
+                       var $container = $(container);
+                       var $containerID = $container.wcfIdentify();
+                       
+                       if (!this._container[$containerID]) {
+                               this._container[$containerID] = $container;
+                               
+                               if ($container.data('canEditInline')) {
+                                       $container.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID).click($.proxy(this._clickInline, this)).dblclick($.proxy(this._click, this));
+                               }
+                               else if ($container.data('canEdit')) {
+                                       $container.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID).click($.proxy(this._click, this));
+                               }
+                       }
+               }, this));
+       },
+       
+       /**
+        * Loads WYSIWYG editor for selected message.
+        * 
+        * @param       object          event
+        * @param       integer         containerID
+        * @return      boolean
+        */
+       _click: function(event, containerID) {
+               var $containerID = (event === null) ? containerID : $(event.currentTarget).data('containerID');
+               
+               if (this._activeElementID === '') {
+                       this._activeElementID = $containerID;
+                       this._prepare();
+                       
+                       this._proxy.setOption('data', {
+                               actionName: 'beginEdit',
+                               className: this._getClassName(),
+                               interfaceName: 'wcf\\data\\IMessageInlineEditorAction',
+                               parameters: {
+                                       containerID: this._containerID,
+                                       objectID: this._container[$containerID].data('objectID')
+                               }
+                       });
+                       this._proxy.sendRequest();
+               }
+               else {
+                       var $notification = new WCF.System.Notification(WCF.Language.get('wcf.message.error.editorAlreadyInUse'), 'warning');
+                       $notification.show();
+               }
+               
+               if (event !== null) {
+                       event.stopPropagation();
+                       return false;
+               }
+       },
+       
+       /**
+        * Provides an inline dropdown menu instead of directly loading the WYSIWYG editor.
+        * 
+        * @param       object          event
+        * @return      boolean
+        */
+       _clickInline: function(event) {
+               var $button = $(event.currentTarget);
+               
+               if (!$button.hasClass('dropdownToggle')) {
+                       var $containerID = $button.data('containerID');
+                       
+                       WCF.DOMNodeInsertedHandler.enable();
+                       
+                       $button.addClass('dropdownToggle').parent().addClass('dropdown');
+                       
+                       var $dropdownMenu = $('<ul class="dropdownMenu" />').insertAfter($button);
+                       this._initDropdownMenu($containerID, $dropdownMenu);
+                       
+                       WCF.DOMNodeInsertedHandler.disable();
+                       
+                       this._dropdowns[this._container[$containerID].data('objectID')] = $dropdownMenu;
+                       
+                       WCF.Dropdown.registerCallback($button.parent().wcfIdentify(), $.proxy(this._toggleDropdown, this));
+                       
+                       // trigger click event
+                       $button.trigger('click');
+               }
+               
+               event.stopPropagation();
+               return false;
+       },
+       
+       /**
+        * Handles errorneus editing requests.
+        * 
+        * @param       object          data
+        */
+       _failure: function(data) {
+               this._revertEditor();
+               
+               if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+                       return true;
+               }
+               
+               var $messageBody = this._container[this._activeElementID].find('.messageBody .messageInlineEditor');
+               var $innerError = $messageBody.children('small.innerError').empty();
+               if (!$innerError.length) {
+                       $innerError = $('<small class="innerError" />').insertBefore($messageBody.children('.formSubmit'));
+               }
+               
+               $innerError.html(data.returnValues.errorType);
+               
+               return false;
+       },
+       
+       /**
+        * Forces message options to stay visible if toggling dropdown menu.
+        * 
+        * @param       jQuery          dropdown
+        * @param       string          action
+        */
+       _toggleDropdown: function(dropdown, action) {
+               dropdown.parents('.messageOptions').toggleClass('forceOpen');
+       },
+       
+       /**
+        * Initializes the inline edit dropdown menu.
+        * 
+        * @param       integer         containerID
+        * @param       jQuery          dropdownMenu
+        */
+       _initDropdownMenu: function(containerID, dropdownMenu) { },
+       
+       /**
+        * Prepares message for WYSIWYG display.
+        */
+       _prepare: function() {
+               var $messageBody = this._container[this._activeElementID].find('.messageBody');
+               $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
+               
+               var $content = $messageBody.find('.messageText');
+               this._cache = $content.html();
+               $content.empty();
+       },
+       
+       /**
+        * Cancels editing and reverts to original message.
+        */
+       _cancel: function() {
+               var $container = this._container[this._activeElementID];
+               
+               // remove ckEditor
+               try {
+                       var $ckEditor = $('#' + this._messageEditorIDPrefix + $container.data('objectID')).ckeditorGet();
+                       $ckEditor.destroy();
+               }
+               catch (e) {
+                       // CKEditor might be not initialized yet, ignore
+               }
+               
+               // restore message
+               var $messageBody = $container.find('.messageBody');
+               $messageBody.children('.icon-spinner').remove();
+               $messageBody.find('.messageText').html(this._cache);
+               
+               // revert message options
+               this._container[this._activeElementID].find('.messageOptions').removeClass('forceHidden');
+               
+               this._activeElementID = '';
+       },
+       
+       /**
+        * Handles successful AJAX calls.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               switch (data.returnValues.actionName) {
+                       case 'beginEdit':
+                               this._showEditor(data);
+                       break;
+                       
+                       case 'save':
+                               this._showMessage(data);
+                       break;
+               }
+       },
+       
+       /**
+        * Shows WYSIWYG editor for active message.
+        * 
+        * @param       object          data
+        */
+       _showEditor: function(data) {
+               var $messageBody = this._container[this._activeElementID].find('.messageBody');
+               $messageBody.children('.icon-spinner').remove();
+               var $content = $messageBody.find('.messageText');
+               
+               // insert wysiwyg
+               $('' + data.returnValues.template).appendTo($content);
+               
+               // bind buttons
+               var $formSubmit = $content.find('.formSubmit');
+               var $saveButton = $formSubmit.find('button[data-type=save]').click($.proxy(this._save, this));
+               if (this._supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
+               $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
+               
+               WCF.Message.Submit.registerButton(
+                       this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID'),
+                       $saveButton
+               );
+               
+               // hide message options
+               this._container[this._activeElementID].find('.messageOptions').addClass('forceHidden');
+               
+               new WCF.PeriodicalExecuter($.proxy(function(pe) {
+                       pe.stop();
+                       
+                       $('#' + this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID')).ckeditorGet().ui.editor.focus();
+               }, this), 250);
+       },
+       
+       /**
+        * Reverts editor.
+        */
+       _revertEditor: function() {
+               var $messageBody = this._container[this._activeElementID].find('.messageBody');
+               $messageBody.children('span.icon-spinner').remove();
+               $messageBody.find('.messageText').children().show();
+       },
+       
+       /**
+        * Saves editor contents.
+        */
+       _save: function() {
+               var $container = this._container[this._activeElementID];
+               var $objectID = $container.data('objectID');
+               var $message = '';
+               
+               if ($.browser.mobile) {
+                       $message = $('#' + this._messageEditorIDPrefix + $objectID).val();
+               }
+               else {
+                       var $ckEditor = $('#' + this._messageEditorIDPrefix + $objectID).ckeditorGet();
+                       $message = $ckEditor.getData();
+               }
+               
+               this._proxy.setOption('data', {
+                       actionName: 'save',
+                       className: this._getClassName(),
+                       interfaceName: 'wcf\\data\\IMessageInlineEditorAction',
+                       parameters: {
+                               containerID: this._containerID,
+                               data: {
+                                       message: $message
+                               },
+                               objectID: $objectID
+                       }
+               });
+               this._proxy.sendRequest();
+               
+               this._hideEditor();
+       },
+       
+       /**
+        * Prepares jumping to extended editing mode.
+        */
+       _prepareExtended: function() {
+               var $container = this._container[this._activeElementID];
+               var $objectID = $container.data('objectID');
+               var $message = '';
+               
+               if ($.browser.mobile) {
+                       $message = $('#' + this._messageEditorIDPrefix + $objectID).val();
+               }
+               else {
+                       var $ckEditor = $('#' + this._messageEditorIDPrefix + $objectID).ckeditorGet();
+                       $message = $ckEditor.getData();
+               }
+               
+               new WCF.Action.Proxy({
+                       autoSend: true,
+                       data: {
+                               actionName: 'jumpToExtended',
+                               className: this._getClassName(),
+                               parameters: {
+                                       containerID: this._containerID,
+                                       message: $message,
+                                       messageID: $objectID
+                               }
+                       },
+                       success: function(data, textStatus, jqXHR) {
+                               window.location = data.returnValues.url;
+                       }
+               });
+       },
+       
+       /**
+        * Hides WYSIWYG editor.
+        */
+       _hideEditor: function() {
+               var $messageBody = this._container[this._activeElementID].find('.messageBody');
+               $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
+               $messageBody.find('.messageText').children().hide();
+       },
+       
+       /**
+        * Shows rendered message.
+        * 
+        * @param       object          data
+        */
+       _showMessage: function(data) {
+               var $container = this._container[this._activeElementID];
+               var $messageBody = $container.find('.messageBody');
+               $messageBody.children('.icon-spinner').remove();
+               var $content = $messageBody.find('.messageText');
+               
+               // revert message options
+               this._container[this._activeElementID].find('.messageOptions').removeClass('forceHidden');
+               
+               // remove editor
+               if (!$.browser.mobile) {
+                       var $ckEditor = $('#' + this._messageEditorIDPrefix + $container.data('objectID')).ckeditorGet();
+                       $ckEditor.destroy();
+               }
+               
+               $content.empty();
+               
+               // insert new message
+               $content.html(data.returnValues.message);
+               
+               this._activeElementID = '';
+               
+               this._updateHistory(this._getHash($container.data('objectID')));
+               
+               this._notification.show();
+       },
+       
+       /**
+        * Returns message action class name.
+        * 
+        * @return      string
+        */
+       _getClassName: function() {
+               return '';
+       },
+       
+       /**
+        * Returns the hash added to the url after successfully editing a message.
+        * 
+        * @return      string
+        */
+       _getHash: function(objectID) {
+               return '#message' + objectID;
+       },
+       
+       /**
+        * Updates the history to avoid old content when going back in the browser
+        * history.
+        * 
+        * @param       hash
+        */
+       _updateHistory: function(hash) {
+               window.location.hash = hash;
+       }
+});
+
+/**
+ * Handles submit buttons for forms with an embedded WYSIWYG editor.
+ */
+WCF.Message.Submit = {
+       /**
+        * list of registered buttons
+        * @var object
+        */
+       _buttons: { },
+       
+       /**
+        * Registers submit button for specified wysiwyg container id.
+        * 
+        * @param       string          wysiwygContainerID
+        * @param       string          selector
+        */
+       registerButton: function(wysiwygContainerID, selector) {
+               if (!WCF.Browser.isChrome()) {
+                       return;
+               }
+               
+               this._buttons[wysiwygContainerID] = $(selector);
+       },
+       
+       /**
+        * Triggers 'click' event for registered buttons.
+        */
+       execute: function(wysiwygContainerID) {
+               if (!this._buttons[wysiwygContainerID]) {
+                       return;
+               }
+               
+               this._buttons[wysiwygContainerID].trigger('click');
+       }
+};
+
+/**
+ * Namespace for message quotes.
+ */
+WCF.Message.Quote = { };
+
+/**
+ * Handles message quotes.
+ * 
+ * @param      string          className
+ * @param      string          objectType
+ * @param      string          containerSelector
+ * @param      string          messageBodySelector
+ */
+WCF.Message.Quote.Handler = Class.extend({
+       /**
+        * active container id
+        * @var string
+        */
+       _activeContainerID: '',
+       
+       /**
+        * action class name
+        * @var string
+        */
+       _className: '',
+       
+       /**
+        * list of message containers
+        * @var object
+        */
+       _containers: { },
+       
+       /**
+        * container selector
+        * @var string
+        */
+       _containerSelector: '',
+       
+       /**
+        * 'copy quote' overlay
+        * @var jQuery
+        */
+       _copyQuote: null,
+       
+       /**
+        * marked message
+        * @var string
+        */
+       _message: '',
+       
+       /**
+        * message body selector
+        * @var string
+        */
+       _messageBodySelector: '',
+       
+       /**
+        * object id
+        * @var integer
+        */
+       _objectID: 0,
+       
+       /**
+        * object type name
+        * @var string
+        */
+       _objectType: '',
+       
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * quote manager
+        * @var WCF.Message.Quote.Manager
+        */
+       _quoteManager: null,
+       
+       /**
+        * Initializes the quote handler for given object type.
+        * 
+        * @param       WCF.Message.Quote.Manager       quoteManager
+        * @param       string                          className
+        * @param       string                          objectType
+        * @param       string                          containerSelector
+        * @param       string                          messageBodySelector
+        * @param       string                          messageContentSelector
+        */
+       init: function(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector) {
+               this._className = className;
+               if (this._className == '') {
+                       console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
+                       return;
+               }
+               
+               this._objectType = objectType;
+               if (this._objectType == '') {
+                       console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
+                       return;
+               }
+               
+               this._containerSelector = containerSelector;
+               this._message = '';
+               this._messageBodySelector = messageBodySelector;
+               this._messageContentSelector = messageContentSelector;
+               this._objectID = 0;
+               this._proxy = new WCF.Action.Proxy({
+                       success: $.proxy(this._success, this)
+               });
+               
+               this._initContainers();
+               this._initCopyQuote();
+               
+               $(document).mouseup($.proxy(this._mouseUp, this));
+               
+               // register with quote manager
+               this._quoteManager = quoteManager;
+               this._quoteManager.register(this._objectType, this);
+               
+               // register with DOMNodeInsertedHandler
+               WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Quote.Handler' + objectType.hashCode(), $.proxy(this._initContainers, this));
+       },
+       
+       /**
+        * Initializes message containers.
+        */
+       _initContainers: function() {
+               var self = this;
+               $(this._containerSelector).each(function(index, container) {
+                       var $container = $(container);
+                       var $containerID = $container.wcfIdentify();
+                       
+                       if (!self._containers[$containerID]) {
+                               self._containers[$containerID] = $container;
+                               if ($container.hasClass('jsInvalidQuoteTarget')) {
+                                       return true;
+                               }
+                               
+                               if (self._messageBodySelector !== null) {
+                                       $container = $container.find(self._messageBodySelector).data('containerID', $containerID);
+                               }
+                               
+                               $container.mousedown($.proxy(self._mouseDown, self));
+                               
+                               // bind event to quote whole message
+                               self._containers[$containerID].find('.jsQuoteMessage').click($.proxy(self._saveFullQuote, self));
+                       }
+               });
+       },
+       
+       /**
+        * Handles mouse down event.
+        * 
+        * @param       object          event
+        */
+       _mouseDown: function(event) {
+               // hide copy quote
+               this._copyQuote.hide();
+               
+               // store container ID
+               var $container = $(event.currentTarget);
+               if (this._messageBodySelector) {
+                       $container = this._containers[$container.data('containerID')];
+               }
+               this._activeContainerID = $container.wcfIdentify();
+               
+               // remove alt-tag from all images, fixes quoting in Firefox
+               if ($.browser.mozilla) {
+                       $container.find('img').each(function() {
+                               var $image = $(this);
+                               $image.data('__alt', $image.attr('alt')).removeAttr('alt');
+                       });
+               }
+       },
+       
+       /**
+        * Returns the text of a node and its children.
+        * 
+        * @param       object          node
+        * @return      string
+        */
+       _getNodeText: function(node) {
+               var nodeText = '';
+               
+               for (var i = 0; i < node.childNodes.length; i++) {
+                       if (node.childNodes[i].nodeType == 3) {
+                               // text node
+                               nodeText += node.childNodes[i].nodeValue;
+                       }
+                       else {
+                               var $tagName = node.childNodes[i].tagName.toLowerCase();
+                               if ($tagName === 'li') {
+                                       nodeText += "\r\n";
+                               }
+                               
+                               nodeText += this._getNodeText(node.childNodes[i]);
+                               
+                               if ($tagName === 'ul') {
+                                       nodeText += "\n";
+                               }
+                       }
+               }
+               
+               return nodeText;
+       },
+       
+       /**
+        * Handles the mouse up event.
+        * 
+        * @param       object          event
+        */
+       _mouseUp: function(event) {
+               // ignore event
+               if (this._activeContainerID == '') {
+                       this._copyQuote.hide();
+                       
+                       return;
+               }
+               
+               var $container = this._containers[this._activeContainerID];
+               var $selection = this._getSelectedText();
+               var $text = $.trim($selection);
+               if ($text == '') {
+                       this._copyQuote.hide();
+                       
+                       return;
+               }
+               
+               // compare selection with message text of given container
+               var $messageText = null;
+               if (this._messageBodySelector) {
+                       $messageText = this._getNodeText($container.find(this._messageContentSelector).get(0));
+               }
+               else {
+                       $messageText = this._getNodeText($container.get(0));
+               }
+               
+               // selected text is not part of $messageText or contains text from unrelated nodes
+               if (this._normalize($messageText).indexOf(this._normalize($text)) === -1) {
+                       return;
+               }
+               this._copyQuote.show();
+               
+               var $coordinates = this._getBoundingRectangle($selection);
+               var $dimensions = this._copyQuote.getDimensions('outer');
+               var $left = ($coordinates.right - $coordinates.left) / 2 - ($dimensions.width / 2) + $coordinates.left;
+               
+               this._copyQuote.css({
+                       top: $coordinates.top - $dimensions.height - 7 + 'px',
+                       left: $left + 'px'
+               });
+               this._copyQuote.hide();
+               
+               // reset containerID
+               this._activeContainerID = '';
+               
+               // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
+               var self = this;
+               new WCF.PeriodicalExecuter(function(pe) {
+                       pe.stop();
+                       
+                       var $text = $.trim(self._getSelectedText());
+                       if ($text != '') {
+                               self._copyQuote.show();
+                               self._message = $text;
+                               self._objectID = $container.data('objectID');
+                               
+                               // revert alt tags, fixes quoting in Firefox
+                               if ($.browser.mozilla) {
+                                       $container.find('img').each(function() {
+                                               var $image = $(this);
+                                               $image.attr('alt', $image.data('__alt'));
+                                       });
+                               }
+                       }
+               }, 10);
+       },
+       
+       /**
+        * Normalizes a text for comparison.
+        * 
+        * @param       string          text
+        * @return      string
+        */
+       _normalize: function(text) {
+               return text.replace(/\r?\n|\r/g, "\n").replace(/\s{2,}/g, ' ');
+       },
+       
+       /**
+        * Returns the left or right offset of the current text selection.
+        * 
+        * @param       objct           range
+        * @param       boolean         before
+        * @return      object
+        */
+       _getOffset: function(range, before) {
+               range.collapse(before);
+               
+               var $elementID = WCF.getRandomID();
+               var $element = document.createElement('span');
+               $element.innerHTML = '<span id="' + $elementID + '"></span>';
+               var $fragment = document.createDocumentFragment(), $node, $lastNode;
+               while ($node = $element.firstChild) {
+                       $lastNode = $fragment.appendChild($node);
+               }
+               range.insertNode($fragment);
+               
+               $element = $('#' + $elementID);
+               var $position = $element.offset();
+               $position.top = $position.top - $(window).scrollTop();
+               $element.remove();
+               
+               return $position;
+       },
+       
+       /**
+        * Returns the offsets of the selection's bounding rectangle.
+        * 
+        * @return      object
+        */
+       _getBoundingRectangle: function(selection) {
+               var $coordinates = null;
+               
+               if (document.createRange && typeof document.createRange().getBoundingClientRect != "undefined") { // Opera, Firefox, Safari, Chrome
+                       if (selection.rangeCount > 0) {
+                               // the coordinates returned by getBoundingClientRect() is relative to the window, not the document!
+                               //var $rect = selection.getRangeAt(0).getBoundingClientRect();
+                               var $rects = selection.getRangeAt(0).getClientRects();
+                               if (!$.browser.mozilla && $rects.length > 1) {
+                                       var $range = selection.getRangeAt(0);
+                                       var $position1 = this._getOffset($range, true);
+                                       
+                                       var $range = selection.getRangeAt(0);
+                                       var $position2 = this._getOffset($range, false);
+                                       
+                                       var $rect = {
+                                               left: ($position1.left > $position2.left) ? $position2.left : $position1.left,
+                                               right: ($position1.left > $position2.left) ? $position1.left : $position2.left,
+                                               top: ($position1.top > $position2.top) ? $position2.top : $position1.top
+                                       };
+                               }
+                               else {
+                                       var $rect = selection.getRangeAt(0).getBoundingClientRect();
+                               }
+                               
+                               var $document = $(document);
+                               var $offsetTop = $document.scrollTop();
+                               
+                               $coordinates = {
+                                       left: $rect.left,
+                                       right: $rect.right,
+                                       top: $rect.top + $offsetTop
+                               };
+                       }
+               }
+               else if (document.selection && document.selection.type != "Control") { // IE
+                       var $range = document.selection.createRange();
+                       // TODO: Check coordinates if they're relative too!
+                       $coordinates = {
+                               left: $range.boundingLeft,
+                               right: $range.boundingRight,
+                               top: $range.boundingTop
+                       };
+               }
+               
+               return $coordinates;
+       },
+       
+       /**
+        * Initializes the 'copy quote' element.
+        */
+       _initCopyQuote: function() {
+               this._copyQuote = $('#quoteManagerCopy');
+               if (!this._copyQuote.length) {
+                       this._copyQuote = $('<div id="quoteManagerCopy" class="balloonTooltip"><span>' + WCF.Language.get('wcf.message.quote.quoteSelected') + '</span><span class="pointer"><span></span></span></div>').hide().appendTo(document.body);
+                       this._copyQuote.click($.proxy(this._saveQuote, this));
+               }
+       },
+       
+       /**
+        * Returns the text selection.
+        * 
+        * @return      object
+        */
+       _getSelectedText: function() {
+               if (window.getSelection) { // Opera, Firefox, Safari, Chrome, IE 9+
+                       return window.getSelection();
+               }
+               else if (document.getSelection) { // Opera, Firefox, Safari, Chrome, IE 9+
+                       return document.getSelection();
+               }
+               else if (document.selection) { // IE 8
+                       return document.selection.createRange().text;
+               }
+               
+               return '';
+       },
+       
+       /**
+        * Saves a full quote.
+        * 
+        * @param       object          event
+        */
+       _saveFullQuote: function(event) {
+               var $listItem = $(event.currentTarget);
+               
+               this._proxy.setOption('data', {
+                       actionName: 'saveFullQuote',
+                       className: this._className,
+                       interfaceName: 'wcf\\data\\IMessageQuoteAction',
+                       objectIDs: [ $listItem.data('objectID') ]
+               });
+               this._proxy.sendRequest();
+               
+               // mark element as quoted
+               if ($listItem.data('isQuoted')) {
+                       $listItem.data('isQuoted', false).children('a').removeClass('active');
+               }
+               else {
+                       $listItem.data('isQuoted', true).children('a').addClass('active');
+               }
+               
+               // discard event
+               event.stopPropagation();
+               return false;
+       },
+       
+       /**
+        * Saves a quote.
+        */
+       _saveQuote: function() {
+               this._proxy.setOption('data', {
+                       actionName: 'saveQuote',
+                       className: this._className,
+                       interfaceName: 'wcf\\data\\IMessageQuoteAction',
+                       objectIDs: [ this._objectID ],
+                       parameters: {
+                               message: this._message
+                       }
+               });
+               this._proxy.sendRequest();
+       },
+       
+       /**
+        * Handles successful AJAX requests.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               if (data.returnValues.count !== undefined) {
+                       var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : { };
+                       this._quoteManager.updateCount(data.returnValues.count, $fullQuoteObjectIDs);
+               }
+       },
+       
+       /**
+        * Updates the full quote data for all matching objects.
+        * 
+        * @param       array<integer>          $objectIDs
+        */
+       updateFullQuoteObjectIDs: function(objectIDs) {
+               for (var $containerID in this._containers) {
+                       this._containers[$containerID].find('.jsQuoteMessage').each(function(index, button) {
+                               // reset all markings
+                               var $button = $(button).data('isQuoted', 0);
+                               $button.children('a').removeClass('active');
+                               
+                               // mark as active
+                               if (WCF.inArray($button.data('objectID'), objectIDs)) {
+                                       $button.data('isQuoted', 1).children('a').addClass('active');
+                               }
+                       });
+               }
+       }
+});
+
+/**
+ * Manages stored quotes.
+ * 
+ * @param      integer         count
+ */
+WCF.Message.Quote.Manager = Class.extend({
+       /**
+        * list of form buttons
+        * @var object
+        */
+       _buttons: { },
+       
+       /**
+        * ckEditor element
+        * @var jQuery
+        */
+       _ckEditor: null,
+       
+       /**
+        * number of stored quotes
+        * @var integer
+        */
+       _count: 0,
+       
+       /**
+        * dialog overlay
+        * @var jQuery
+        */
+       _dialog: null,
+       
+       /**
+        * form element
+        * @var jQuery
+        */
+       _form: null,
+       
+       /**
+        * list of quote handlers
+        * @var object
+        */
+       _handlers: { },
+       
+       /**
+        * true, if an up-to-date template exists
+        * @var boolean
+        */
+       _hasTemplate: false,
+       
+       /**
+        * true, if related quotes should be inserted
+        * @var boolean
+        */
+       _insertQuotes: true,
+       
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * list of quotes to remove upon submit
+        * @var array<string>
+        */
+       _removeOnSubmit: [ ],
+       
+       /**
+        * show quotes element
+        * @var jQuery
+        */
+       _showQuotes: null,
+       
+       /**
+        * allow pasting
+        * @var boolean
+        */
+       _supportPaste: false,
+       
+       /**
+        * Initializes the quote manager.
+        * 
+        * @param       integer         count
+        * @param       string          ckEditorID
+        * @param       boolean         supportPaste
+        * @param       array<string>   removeOnSubmit
+        */
+       init: function(count, ckEditorID, supportPaste, removeOnSubmit) {
+               this._buttons = {
+                       insert: null,
+                       remove: null
+               };
+               this._ckEditor = null;
+               this._count = parseInt(count) || 0;
+               this._dialog = null;
+               this._form = null;
+               this._handlers = { };
+               this._hasTemplate = false;
+               this._insertQuotes = true;
+               this._removeOnSubmit = [ ];
+               this._showQuotes = null;
+               this._supportPaste = false;
+               
+               if (ckEditorID) {
+                       this._ckEditor = $('#' + ckEditorID);
+                       if (this._ckEditor.length) {
+                               this._supportPaste = true;
+                               
+                               // get surrounding form-tag
+                               this._form = this._ckEditor.parents('form:eq(0)');
+                               if (this._form.length) {
+                                       this._form.submit($.proxy(this._submit, this));
+                                       this._removeOnSubmit = removeOnSubmit || [ ];
+                               }
+                               else {
+                                       this._form = null;
+                                       
+                                       // allow override
+                                       this._supportPaste = (supportPaste === true) ? true : false;
+                               }
+                       }
+               }
+               
+               this._proxy = new WCF.Action.Proxy({
+                       showLoadingOverlay: false,
+                       success: $.proxy(this._success, this),
+                       url: 'index.php/MessageQuote/?t=' + SECURITY_TOKEN + SID_ARG_2ND
+               });
+               
+               this._toggleShowQuotes();
+       },
+       
+       /**
+        * Registers a quote handler.
+        * 
+        * @param       string                          objectType
+        * @param       WCF.Message.Quote.Handler       handler
+        */
+       register: function(objectType, handler) {
+               this._handlers[objectType] = handler;
+       },
+       
+       /**
+        * Updates number of stored quotes.
+        * 
+        * @param       integer         count
+        * @param       object          fullQuoteObjectIDs
+        */
+       updateCount: function(count, fullQuoteObjectIDs) {
+               this._count = parseInt(count) || 0;
+               
+               this._toggleShowQuotes();
+               
+               // update full quote ids of handlers
+               for (var $objectType in this._handlers) {
+                       if (fullQuoteObjectIDs[$objectType]) {
+                               this._handlers[$objectType].updateFullQuoteObjectIDs(fullQuoteObjectIDs[$objectType]);
+                       }
+               }
+       },
+       
+       /**
+        * Inserts all associated quotes upon first time using quick reply.
+        * 
+        * @param       string          className
+        * @param       integer         parentObjectID
+        * @param       object          callback
+        */
+       insertQuotes: function(className, parentObjectID, callback) {
+               if (!this._insertQuotes) {
+                       this._insertQuotes = true;
+                       
+                       return;
+               }
+               
+               new WCF.Action.Proxy({
+                       autoSend: true,
+                       data: {
+                               actionName: 'getRenderedQuotes',
+                               className: className,
+                               interfaceName: 'wcf\\data\\IMessageQuoteAction',
+                               parameters: {
+                                       parentObjectID: parentObjectID
+                               }
+                       },
+                       success: callback
+               });
+       },
+       
+       /**
+        * Toggles the display of the 'Show quotes' button
+        */
+       _toggleShowQuotes: function() {
+               if (!this._count) {
+                       if (this._showQuotes !== null) {
+                               this._showQuotes.hide();
+                       }
+               }
+               else {
+                       if (this._showQuotes === null) {
+                               this._showQuotes = $('#showQuotes');
+                               if (!this._showQuotes.length) {
+                                       this._showQuotes = $('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click, this)).appendTo(document.body);
+                               }
+                       }
+                       
+                       var $text = WCF.Language.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count);
+                       this._showQuotes.text($text).show();
+               }
+               
+               this._hasTemplate = false;
+       },
+       
+       /**
+        * Handles clicks on 'Show quotes'.
+        */
+       _click: function() {
+               if (this._hasTemplate) {
+                       this._dialog.wcfDialog('open');
+               }
+               else {
+                       this._proxy.showLoadingOverlayOnce();
+                       
+                       this._proxy.setOption('data', {
+                               actionName: 'getQuotes',
+                               supportPaste: this._supportPaste
+                       });
+                       this._proxy.sendRequest();
+               }
+       },
+       
+       /**
+        * Renders the dialog.
+        * 
+        * @param       string          template
+        */
+       renderDialog: function(template) {
+               // create dialog if not exists
+               if (this._dialog === null) {
+                       this._dialog = $('#messageQuoteList');
+                       if (!this._dialog.length) {
+                               this._dialog = $('<div id="messageQuoteList" />').hide().appendTo(document.body);
+                       }
+               }
+               
+               // add template
+               this._dialog.html(template);
+               
+               // add 'insert' and 'delete' buttons
+               var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
+               if (this._supportPaste) this._buttons.insert = $('<button>' + WCF.Language.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected, this)).appendTo($formSubmit);
+               this._buttons.remove = $('<button>' + WCF.Language.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected, this)).appendTo($formSubmit);
+               
+               // show dialog
+               this._dialog.wcfDialog({
+                       title: WCF.Language.get('wcf.message.quote.manageQuotes')
+               });
+               this._dialog.wcfDialog('render');
+               this._hasTemplate = true;
+               
+               // bind event listener
+               var $insertQuoteButtons = this._dialog.find('.jsInsertQuote');
+               if (this._supportPaste) {
+                       $insertQuoteButtons.click($.proxy(this._insertQuote, this));
+               }
+               else {
+                       $insertQuoteButtons.hide();
+               }
+               
+               this._dialog.find('input.jsCheckbox').change($.proxy(this._changeButtons, this));
+               
+               // mark quotes for removal
+               // TODO: is this still supported?
+               if (this._removeOnSubmit.length) {
+                       var self = this;
+                       this._dialog.find('input.jsRemoveQuote').each(function(index, input) {
+                               var $input = $(input).change($.proxy(this._change, this));
+                               
+                               // mark for deletion
+                               if (WCF.inArray($input.parent('li').attr('data-quote-id'), self._removeOnSubmit)) {
+                                       $input.attr('checked', 'checked');
+                               }
+                       });
+               }
+       },
+       
+       /**
+        * Updates button labels if a checkbox is checked or unchecked.
+        */
+       _changeButtons: function() {
+               // selection
+               if (this._dialog.find('input.jsCheckbox:checked').length) {
+                       if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertSelectedQuotes'));
+                       this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeSelectedQuotes'));
+               }
+               else {
+                       // no selection, pick all
+                       if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertAllQuotes'));
+                       this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeAllQuotes'));
+               }
+       },
+       
+       /**
+        * Checks for change event on delete-checkboxes.
+        * 
+        * @param       object          event
+        */
+       _change: function(event) {
+               var $input = $(event.currentTarget);
+               var $quoteID = $input.parent('li').attr('data-quote-id');
+               
+               if ($input.prop('checked')) {
+                       this._removeOnSubmit.push($quoteID);
+               }
+               else {
+                       for (var $index in this._removeOnSubmit) {
+                               if (this._removeOnSubmit[$index] == $quoteID) {
+                                       delete this._removeOnSubmit[$index];
+                                       break;
+                               }
+                       }
+               }
+       },
+       
+       /**
+        * Inserts the selected quotes.
+        */
+       _insertSelected: function() {
+               var $api = $('.jsQuickReply:eq(0)').data('__api');
+               if ($api && !$api.getContainer().is(':visible')) {
+                       this._insertQuotes = false;
+                       $api.click(null);
+               }
+               
+               if (!this._dialog.find('input.jsCheckbox:checked').length) {
+                       this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
+               }
+               
+               // insert all quotes
+               this._dialog.find('input.jsCheckbox:checked').each($.proxy(function(index, input) {
+                       this._insertQuote(null, input);
+               }, this));
+               
+               // close dialog
+               this._dialog.wcfDialog('close');
+       },
+       
+       /**
+        * Inserts a quote.
+        * 
+        * @param       object          event
+        * @param       object          inputElement
+        */
+       _insertQuote: function(event, inputElement) {
+               if (event !== null) {
+                       var $api = $('.jsQuickReply:eq(0)').data('__api');
+                       if ($api && !$api.getContainer().is(':visible')) {
+                               this._insertQuotes = false;
+                               $api.click(null);
+                       }
+               }
+               
+               var $listItem = (event === null) ? $(inputElement).parents('li') : $(event.currentTarget).parents('li');
+               var $quote = $.trim($listItem.children('div.jsFullQuote').text());
+               var $message = $listItem.parents('article.message');
+               
+               // build quote tag
+               $quote = "[quote='" + $message.attr('data-username') + "','" + $message.data('link') + "']" + $quote + "[/quote]";
+               
+               // insert into ckEditor
+               var $ckEditor = ($.browser.mobile) ? null : this._ckEditor.ckeditorGet();
+               if ($ckEditor !== null && $ckEditor.mode === 'wysiwyg') {
+                       // in design mode
+                       $ckEditor.insertText($quote + "\n\n");
+               }
+               else {
+                       // in source mode
+                       var $textarea = ($.browser.mobile) ? this._ckEditor : this._ckEditor.next('.cke_editor_text').find('textarea');
+                       var $value = $textarea.val();
+                       $quote += "\n\n";
+                       if ($value.length == 0) {
+                               $textarea.val($quote);
+                       }
+                       else {
+                               var $position = $textarea.getCaret();
+                               $textarea.val( $value.substr(0, $position) + $quote + $value.substr($position) );
+                       }
+               }
+               
+               // remove quote upon submit or upon request
+               this._removeOnSubmit.push($listItem.attr('data-quote-id'));
+               
+               // close dialog
+               if (event !== null) {
+                       this._dialog.wcfDialog('close');
+               }
+       },
+       
+       /**
+        * Removes selected quotes.
+        */
+       _removeSelected: function() {
+               if (!this._dialog.find('input.jsCheckbox:checked').length) {
+                       this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
+               }
+               
+               var $quoteIDs = [ ];
+               this._dialog.find('input.jsCheckbox:checked').each(function(index, input) {
+                       $quoteIDs.push($(input).parents('li').attr('data-quote-id'));
+               });
+               
+               if ($quoteIDs.length) {
+                       // get object types
+                       var $objectTypes = [ ];
+                       for (var $objectType in this._handlers) {
+                               $objectTypes.push($objectType);
+                       }
+                       
+                       this._proxy.setOption('data', {
+                               actionName: 'remove',
+                               objectTypes: $objectTypes,
+                               quoteIDs: $quoteIDs
+                       });
+                       this._proxy.sendRequest();
+                       
+                       this._dialog.wcfDialog('close');
+               }
+       },
+       
+       /**
+        * Appends list of quote ids to remove after successful submit.
+        */
+       _submit: function() {
+               if (this._supportPaste && this._removeOnSubmit.length > 0) {
+                       var $formSubmit = this._form.find('.formSubmit');
+                       for (var $i in this._removeOnSubmit) {
+                               $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit[$i] + '" />').appendTo($formSubmit);
+                       }
+               }
+       },
+       
+       /**
+        * Returns a list of quote ids marked for removal.
+        * 
+        * @return      array<integer>
+        */
+       getQuotesMarkedForRemoval: function() {
+               return this._removeOnSubmit;
+       },
+       
+       /**
+        * Marks quote ids for removal.
+        */
+       markQuotesForRemoval: function() {
+               if (this._removeOnSubmit.length) {
+                       this._proxy.setOption('data', {
+                               actionName: 'markForRemoval',
+                               quoteIDs: this._removeOnSubmit
+                       });
+                       this._proxy.sendRequest();
+               }
+       },
+       
+       /**
+        * Remoes all marked quote ids.
+        */
+       removeMarkedQuotes: function() {
+               if (this._removeOnSubmit.length) {
+                       this._proxy.setOption('data', {
+                               actionName: 'removeMarkedQuotes'
+                       });
+                       this._proxy.sendRequest();
+               }
+       },
+       
+       /**
+        * Counts stored quotes.
+        */
+       countQuotes: function() {
+               var $objectTypes = [ ];
+               for (var $objectType in this._handlers) {
+                       $objectTypes.push($objectType);
+               }
+               
+               this._proxy.setOption('data', {
+                       actionName: 'count',
+                       objectTypes: $objectTypes
+               });
+               this._proxy.sendRequest();
+       },
+       
+       /**
+        * Handles successful AJAX requests.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               if (data === null) {
+                       return;
+               }
+               
+               if (data.count !== undefined) {
+                       var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : { };
+                       this.updateCount(data.count, $fullQuoteObjectIDs);
+               }
+               
+               if (data.template !== undefined) {
+                       if ($.trim(data.template) == '') {
+                               this.updateCount(0, { });
+                       }
+                       else {
+                               this.renderDialog(data.template);
+                       }
+               }
+       }
+});
+
+/**
+ * Namespace for message sharing related classes.
+ */
+WCF.Message.Share = { };
+
+/**
+ * Displays a dialog overlay for permalinks.
+ */
+WCF.Message.Share.Content = Class.extend({
+       /**
+        * list of cached templates
+        * @var object
+        */
+       _cache: { },
+       
+       /**
+        * dialog overlay
+        * @var jQuery
+        */
+       _dialog: null,
+       
+       /**
+        * Initializes the WCF.Message.Share.Content class.
+        */
+       init: function() {
+               this._cache = { };
+               this._dialog = null;
+               
+               this._initLinks();
+               
+               WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Share.Content', $.proxy(this._initLinks, this));
+       },
+       
+       /**
+        * Initializes share links.
+        */
+       _initLinks: function() {
+               $('a.jsButtonShare').removeClass('jsButtonShare').click($.proxy(this._click, this));
+       },
+       
+       /**
+        * Displays links to share this content.
+        * 
+        * @param       object          event
+        */
+       _click: function(event) {
+               event.preventDefault();
+               
+               var $target = $(event.currentTarget);
+               var $link = $target.prop('href');
+               var $title = ($target.data('linkTitle') ? $target.data('linkTitle') : $link);
+               var $key = $link.hashCode();
+               if (this._cache[$key] === undefined) {
+                       
+                       // remove dialog contents
+                       var $dialogInitialized = false;
+                       if (this._dialog === null) {
+                               this._dialog = $('<div />').hide().appendTo(document.body);
+                               $dialogInitialized = true;
+                       }
+                       else {
+                               this._dialog.empty();
+                       }
+                       
+                       // permalink (plain text)
+                       var $fieldset = $('<fieldset><legend><label for="__sharePermalink">' + WCF.Language.get('wcf.message.share.permalink') + '</label></legend></fieldset>').appendTo(this._dialog);
+                       $('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr('value', $link).appendTo($fieldset);
+                       
+                       // permalink (BBCode)
+                       var $fieldset = $('<fieldset><legend><label for="__sharePermalinkBBCode">' + WCF.Language.get('wcf.message.share.permalink.bbcode') + '</label></legend></fieldset>').appendTo(this._dialog);
+                       $('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr('value', '[url=\'' + $link + '\']' + $title + '[/url]').appendTo($fieldset);
+                       
+                       // permalink (HTML)
+                       var $fieldset = $('<fieldset><legend><label for="__sharePermalinkHTML">' + WCF.Language.get('wcf.message.share.permalink.html') + '</label></legend></fieldset>').appendTo(this._dialog);
+                       $('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr('value', '<a href="' + $link + '">' + WCF.String.escapeHTML($title) + '</a>').appendTo($fieldset);
+                       
+                       this._cache[$key] = this._dialog.html();
+                       
+                       if ($dialogInitialized) {
+                               this._dialog.wcfDialog({
+                                       title: WCF.Language.get('wcf.message.share')
+                               });
+                       }
+                       else {
+                               this._dialog.wcfDialog('open');
+                       }
+               }
+               else {
+                       
+                       this._dialog.html(this._cache[$key]).wcfDialog('open');
+               }
+               
+               this._dialog.find('input').click(function() { $(this).select(); });
+       }
+});
+
+/**
+ * Provides buttons to share a page through multiple social community sites.
+ * 
+ * @param      boolean         fetchObjectCount
+ */
+WCF.Message.Share.Page = Class.extend({
+       /**
+        * list of share buttons
+        * @var object
+        */
+       _ui: { },
+       
+       /**
+        * page description
+        * @var string
+        */
+       _pageDescription: '',
+       
+       /**
+        * canonical page URL
+        * @var string
+        */
+       _pageURL: '',
+       
+       /**
+        * Initializes the WCF.Message.Share.Page class.
+        * 
+        * @param       boolean         fetchObjectCount
+        */
+       init: function(fetchObjectCount) {
+               this._pageDescription = encodeURIComponent($('meta[property="og:description"]').prop('content'));
+               this._pageURL = encodeURIComponent($('meta[property="og:url"]').prop('content'));
+               
+               var $container = $('.messageShareButtons');
+               this._ui = {
+                       facebook: $container.find('.jsShareFacebook'),
+                       google: $container.find('.jsShareGoogle'),
+                       reddit: $container.find('.jsShareReddit'),
+                       twitter: $container.find('.jsShareTwitter')
+               };
+               
+               this._ui.facebook.children('a').click($.proxy(this._shareFacebook, this));
+               this._ui.google.children('a').click($.proxy(this._shareGoogle, this));
+               this._ui.reddit.children('a').click($.proxy(this._shareReddit, this));
+               this._ui.twitter.children('a').click($.proxy(this._shareTwitter, this));
+               
+               if (fetchObjectCount === true) {
+                       this._fetchFacebook();
+                       this._fetchTwitter();
+                       this._fetchReddit();
+               }
+       },
+       
+       /**
+        * Shares current page to selected social community site.
+        * 
+        * @param       string          objectName
+        * @param       string          url
+        */
+       _share: function(objectName, url) {
+               window.open(url.replace(/{pageURL}/, this._pageURL).replace(/{text}/, this._pageDescription), 'height=600,width=600');
+       },
+       
+       /**
+        * Shares current page with Facebook.
+        */
+       _shareFacebook: function() {
+               this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}');
+       },
+       
+       /**
+        * Shares current page with Google Plus.
+        */
+       _shareGoogle: function() {
+               this._share('google', 'https://plus.google.com/share?url={pageURL}');
+       },
+       
+       /**
+        * Shares current page with Reddit.
+        */
+       _shareReddit: function() {
+               this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}');
+       },
+       
+       /**
+        * Shares current page with Twitter.
+        */
+       _shareTwitter: function() {
+               this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}');
+       },
+       
+       /**
+        * Fetches share count from a social community site.
+        * 
+        * @param       string          url
+        * @param       object          callback
+        * @param       string          callbackName
+        */
+       _fetchCount: function(url, callback, callbackName) {
+               var $options = {
+                       autoSend: true,
+                       dataType: 'jsonp',
+                       showLoadingOverlay: false,
+                       success: callback,
+                       suppressErrors: true,
+                       type: 'GET',
+                       url: url.replace(/{pageURL}/, this._pageURL)
+               };
+               if (callbackName) {
+                       $options.jsonp = callbackName;
+               }
+               
+               new WCF.Action.Proxy($options);
+       },
+       
+       /**
+        * Fetches number of Facebook likes.
+        */
+       _fetchFacebook: function() {
+               this._fetchCount('https://graph.facebook.com/?id={pageURL}', $.proxy(function(data) {
+                       if (data.shares) {
+                               this._ui.facebook.children('span.badge').show().text(data.shares);
+                       }
+               }, this));
+       },
+       
+       /**
+        * Fetches tweet count from Twitter.
+        */
+       _fetchTwitter: function() {
+               this._fetchCount('http://urls.api.twitter.com/1/urls/count.json?url={pageURL}', $.proxy(function(data) {
+                       if (data.count) {
+                               this._ui.twitter.children('span.badge').show().text(data.count);
+                       }
+               }, this));
+       },
+       
+       /**
+        * Fetches cumulative vote sum from Reddit.
+        */
+       _fetchReddit: function() {
+               this._fetchCount('http://www.reddit.com/api/info.json?url={pageURL}', $.proxy(function(data) {
+                       if (data.data.children.length) {
+                               this._ui.reddit.children('span.badge').show().text(data.data.children[0].data.score);
+                       }
+               }, this), 'jsonp');
+       }
+});
diff --git a/wcfsetup/install/files/js/WCF.Message.min.js b/wcfsetup/install/files/js/WCF.Message.min.js
new file mode 100644 (file)
index 0000000..a2bca0b
--- /dev/null
@@ -0,0 +1 @@
+WCF.Message={};WCF.Message.BBCode={};WCF.Message.BBCode.CodeViewer=Class.extend({_dialog:null,init:function(){this._dialog=null;this._initCodeBoxes();WCF.DOMNodeInsertedHandler.addCallback("WCF.Message.BBCode.CodeViewer",$.proxy(this._initCodeBoxes,this));WCF.DOMNodeInsertedHandler.forceExecution()},_initCodeBoxes:function(){$(".codeBox:not(.jsCodeViewer)").each($.proxy(function(a,c){var b=$(c).addClass("jsCodeViewer");$('<span class="icon icon16 icon-copy pointer jsTooltip" title="'+WCF.Language.get("wcf.message.bbcode.code.copy")+'" />').appendTo(b.find("div > h3")).click($.proxy(this._click,this))},this))},_click:function(b){var a="";$(b.currentTarget).parents("div").next("ol").children("li").each(function(c,d){if(a){a+="\n"}a+=$(d).text().replace(/\n+$/,"")});if(this._dialog===null){this._dialog=$('<div><textarea cols="60" rows="12" readonly="readonly" /></div>').hide().appendTo(document.body);this._dialog.children("textarea").val(a);this._dialog.wcfDialog({title:WCF.Language.get("wcf.message.bbcode.code.copy")})}else{this._dialog.children("textarea").val(a);this._dialog.wcfDialog("open")}this._dialog.children("textarea").select()}});WCF.Message.FormGuard=Class.extend({init:function(){var a=$("form.jsFormGuard").removeClass("jsFormGuard").submit(function(){$(this).find(".formSubmit input[type=submit]").disable()});$(window).unload(function(){a.find(".formSubmit input[type=submit]").enable()})}});WCF.Message.Preview=Class.extend({_className:"",_messageFieldID:"",_messageField:null,_proxy:null,_previewButton:null,_previewButtonLabel:"",init:function(b,a,c){this._className=b;this._messageFieldID=$.wcfEscapeID(a);this._messageField=$("#"+this._messageFieldID);if(!this._messageField.length){console.debug("[WCF.Message.Preview] Unable to find message field identified by '"+this._messageFieldID+"'");return}c=$.wcfEscapeID(c);this._previewButton=$("#"+c);if(!this._previewButton.length){console.debug("[WCF.Message.Preview] Unable to find preview button identified by '"+c+"'");return}this._previewButton.click($.proxy(this._click,this));this._proxy=new WCF.Action.Proxy({success:$.proxy(this._success,this)})},_click:function(b){var a=this._getMessage();if(a===null){console.debug("[WCF.Message.Preview] Unable to access ckEditor instance of '"+this._messageFieldID+"'");return}this._proxy.setOption("data",{actionName:"getMessagePreview",className:this._className,parameters:this._getParameters(a)});this._proxy.sendRequest();this._previewButtonLabel=this._previewButton.html();this._previewButton.html(WCF.Language.get("wcf.global.loading")).disable();b.stopPropagation();return false},_getParameters:function(b){var a={};$("#settings").find("input[type=checkbox]").each(function(c,e){var d=$(e);if(d.is(":checked")){a[d.prop("name")]=d.prop("value")}});return{data:{message:b},options:a}},_getMessage:function(){if($.browser.mobile){return this._messageField.val()}else{if(this._messageField.data("ckeditorInstance")){var a=this._messageField.ckeditorGet();return a.getData()}}return null},_success:function(b,c,a){this._previewButton.html(this._previewButtonLabel).enable();this._handleResponse(b)},_handleResponse:function(a){}});WCF.Message.DefaultPreview=WCF.Message.Preview.extend({_attachmentObjectType:null,_attachmentObjectID:null,_tmpHash:null,init:function(b,a,c){this._super("wcf\\data\\bbcode\\MessagePreviewAction","text","previewButton");this._attachmentObjectType=b||null;this._attachmentObjectID=a||null;this._tmpHash=c||null},_handleResponse:function(b){var a=$("#previewContainer");if(!a.length){a=$('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>'+WCF.Language.get("wcf.global.preview")+"</legend><div></div></fieldset>").prependTo($("#messageContainer")).wcfFadeIn()}a.find("div:eq(0)").html(b.returnValues.message)},_getParameters:function(b){var a=this._super(b);if(this._attachmentObjectType!=null){a.attachmentObjectType=this._attachmentObjectType;a.attachmentObjectID=this._attachmentObjectID;a.tmpHash=this._tmpHash}return a}});WCF.Message.Multilingualism=Class.extend({_availableLanguages:{},_languageID:0,_languageInput:null,init:function(c,d,a){this._availableLanguages=d;this._languageID=c||0;this._languageInput=$("#languageID");this._updateLabel();this._languageInput.find(".dropdownMenu > li").click($.proxy(this._click,this));if(!a){var b=this._languageInput.find(".dropdownMenu");$('<li class="dropdownDivider" />').appendTo(b);$('<li><span><span class="badge">'+this._availableLanguages[0]+"</span></span></li>").click($.proxy(this._disable,this)).appendTo(b)}this._languageInput.parents("form").submit($.proxy(this._submit,this))},_click:function(a){this._languageID=$(a.currentTarget).data("languageID");this._updateLabel()},_disable:function(){this._languageID=0;this._updateLabel()},_updateLabel:function(){this._languageInput.find(".dropdownToggle > span").text(this._availableLanguages[this._languageID])},_submit:function(){this._languageInput.next("input[name=languageID]").prop("value",this._languageID)}});WCF.Message.SmileyCategories=Class.extend({_cache:[],_proxy:null,_ckEditor:null,init:function(){this._cache=[];this._proxy=new WCF.Action.Proxy({success:$.proxy(this._success,this)});$("#smilies").on("wcftabsbeforeactivate",$.proxy(this._click,this));var a=this;new WCF.PeriodicalExecuter(function(b){b.stop();a._click({},{newTab:$("#smilies > .menu li.ui-state-active")})},100)},_click:function(b,c){var a=parseInt($(c.newTab).children("a").data("smileyCategoryID"));if(a&&!WCF.inArray(a,this._cache)){this._proxy.setOption("data",{actionName:"getSmilies",className:"wcf\\data\\smiley\\category\\SmileyCategoryAction",objectIDs:[a]});this._proxy.sendRequest()}},_success:function(c,d,b){var a=parseInt(c.returnValues.smileyCategoryID);this._cache.push(a);$("#smilies-"+a).html(c.returnValues.template)}});WCF.Message.Smilies=Class.extend({_ckEditor:null,init:function(a){if(a){this._ckEditor=$("#"+a);$(document).on("click",".jsSmiley",$.proxy(this._smileyClick,this))}},_smileyClick:function(c){var e=$(c.currentTarget);var h=e.data("smileyCode");var i=this._ckEditor.ckeditorGet();var f=e.find("img").attr("src");if(!WCF.inArray(h,i.config.smiley_descriptions)){i.config.smiley_descriptions.push(h);i.config.smiley_images.push(f)}if(i.mode==="wysiwyg"){var a=i.document.createElement("img",{attributes:{src:f,"class":"smiley",alt:h}});i.insertText(" ");i.insertElement(a);i.insertText(" ")}else{var g=this._ckEditor.next(".cke_editor_text").find("textarea");var j=g.val();if(j.length==0){g.val(h);g.setCaret(h.length)}else{var d=g.getCaret();var b=((j.substr(d-1,1)!==" ")?" ":"")+h+" ";g.val(j.substr(0,d)+b+j.substr(d));g.setCaret(d+b.length)}}}});WCF.Message.QuickReply=Class.extend({_container:null,_messageField:null,_notification:null,_proxy:null,_quoteManager:null,_scrollHandler:null,_successMessageNonVisible:"",init:function(c,b){this._container=$("#messageQuickReply");this._messageField=$("#text");if(!this._container||!this._messageField){return}var a=this._container.find(".formSubmit");a.find("button[data-type=save]").click($.proxy(this._save,this));if(c){a.find("button[data-type=extended]").click($.proxy(this._prepareExtended,this))}a.find("button[data-type=cancel]").click($.proxy(this._cancel,this));if(b){this._quoteManager=b}$(".jsQuickReply").data("__api",this).click($.proxy(this.click,this));this._proxy=new WCF.Action.Proxy({failure:$.proxy(this._failure,this),showLoadingOverlay:false,success:$.proxy(this._success,this)});this._scroll=new WCF.Effect.Scroll();this._notification=new WCF.System.Notification(WCF.Language.get("wcf.global.success.add"));this._successMessageNonVisible=""},click:function(b){this._container.toggle();if(this._container.is(":visible")){this._scroll.scrollTo(this._container,true);WCF.Message.Submit.registerButton("text",this._container.find(".formSubmit button[data-type=save]"));if(this._quoteManager){var a=true;if($.browser.touch){a=(!this._messageField.val().length)}else{a=(!this._messageField.ckeditorGet().getData().length)}if(a){this._quoteManager.insertQuotes(this._getClassName(),this._getObjectID(),$.proxy(this._insertQuotes,this))}}new WCF.PeriodicalExecuter($.proxy(function(c){c.stop();if($.browser.mobile){this._messageField.focus()}else{this._messageField.ckeditorGet().ui.editor.focus()}},this),250)}if(b!==null){b.stopPropagation();return false}},getContainer:function(){return this._container},_insertQuotes:function(a){if(!a.returnValues.template){return}if($.browser.mobile){this._messageField.val(a.returnValues.template)}else{this._messageField.ckeditorGet().insertText(a.returnValues.template)}},_save:function(){var b="";if($.browser.mobile){b=$.trim(this._messageField.val())}else{var a=this._messageField.ckeditorGet();b=$.trim(a.getData())}var d=this._messageField.parent().find("small.innerError");if(b===""){if(!d.length){d=$('<small class="innerError" />').appendTo(this._messageField.parent())}d.html(WCF.Language.get("wcf.global.form.error.empty"));return}else{d.remove()}this._proxy.setOption("data",{actionName:"quickReply",className:this._getClassName(),interfaceName:"wcf\\data\\IMessageQuickReplyAction",parameters:this._getParameters(b)});this._proxy.sendRequest();var c=this._container.find(".messageQuickReplyContent .messageBody");$('<span class="icon icon48 icon-spinner" />').appendTo(c);c.children("#cke_text").hide().end().next().hide()},_getParameters:function(b){var a={objectID:this._getObjectID(),data:{message:b},lastPostTime:this._container.data("lastPostTime"),pageNo:this._container.data("pageNo"),removeQuoteIDs:(this._quoteManager===null?[]:this._quoteManager.getQuotesMarkedForRemoval())};if(this._container.data("anchor")){a.anchor=this._container.data("anchor")}return a},_cancel:function(){this._revertQuickReply(true);if($.browser.mobile){this._messageField.val("")}else{this._messageField.ckeditorGet().setData("")}},_revertQuickReply:function(b){var a=this._container.find(".messageQuickReplyContent .messageBody");if(b){this._container.hide();a.children("small.innerError").remove()}a.children(".icon-spinner").remove();a.children("#cke_text").show();a.next().show()},_prepareExtended:function(){if(this._quoteManager!==null){this._quoteManager.markQuotesForRemoval()}var b="";if($.browser.mobile){b=this._messageField.val()}else{var a=this._messageField.ckeditorGet();b=a.getData()}new WCF.Action.Proxy({autoSend:true,data:{actionName:"jumpToExtended",className:this._getClassName(),interfaceName:"wcf\\data\\IExtendedMessageQuickReplyAction",parameters:{containerID:this._getObjectID(),message:b}},success:function(d,e,c){window.location=d.returnValues.url}})},_success:function(c,d,b){if(c.returnValues.url){window.location=c.returnValues.url}else{if(c.returnValues.template){var a=$(""+c.returnValues.template);a.insertBefore(this._container);this._container.data("lastPostTime",c.returnValues.lastPostTime);this._notification.show(undefined,undefined,WCF.Language.get("wcf.global.success.add"));this._updateHistory(a.wcfIdentify())}else{var a=(this._successMessageNonVisible)?this._successMessageNonVisible:"wcf.global.success.add";this._notification.show(undefined,5000,WCF.Language.get(a))}if($.browser.mobile){this._messageField.val("")}else{this._messageField.ckeditorGet().setData("")}this._revertQuickReply(true);if(this._quoteManager!==null){this._quoteManager.countQuotes()}}},_failure:function(b){this._revertQuickReply(false);if(b===null||b.returnValues===undefined||b.returnValues.errorType===undefined){return true}var a=this._container.find(".messageQuickReplyContent .messageBody");var c=a.children("small.innerError").empty();if(!c.length){c=$('<small class="innerError" />').appendTo(a)}c.html(b.returnValues.errorType);return false},_getClassName:function(){return""},_getObjectID:function(){return 0},_updateHistory:function(a){window.location.hash=a}});WCF.Message.InlineEditor=Class.extend({_activeElementID:"",_cache:"",_container:{},_containerID:0,_dropdowns:{},_messageContainerSelector:".jsMessage",_messageEditorIDPrefix:"messageEditor",_notification:null,_proxy:null,_supportExtendedForm:false,init:function(a,b){this._activeElementID="";this._cache="";this._container={};this._containerID=parseInt(a);this._dropdowns={};this._supportExtendedForm=(b)?true:false;this._proxy=new WCF.Action.Proxy({failure:$.proxy(this._failure,this),showLoadingOverlay:false,success:$.proxy(this._success,this)});this._notification=new WCF.System.Notification(WCF.Language.get("wcf.global.success.edit"));this.initContainers();WCF.DOMNodeInsertedHandler.addCallback("WCF.Message.InlineEditor",$.proxy(this.initContainers,this))},initContainers:function(){$(this._messageContainerSelector).each($.proxy(function(b,a){var d=$(a);var c=d.wcfIdentify();if(!this._container[c]){this._container[c]=d;if(d.data("canEditInline")){d.find(".jsMessageEditButton:eq(0)").data("containerID",c).click($.proxy(this._clickInline,this)).dblclick($.proxy(this._click,this))}else{if(d.data("canEdit")){d.find(".jsMessageEditButton:eq(0)").data("containerID",c).click($.proxy(this._click,this))}}}},this))},_click:function(c,a){var b=(c===null)?a:$(c.currentTarget).data("containerID");if(this._activeElementID===""){this._activeElementID=b;this._prepare();this._proxy.setOption("data",{actionName:"beginEdit",className:this._getClassName(),interfaceName:"wcf\\data\\IMessageInlineEditorAction",parameters:{containerID:this._containerID,objectID:this._container[b].data("objectID")}});this._proxy.sendRequest()}else{var d=new WCF.System.Notification(WCF.Language.get("wcf.message.error.editorAlreadyInUse"),"warning");d.show()}if(c!==null){c.stopPropagation();return false}},_clickInline:function(c){var d=$(c.currentTarget);if(!d.hasClass("dropdownToggle")){var b=d.data("containerID");WCF.DOMNodeInsertedHandler.enable();d.addClass("dropdownToggle").parent().addClass("dropdown");var a=$('<ul class="dropdownMenu" />').insertAfter(d);this._initDropdownMenu(b,a);WCF.DOMNodeInsertedHandler.disable();this._dropdowns[this._container[b].data("objectID")]=a;WCF.Dropdown.registerCallback(d.parent().wcfIdentify(),$.proxy(this._toggleDropdown,this));d.trigger("click")}c.stopPropagation();return false},_failure:function(b){this._revertEditor();if(b===null||b.returnValues===undefined||b.returnValues.errorType===undefined){return true}var a=this._container[this._activeElementID].find(".messageBody .messageInlineEditor");var c=a.children("small.innerError").empty();if(!c.length){c=$('<small class="innerError" />').insertBefore(a.children(".formSubmit"))}c.html(b.returnValues.errorType);return false},_toggleDropdown:function(b,a){b.parents(".messageOptions").toggleClass("forceOpen")},_initDropdownMenu:function(a,b){},_prepare:function(){var b=this._container[this._activeElementID].find(".messageBody");$('<span class="icon icon48 icon-spinner" />').appendTo(b);var a=b.find(".messageText");this._cache=a.html();a.empty()},_cancel:function(){var d=this._container[this._activeElementID];try{var a=$("#"+this._messageEditorIDPrefix+d.data("objectID")).ckeditorGet();a.destroy()}catch(c){}var b=d.find(".messageBody");b.children(".icon-spinner").remove();b.find(".messageText").html(this._cache);this._container[this._activeElementID].find(".messageOptions").removeClass("forceHidden");this._activeElementID=""},_success:function(b,c,a){switch(b.returnValues.actionName){case"beginEdit":this._showEditor(b);break;case"save":this._showMessage(b);break}},_showEditor:function(e){var c=this._container[this._activeElementID].find(".messageBody");c.children(".icon-spinner").remove();var b=c.find(".messageText");$(""+e.returnValues.template).appendTo(b);var a=b.find(".formSubmit");var d=a.find("button[data-type=save]").click($.proxy(this._save,this));if(this._supportExtendedForm){a.find("button[data-type=extended]").click($.proxy(this._prepareExtended,this))}a.find("button[data-type=cancel]").click($.proxy(this._cancel,this));WCF.Message.Submit.registerButton(this._messageEditorIDPrefix+this._container[this._activeElementID].data("objectID"),d);this._container[this._activeElementID].find(".messageOptions").addClass("forceHidden");new WCF.PeriodicalExecuter($.proxy(function(f){f.stop();$("#"+this._messageEditorIDPrefix+this._container[this._activeElementID].data("objectID")).ckeditorGet().ui.editor.focus()},this),250)},_revertEditor:function(){var a=this._container[this._activeElementID].find(".messageBody");a.children("span.icon-spinner").remove();a.find(".messageText").children().show()},_save:function(){var d=this._container[this._activeElementID];var c=d.data("objectID");var b="";if($.browser.mobile){b=$("#"+this._messageEditorIDPrefix+c).val()}else{var a=$("#"+this._messageEditorIDPrefix+c).ckeditorGet();b=a.getData()}this._proxy.setOption("data",{actionName:"save",className:this._getClassName(),interfaceName:"wcf\\data\\IMessageInlineEditorAction",parameters:{containerID:this._containerID,data:{message:b},objectID:c}});this._proxy.sendRequest();this._hideEditor()},_prepareExtended:function(){var d=this._container[this._activeElementID];var c=d.data("objectID");var b="";if($.browser.mobile){b=$("#"+this._messageEditorIDPrefix+c).val()}else{var a=$("#"+this._messageEditorIDPrefix+c).ckeditorGet();b=a.getData()}new WCF.Action.Proxy({autoSend:true,data:{actionName:"jumpToExtended",className:this._getClassName(),parameters:{containerID:this._containerID,message:b,messageID:c}},success:function(f,g,e){window.location=f.returnValues.url}})},_hideEditor:function(){var a=this._container[this._activeElementID].find(".messageBody");$('<span class="icon icon48 icon-spinner" />').appendTo(a);a.find(".messageText").children().hide()},_showMessage:function(d){var e=this._container[this._activeElementID];var c=e.find(".messageBody");c.children(".icon-spinner").remove();var b=c.find(".messageText");this._container[this._activeElementID].find(".messageOptions").removeClass("forceHidden");if(!$.browser.mobile){var a=$("#"+this._messageEditorIDPrefix+e.data("objectID")).ckeditorGet();a.destroy()}b.empty();b.html(d.returnValues.message);this._activeElementID="";this._updateHistory(this._getHash(e.data("objectID")));this._notification.show()},_getClassName:function(){return""},_getHash:function(a){return"#message"+a},_updateHistory:function(a){window.location.hash=a}});WCF.Message.Submit={_buttons:{},registerButton:function(b,a){if(!WCF.Browser.isChrome()){return}this._buttons[b]=$(a)},execute:function(a){if(!this._buttons[a]){return}this._buttons[a].trigger("click")}};WCF.Message.Quote={};WCF.Message.Quote.Handler=Class.extend({_activeContainerID:"",_className:"",_containers:{},_containerSelector:"",_copyQuote:null,_message:"",_messageBodySelector:"",_objectID:0,_objectType:"",_proxy:null,_quoteManager:null,init:function(e,d,b,a,c,f){this._className=d;if(this._className==""){console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");return}this._objectType=b;if(this._objectType==""){console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");return}this._containerSelector=a;this._message="";this._messageBodySelector=c;this._messageContentSelector=f;this._objectID=0;this._proxy=new WCF.Action.Proxy({success:$.proxy(this._success,this)});this._initContainers();this._initCopyQuote();$(document).mouseup($.proxy(this._mouseUp,this));this._quoteManager=e;this._quoteManager.register(this._objectType,this);WCF.DOMNodeInsertedHandler.addCallback("WCF.Message.Quote.Handler"+b.hashCode(),$.proxy(this._initContainers,this))},_initContainers:function(){var a=this;$(this._containerSelector).each(function(c,b){var e=$(b);var d=e.wcfIdentify();if(!a._containers[d]){a._containers[d]=e;if(e.hasClass("jsInvalidQuoteTarget")){return true}if(a._messageBodySelector!==null){e=e.find(a._messageBodySelector).data("containerID",d)}e.mousedown($.proxy(a._mouseDown,a));a._containers[d].find(".jsQuoteMessage").click($.proxy(a._saveFullQuote,a))}})},_mouseDown:function(a){this._copyQuote.hide();var b=$(a.currentTarget);if(this._messageBodySelector){b=this._containers[b.data("containerID")]}this._activeContainerID=b.wcfIdentify();if($.browser.mozilla){b.find("img").each(function(){var c=$(this);c.data("__alt",c.attr("alt")).removeAttr("alt")})}},_getNodeText:function(d){var c="";for(var b=0;b<d.childNodes.length;b++){if(d.childNodes[b].nodeType==3){c+=d.childNodes[b].nodeValue}else{var a=d.childNodes[b].tagName.toLowerCase();if(a==="li"){c+="\r\n"}c+=this._getNodeText(d.childNodes[b]);if(a==="ul"){c+="\n"}}}return c},_mouseUp:function(a){if(this._activeContainerID==""){this._copyQuote.hide();return}var i=this._containers[this._activeContainerID];var c=this._getSelectedText();var f=$.trim(c);if(f==""){this._copyQuote.hide();return}var d=null;if(this._messageBodySelector){d=this._getNodeText(i.find(this._messageContentSelector).get(0))}else{d=this._getNodeText(i.get(0))}if(this._normalize(d).indexOf(this._normalize(f))===-1){return}this._copyQuote.show();var g=this._getBoundingRectangle(c);var e=this._copyQuote.getDimensions("outer");var b=(g.right-g.left)/2-(e.width/2)+g.left;this._copyQuote.css({top:g.top-e.height-7+"px",left:b+"px"});this._copyQuote.hide();this._activeContainerID="";var h=this;new WCF.PeriodicalExecuter(function(j){j.stop();var k=$.trim(h._getSelectedText());if(k!=""){h._copyQuote.show();h._message=k;h._objectID=i.data("objectID");if($.browser.mozilla){i.find("img").each(function(){var l=$(this);l.attr("alt",l.data("__alt"))})}}},10)},_normalize:function(a){return a.replace(/\r?\n|\r/g,"\n").replace(/\s{2,}/g," ")},_getOffset:function(c,e){c.collapse(e);var g=WCF.getRandomID();var a=document.createElement("span");a.innerHTML='<span id="'+g+'"></span>';var h=document.createDocumentFragment(),b,d;while(b=a.firstChild){d=h.appendChild(b)}c.insertNode(h);a=$("#"+g);var f=a.offset();f.top=f.top-$(window).scrollTop();a.remove();return f},_getBoundingRectangle:function(g){var i=null;if(document.createRange&&typeof document.createRange().getBoundingClientRect!="undefined"){if(g.rangeCount>0){var h=g.getRangeAt(0).getClientRects();if(!$.browser.mozilla&&h.length>1){var d=g.getRangeAt(0);var b=this._getOffset(d,true);var d=g.getRangeAt(0);var a=this._getOffset(d,false);var e={left:(b.left>a.left)?a.left:b.left,right:(b.left>a.left)?b.left:a.left,top:(b.top>a.top)?a.top:b.top}}else{var e=g.getRangeAt(0).getBoundingClientRect()}var c=$(document);var f=c.scrollTop();i={left:e.left,right:e.right,top:e.top+f}}}else{if(document.selection&&document.selection.type!="Control"){var d=document.selection.createRange();i={left:d.boundingLeft,right:d.boundingRight,top:d.boundingTop}}}return i},_initCopyQuote:function(){this._copyQuote=$("#quoteManagerCopy");if(!this._copyQuote.length){this._copyQuote=$('<div id="quoteManagerCopy" class="balloonTooltip"><span>'+WCF.Language.get("wcf.message.quote.quoteSelected")+'</span><span class="pointer"><span></span></span></div>').hide().appendTo(document.body);this._copyQuote.click($.proxy(this._saveQuote,this))}},_getSelectedText:function(){if(window.getSelection){return window.getSelection()}else{if(document.getSelection){return document.getSelection()}else{if(document.selection){return document.selection.createRange().text}}}return""},_saveFullQuote:function(b){var a=$(b.currentTarget);this._proxy.setOption("data",{actionName:"saveFullQuote",className:this._className,interfaceName:"wcf\\data\\IMessageQuoteAction",objectIDs:[a.data("objectID")]});this._proxy.sendRequest();if(a.data("isQuoted")){a.data("isQuoted",false).children("a").removeClass("active")}else{a.data("isQuoted",true).children("a").addClass("active")}b.stopPropagation();return false},_saveQuote:function(){this._proxy.setOption("data",{actionName:"saveQuote",className:this._className,interfaceName:"wcf\\data\\IMessageQuoteAction",objectIDs:[this._objectID],parameters:{message:this._message}});this._proxy.sendRequest()},_success:function(c,d,b){if(c.returnValues.count!==undefined){var a=(c.fullQuoteObjectIDs!==undefined)?c.fullQuoteObjectIDs:{};this._quoteManager.updateCount(c.returnValues.count,a)}},updateFullQuoteObjectIDs:function(b){for(var a in this._containers){this._containers[a].find(".jsQuoteMessage").each(function(c,d){var e=$(d).data("isQuoted",0);e.children("a").removeClass("active");if(WCF.inArray(e.data("objectID"),b)){e.data("isQuoted",1).children("a").addClass("active")}})}}});WCF.Message.Quote.Manager=Class.extend({_buttons:{},_ckEditor:null,_count:0,_dialog:null,_form:null,_handlers:{},_hasTemplate:false,_insertQuotes:true,_proxy:null,_removeOnSubmit:[],_showQuotes:null,_supportPaste:false,init:function(c,b,a,d){this._buttons={insert:null,remove:null};this._ckEditor=null;this._count=parseInt(c)||0;this._dialog=null;this._form=null;this._handlers={};this._hasTemplate=false;this._insertQuotes=true;this._removeOnSubmit=[];this._showQuotes=null;this._supportPaste=false;if(b){this._ckEditor=$("#"+b);if(this._ckEditor.length){this._supportPaste=true;this._form=this._ckEditor.parents("form:eq(0)");if(this._form.length){this._form.submit($.proxy(this._submit,this));this._removeOnSubmit=d||[]}else{this._form=null;this._supportPaste=(a===true)?true:false}}}this._proxy=new WCF.Action.Proxy({showLoadingOverlay:false,success:$.proxy(this._success,this),url:"index.php/MessageQuote/?t="+SECURITY_TOKEN+SID_ARG_2ND});this._toggleShowQuotes()},register:function(a,b){this._handlers[a]=b},updateCount:function(c,b){this._count=parseInt(c)||0;this._toggleShowQuotes();for(var a in this._handlers){if(b[a]){this._handlers[a].updateFullQuoteObjectIDs(b[a])}}},insertQuotes:function(a,b,c){if(!this._insertQuotes){this._insertQuotes=true;return}new WCF.Action.Proxy({autoSend:true,data:{actionName:"getRenderedQuotes",className:a,interfaceName:"wcf\\data\\IMessageQuoteAction",parameters:{parentObjectID:b}},success:c})},_toggleShowQuotes:function(){if(!this._count){if(this._showQuotes!==null){this._showQuotes.hide()}}else{if(this._showQuotes===null){this._showQuotes=$("#showQuotes");if(!this._showQuotes.length){this._showQuotes=$('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click,this)).appendTo(document.body)}}var a=WCF.Language.get("wcf.message.quote.showQuotes").replace(/#count#/,this._count);this._showQuotes.text(a).show()}this._hasTemplate=false},_click:function(){if(this._hasTemplate){this._dialog.wcfDialog("open")}else{this._proxy.showLoadingOverlayOnce();this._proxy.setOption("data",{actionName:"getQuotes",supportPaste:this._supportPaste});this._proxy.sendRequest()}},renderDialog:function(c){if(this._dialog===null){this._dialog=$("#messageQuoteList");if(!this._dialog.length){this._dialog=$('<div id="messageQuoteList" />').hide().appendTo(document.body)}}this._dialog.html(c);var a=$('<div class="formSubmit" />').appendTo(this._dialog);if(this._supportPaste){this._buttons.insert=$("<button>"+WCF.Language.get("wcf.message.quote.insertAllQuotes")+"</button>").click($.proxy(this._insertSelected,this)).appendTo(a)}this._buttons.remove=$("<button>"+WCF.Language.get("wcf.message.quote.removeAllQuotes")+"</button>").click($.proxy(this._removeSelected,this)).appendTo(a);this._dialog.wcfDialog({title:WCF.Language.get("wcf.message.quote.manageQuotes")});this._dialog.wcfDialog("render");this._hasTemplate=true;var d=this._dialog.find(".jsInsertQuote");if(this._supportPaste){d.click($.proxy(this._insertQuote,this))}else{d.hide()}this._dialog.find("input.jsCheckbox").change($.proxy(this._changeButtons,this));if(this._removeOnSubmit.length){var b=this;this._dialog.find("input.jsRemoveQuote").each(function(f,e){var g=$(e).change($.proxy(this._change,this));if(WCF.inArray(g.parent("li").attr("data-quote-id"),b._removeOnSubmit)){g.attr("checked","checked")}})}},_changeButtons:function(){if(this._dialog.find("input.jsCheckbox:checked").length){if(this._supportPaste){this._buttons.insert.html(WCF.Language.get("wcf.message.quote.insertSelectedQuotes"))}this._buttons.remove.html(WCF.Language.get("wcf.message.quote.removeSelectedQuotes"))}else{if(this._supportPaste){this._buttons.insert.html(WCF.Language.get("wcf.message.quote.insertAllQuotes"))}this._buttons.remove.html(WCF.Language.get("wcf.message.quote.removeAllQuotes"))}},_change:function(c){var d=$(c.currentTarget);var b=d.parent("li").attr("data-quote-id");if(d.prop("checked")){this._removeOnSubmit.push(b)}else{for(var a in this._removeOnSubmit){if(this._removeOnSubmit[a]==b){delete this._removeOnSubmit[a];break}}}},_insertSelected:function(){var a=$(".jsQuickReply:eq(0)").data("__api");if(a&&!a.getContainer().is(":visible")){this._insertQuotes=false;a.click(null)}if(!this._dialog.find("input.jsCheckbox:checked").length){this._dialog.find("input.jsCheckbox").prop("checked","checked")}this._dialog.find("input.jsCheckbox:checked").each($.proxy(function(c,b){this._insertQuote(null,b)},this));this._dialog.wcfDialog("close")},_insertQuote:function(a,i){if(a!==null){var c=$(".jsQuickReply:eq(0)").data("__api");if(c&&!c.getContainer().is(":visible")){this._insertQuotes=false;c.click(null)}}var g=(a===null)?$(i).parents("li"):$(a.currentTarget).parents("li");var j=$.trim(g.children("div.jsFullQuote").text());var e=g.parents("article.message");j="[quote='"+e.attr("data-username")+"','"+e.data("link")+"']"+j+"[/quote]";var f=($.browser.mobile)?null:this._ckEditor.ckeditorGet();if(f!==null&&f.mode==="wysiwyg"){f.insertText(j+"\n\n")}else{var d=($.browser.mobile)?this._ckEditor:this._ckEditor.next(".cke_editor_text").find("textarea");var h=d.val();j+="\n\n";if(h.length==0){d.val(j)}else{var b=d.getCaret();d.val(h.substr(0,b)+j+h.substr(b))}}this._removeOnSubmit.push(g.attr("data-quote-id"));if(a!==null){this._dialog.wcfDialog("close")}},_removeSelected:function(){if(!this._dialog.find("input.jsCheckbox:checked").length){this._dialog.find("input.jsCheckbox").prop("checked","checked")}var b=[];this._dialog.find("input.jsCheckbox:checked").each(function(e,d){b.push($(d).parents("li").attr("data-quote-id"))});if(b.length){var c=[];for(var a in this._handlers){c.push(a)}this._proxy.setOption("data",{actionName:"remove",objectTypes:c,quoteIDs:b});this._proxy.sendRequest();this._dialog.wcfDialog("close")}},_submit:function(){if(this._supportPaste&&this._removeOnSubmit.length>0){var a=this._form.find(".formSubmit");for(var b in this._removeOnSubmit){$('<input type="hidden" name="__removeQuoteIDs[]" value="'+this._removeOnSubmit[b]+'" />').appendTo(a)}}},getQuotesMarkedForRemoval:function(){return this._removeOnSubmit},markQuotesForRemoval:function(){if(this._removeOnSubmit.length){this._proxy.setOption("data",{actionName:"markForRemoval",quoteIDs:this._removeOnSubmit});this._proxy.sendRequest()}},removeMarkedQuotes:function(){if(this._removeOnSubmit.length){this._proxy.setOption("data",{actionName:"removeMarkedQuotes"});this._proxy.sendRequest()}},countQuotes:function(){var b=[];for(var a in this._handlers){b.push(a)}this._proxy.setOption("data",{actionName:"count",objectTypes:b});this._proxy.sendRequest()},_success:function(c,d,b){if(c===null){return}if(c.count!==undefined){var a=(c.fullQuoteObjectIDs!==undefined)?c.fullQuoteObjectIDs:{};this.updateCount(c.count,a)}if(c.template!==undefined){if($.trim(c.template)==""){this.updateCount(0,{})}else{this.renderDialog(c.template)}}}});WCF.Message.Share={};WCF.Message.Share.Content=Class.extend({_cache:{},_dialog:null,init:function(){this._cache={};this._dialog=null;this._initLinks();WCF.DOMNodeInsertedHandler.addCallback("WCF.Message.Share.Content",$.proxy(this._initLinks,this))},_initLinks:function(){$("a.jsButtonShare").removeClass("jsButtonShare").click($.proxy(this._click,this))},_click:function(e){e.preventDefault();var a=$(e.currentTarget);var b=a.prop("href");var d=(a.data("linkTitle")?a.data("linkTitle"):b);var c=b.hashCode();if(this._cache[c]===undefined){var g=false;if(this._dialog===null){this._dialog=$("<div />").hide().appendTo(document.body);g=true}else{this._dialog.empty()}var f=$('<fieldset><legend><label for="__sharePermalink">'+WCF.Language.get("wcf.message.share.permalink")+"</label></legend></fieldset>").appendTo(this._dialog);$('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr("value",b).appendTo(f);var f=$('<fieldset><legend><label for="__sharePermalinkBBCode">'+WCF.Language.get("wcf.message.share.permalink.bbcode")+"</label></legend></fieldset>").appendTo(this._dialog);$('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr("value","[url='"+b+"']"+d+"[/url]").appendTo(f);var f=$('<fieldset><legend><label for="__sharePermalinkHTML">'+WCF.Language.get("wcf.message.share.permalink.html")+"</label></legend></fieldset>").appendTo(this._dialog);$('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr("value",'<a href="'+b+'">'+WCF.String.escapeHTML(d)+"</a>").appendTo(f);this._cache[c]=this._dialog.html();if(g){this._dialog.wcfDialog({title:WCF.Language.get("wcf.message.share")})}else{this._dialog.wcfDialog("open")}}else{this._dialog.html(this._cache[c]).wcfDialog("open")}this._dialog.find("input").click(function(){$(this).select()})}});WCF.Message.Share.Page=Class.extend({_ui:{},_pageDescription:"",_pageURL:"",init:function(a){this._pageDescription=encodeURIComponent($('meta[property="og:description"]').prop("content"));this._pageURL=encodeURIComponent($('meta[property="og:url"]').prop("content"));var b=$(".messageShareButtons");this._ui={facebook:b.find(".jsShareFacebook"),google:b.find(".jsShareGoogle"),reddit:b.find(".jsShareReddit"),twitter:b.find(".jsShareTwitter")};this._ui.facebook.children("a").click($.proxy(this._shareFacebook,this));this._ui.google.children("a").click($.proxy(this._shareGoogle,this));this._ui.reddit.children("a").click($.proxy(this._shareReddit,this));this._ui.twitter.children("a").click($.proxy(this._shareTwitter,this));if(a===true){this._fetchFacebook();this._fetchTwitter();this._fetchReddit()}},_share:function(b,a){window.open(a.replace(/{pageURL}/,this._pageURL).replace(/{text}/,this._pageDescription),"height=600,width=600")},_shareFacebook:function(){this._share("facebook","https://www.facebook.com/sharer.php?u={pageURL}&t={text}")},_shareGoogle:function(){this._share("google","https://plus.google.com/share?url={pageURL}")},_shareReddit:function(){this._share("reddit","https://ssl.reddit.com/submit?url={pageURL}")},_shareTwitter:function(){this._share("twitter","https://twitter.com/share?url={pageURL}&text={text}")},_fetchCount:function(b,d,c){var a={autoSend:true,dataType:"jsonp",showLoadingOverlay:false,success:d,suppressErrors:true,type:"GET",url:b.replace(/{pageURL}/,this._pageURL)};if(c){a.jsonp=c}new WCF.Action.Proxy(a)},_fetchFacebook:function(){this._fetchCount("https://graph.facebook.com/?id={pageURL}",$.proxy(function(a){if(a.shares){this._ui.facebook.children("span.badge").show().text(a.shares)}},this))},_fetchTwitter:function(){this._fetchCount("http://urls.api.twitter.com/1/urls/count.json?url={pageURL}",$.proxy(function(a){if(a.count){this._ui.twitter.children("span.badge").show().text(a.count)}},this))},_fetchReddit:function(){this._fetchCount("http://www.reddit.com/api/info.json?url={pageURL}",$.proxy(function(a){if(a.data.children.length){this._ui.reddit.children("span.badge").show().text(a.data.children[0].data.score)}},this),"jsonp")}});
\ No newline at end of file
diff --git a/wcfsetup/install/files/lib/action/MessageQuoteAction.class.php b/wcfsetup/install/files/lib/action/MessageQuoteAction.class.php
new file mode 100644 (file)
index 0000000..2eda385
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+namespace wcf\action;
+use wcf\system\exception\SystemException;
+use wcf\system\exception\UserInputException;
+use wcf\system\message\quote\MessageQuoteManager;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\JSON;
+use wcf\util\StringUtil;
+
+/**
+ * Handles message quotes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage action
+ * @category   Community Framework
+ */
+class MessageQuoteAction extends AJAXProxyAction {
+       /**
+        * list of quote ids
+        * @var array<string>
+        */
+       public $quoteIDs = array();
+       
+       /**
+        * list of object types
+        * @var array<string>
+        */
+       public $objectTypes = array();
+       
+       /**
+        * @see wcf\action\IAction::readParameters()
+        */
+       public function readParameters() {
+               AbstractSecureAction::readParameters();
+               
+               if (isset($_POST['actionName'])) $this->actionName = StringUtil::trim($_POST['actionName']);
+               if (isset($_POST['objectTypes']) && is_array($_POST['objectTypes'])) $this->objectTypes = ArrayUtil::trim($_POST['objectTypes']); 
+               if (isset($_POST['quoteIDs'])) {
+                       $this->quoteIDs = ArrayUtil::trim($_POST['quoteIDs']);
+                       
+                       // validate quote ids
+                       foreach ($this->quoteIDs as $key => $quoteID) {
+                               if (MessageQuoteManager::getInstance()->getQuote($quoteID) === null) {
+                                       unset($this->quoteIDs[$key]);
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * @see wcf\action\IAction::execute()
+        */
+       public function execute() {
+               AbstractAction::execute();
+               
+               $returnValues = null;
+               switch ($this->actionName) {
+                       case 'count':
+                               $returnValues = array(
+                                       'count' => $this->count(),
+                                       'fullQuoteObjectIDs' => $this->getFullQuoteObjectIDs()
+                               );
+                       break;
+                       
+                       case 'getQuotes':
+                               $returnValues = array('template' => $this->getQuotes());
+                       break;
+                       
+                       case 'markForRemoval':
+                               $this->markForRemoval();
+                       break;
+                       
+                       case 'remove':
+                               $returnValues = array(
+                                       'count' => $this->remove(),
+                                       'fullQuoteObjectIDs' => $this->getFullQuoteObjectIDs()
+                               );
+                       break;
+                       
+                       case 'removeMarkedQuotes':
+                               $returnValues = array(
+                                       'count' => $this->removeMarkedQuotes(),
+                                       'fullQuoteObjectIDs' => $this->getFullQuoteObjectIDs()
+                               );
+                       break;
+                       
+                       default:
+                               throw new SystemException("Unknown action '".$this->actionName."'");
+                       break;
+               }
+               
+               $this->executed();
+               
+               // force session update
+               WCF::getSession()->update();
+               WCF::getSession()->disableUpdate();
+               
+               if ($returnValues !== null) {
+                       // send JSON-encoded response
+                       header('Content-type: application/json');
+                       echo JSON::encode($returnValues);
+               }
+               
+               exit;
+       }
+       
+       /**
+        * Returns the count of stored quotes.
+        * 
+        * @return      integer
+        */
+       protected function count() {
+               return MessageQuoteManager::getInstance()->countQuotes();
+       }
+       
+       /**
+        * Returns the quote list template.
+        * 
+        * @return      string
+        */
+       protected function getQuotes() {
+               $supportPaste = (isset($_POST['supportPaste'])) ? (bool)$_POST['supportPaste'] : false;
+               
+               return MessageQuoteManager::getInstance()->getQuotes($supportPaste);
+       }
+       
+       /**
+        * Marks quotes for removal.
+        */
+       protected function markForRemoval() {
+               if (!empty($this->quoteIDs)) {
+                       MessageQuoteManager::getInstance()->markQuotesForRemoval($this->quoteIDs);
+               }
+       }
+       
+       /**
+        * Removes a list of quotes from storage and returns the remaining count.
+        * 
+        * @return      integer
+        */
+       protected function remove() {
+               if (empty($this->quoteIDs)) {
+                       throw new UserInputException('quoteIDs');
+               }
+               
+               foreach ($this->quoteIDs as $quoteID) {
+                       if (!MessageQuoteManager::getInstance()->removeQuote($quoteID)) {
+                               throw new SystemException("Unable to remove quote identified by '".$quoteID."'");
+                       }
+               }
+               
+               return $this->count();
+       }
+       
+       /**
+        * Removes all quotes marked for removal and returns the remaining count.
+        * 
+        * @return      integer
+        */
+       protected function removeMarkedQuotes() {
+               MessageQuoteManager::getInstance()->removeMarkedQuotes();
+               
+               return $this->count();
+       }
+       
+       /**
+        * Returns a list of full quotes by object ids for given object types.
+        * 
+        * @return      array<array>
+        */
+       protected function getFullQuoteObjectIDs() {
+               if (empty($this->objectTypes)) {
+                       throw new UserInputException('objectTypes');
+               }
+               
+               try {
+                       return MessageQuoteManager::getInstance()->getFullQuoteObjectIDs($this->objectTypes);
+               }
+               catch (SystemException $e) {
+                       throw new UserInputException('objectTypes');
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/IExtendedMessageQuickReplyAction.class.php b/wcfsetup/install/files/lib/data/IExtendedMessageQuickReplyAction.class.php
new file mode 100644 (file)
index 0000000..f222198
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Default interface for actions implementing quick reply with extended mode.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IExtendedMessageQuickReplyAction extends IMessageQuickReplyAction {
+       /**
+        * Saves message and jumps to extended mode.
+        * 
+        * @return      array
+        */
+       public function jumpToExtended();
+       
+       /**
+        * Validates parameters to jump to extended mode.
+        */
+       public function validateJumpToExtended();
+}
diff --git a/wcfsetup/install/files/lib/data/IFeedEntry.class.php b/wcfsetup/install/files/lib/data/IFeedEntry.class.php
new file mode 100644 (file)
index 0000000..519a56c
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every feed entry should implement this interface.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IFeedEntry extends IMessage {
+       /**
+        * Returns the number of comments.
+        * 
+        * @return      integer
+        */
+       public function getComments();
+       
+       /**
+        * Returns a list of category names.
+        * 
+        * @return      array<string>
+        */
+       public function getCategories();
+}
diff --git a/wcfsetup/install/files/lib/data/IMessage.class.php b/wcfsetup/install/files/lib/data/IMessage.class.php
new file mode 100644 (file)
index 0000000..1036b49
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Default interface for message database objects.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IMessage extends IUserContent {
+       /**
+        * Returns a simplified message (only inline codes), truncated to 255 characters by default.
+        * 
+        * @param       integer         $maxLength
+        * @return      string
+        */
+       public function getExcerpt($maxLength = 255);
+       
+       /**
+        * Returns formatted message text.
+        * 
+        * @return      string
+        */
+       public function getFormattedMessage();
+       
+       /**
+        * Returns message text.
+        * 
+        * @return      string
+        */
+       public function getMessage();
+       
+       /**
+        * Returns true, if message is visible for current user.
+        * 
+        * @return      boolean
+        */
+       public function isVisible();
+       
+       /**
+        * Returns formatted message text.
+        * 
+        * @see wcf\data\IMessage::getFormattedMessage()
+        */
+       public function __toString();
+}
diff --git a/wcfsetup/install/files/lib/data/IMessageInlineEditorAction.class.php b/wcfsetup/install/files/lib/data/IMessageInlineEditorAction.class.php
new file mode 100644 (file)
index 0000000..cc60a42
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Default interface for actions implementing message inline editing.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IMessageInlineEditorAction {
+       /**
+        * Provides WYSIWYG editor for message inline editing.
+        * 
+        * @return      array
+        */
+       public function beginEdit();
+       
+       /**
+        * Saves changes made to a message.
+        * 
+        * @return      array
+        */
+       public function save();
+       
+       /**
+        * Validates parameters to begin message inline editing.
+        */
+       public function validateBeginEdit();
+       
+       /**
+        * Validates parameters to save changes made to a message.
+        */
+       public function validateSave();
+}
diff --git a/wcfsetup/install/files/lib/data/IMessageQuickReplyAction.class.php b/wcfsetup/install/files/lib/data/IMessageQuickReplyAction.class.php
new file mode 100644 (file)
index 0000000..3029a82
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Default interface for actions implementing quick reply.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IMessageQuickReplyAction {
+       /**
+        * Creates a new message object.
+        * 
+        * @return      wcf\data\DatabaseObject
+        */
+       public function create();
+       
+       /**
+        * Returns a message list object.
+        * 
+        * @param       wcf\data\DatabaseObject         $container
+        * @param       integer                         $lastMessageTime
+        * @return      wcf\data\DatabaseObjectList
+        */
+       public function getMessageList(DatabaseObject $container, $lastMessageTime);
+       
+       /**
+        * Returns page no for given container object.
+        * 
+        * @param       wcf\data\DatabaseObject         $container
+        * @return      array
+        */
+       public function getPageNo(DatabaseObject $container);
+       
+       /**
+        * Returns the redirect url.
+        * 
+        * @param       wcf\data\DatabaseObject         $container
+        * @param       wcf\data\DatabaseObject         $message
+        * @return      string
+        */
+       public function getRedirectUrl(DatabaseObject $container, DatabaseObject $message);
+       
+       /**
+        * Validates the message.
+        * 
+        * @param       wcf\data\DatabaseObject         $container
+        * @param       string                          $message
+        */
+       public function validateMessage(DatabaseObject $container, $message);
+       
+       /**
+        * Creates a new message and returns it.
+        *
+        * @return      array
+        */
+       public function quickReply();
+       
+       /**
+        * Validates the container object for quick reply.
+        *
+        * @param       wcf\data\DatabaseObject         $container
+        */
+       public function validateContainer(DatabaseObject $container);
+       
+       /**
+        * Validates parameters for quick reply.
+        */
+       public function validateQuickReply();
+}
diff --git a/wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php b/wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php
new file mode 100644 (file)
index 0000000..62c60ae
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Default interface for message action classes supporting quotes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data
+ * @category   Community Framework
+ */
+interface IMessageQuoteAction {
+       /**
+        * Validates parameters to return a parsed template of all associated quotes.
+        */
+       public function validateGetRenderedQuotes();
+       
+       /**
+        * Returns the parsed template for all associated quotes.
+        * 
+        * @return      array
+        */
+       public function getRenderedQuotes();
+       
+       /**
+        * Validates parameters to quote an entire message.
+        */
+       public function validateSaveFullQuote();
+       
+       /**
+        * Quotes an entire message.
+        * 
+        * @return      array
+        */
+       public function saveFullQuote();
+       
+       /**
+        * Validates parameters to save a quote.
+        */
+       public function validateSaveQuote();
+       
+       /**
+        * Saves the quote message and returns the number of stored quotes.
+        * 
+        * @return      array
+        */
+       public function saveQuote();
+}
diff --git a/wcfsetup/install/files/lib/data/bbcode/MessagePreviewAction.class.php b/wcfsetup/install/files/lib/data/bbcode/MessagePreviewAction.class.php
new file mode 100644 (file)
index 0000000..c62d29a
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+namespace wcf\data\bbcode;
+use wcf\data\attachment\GroupedAttachmentList;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\bbcode\AttachmentBBCode;
+use wcf\system\bbcode\MessageParser;
+use wcf\system\bbcode\PreParser;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Provides a default message preview action.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage data.message
+ * @category   Community Framework
+ */
+class MessagePreviewAction extends BBCodeAction {
+       /**
+        * @see wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
+        */
+       protected $allowGuestAccess = array('getMessagePreview');
+               
+       /**
+        * Validates parameters for message preview.
+        */
+       public function validateGetMessagePreview() {
+               if (!isset($this->parameters['data']['message'])) {
+                       throw new UserInputException('message');
+               }
+               
+               if (!isset($this->parameters['options'])) {
+                       throw new UserInputException('options');
+               }
+       }
+       
+       /**
+        * Returns a rendered message preview.
+        *
+        * @return      array
+        */
+       public function getMessagePreview() {
+               // get options
+               $enableBBCodes = (isset($this->parameters['options']['enableBBCodes'])) ? 1 : 0;
+               $enableHtml = (isset($this->parameters['options']['enableHtml'])) ? 1 : 0;
+               $enableSmilies = (isset($this->parameters['options']['enableSmilies'])) ? 1 : 0;
+               $preParse = (isset($this->parameters['options']['preParse'])) ? 1 : 0;
+               
+               // validate permissions for options
+               if ($enableBBCodes && !WCF::getSession()->getPermission('user.message.canUseBBCodes')) $enableBBCodes = 0;
+               if ($enableHtml && !WCF::getSession()->getPermission('user.message.canUseHtml')) $enableHtml = 0;
+               if ($enableSmilies && !WCF::getSession()->getPermission('user.message.canUseSmilies')) $enableSmilies = 0;
+               
+               // get attachments
+               if (!empty($this->parameters['attachmentObjectType'])) {
+                       $attachmentList = new GroupedAttachmentList($this->parameters['attachmentObjectType']);
+                       if (!empty($this->parameters['attachmentObjectID'])) {
+                               $attachmentList->getConditionBuilder()->add('attachment.objectID = ?', array($this->parameters['attachmentObjectID']));
+                               AttachmentBBCode::setObjectID($this->parameters['attachmentObjectID']);
+                               
+                               $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $this->parameters['attachmentObjectType']);
+                               $processor = $objectType->getProcessor();
+                               if (!$processor->canDownload($this->parameters['attachmentObjectID']) && !$processor->canViewPreview($this->parameters['attachmentObjectID'])) {
+                                       if (WCF::getUser()->userID) {
+                                               $attachmentList->getConditionBuilder()->add('attachment.userID = ?', array(WCF::getUser()->userID));
+                                       }
+                                       else {
+                                               $attachmentList->getConditionBuilder()->add('attachment.userID IS NULL');
+                                       }
+                               }
+                       }
+                       else {
+                               $attachmentList->getConditionBuilder()->add('attachment.tmpHash = ?', array($this->parameters['tmpHash']));
+                               
+                               if (WCF::getUser()->userID) {
+                                       $attachmentList->getConditionBuilder()->add('attachment.userID = ?', array(WCF::getUser()->userID));
+                               }
+                               else {
+                                       $attachmentList->getConditionBuilder()->add('attachment.userID IS NULL');
+                               }
+                       }
+                       
+                       $attachmentList->readObjects();
+                       AttachmentBBCode::setAttachmentList($attachmentList);
+               }
+               
+               // get message
+               $message = StringUtil::trim($this->parameters['data']['message']);
+               
+               // parse URLs
+               if ($preParse && $enableBBCodes) {
+                       $message = PreParser::getInstance()->parse($message);
+               }
+               
+               // parse message
+               $preview = MessageParser::getInstance()->parse($message, $enableSmilies, $enableHtml, $enableBBCodes, false);
+               
+               return array(
+                       'message' => $preview
+               );
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/smiley/category/SmileyCategoryAction.class.php b/wcfsetup/install/files/lib/data/smiley/category/SmileyCategoryAction.class.php
new file mode 100644 (file)
index 0000000..7476886
--- /dev/null
@@ -0,0 +1,60 @@
+<?php 
+namespace wcf\data\smiley\category;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Executes smiley category-related actions.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.bbcode
+ * @subpackage data.smiley.category
+ * @category   Community Framework
+ */
+class SmileyCategoryAction extends AbstractDatabaseObjectAction {
+       /**
+        * @see wcf\data\AbstractDatabaseObjectAction::$className
+        */
+       protected $className = 'wcf\data\category\CategoryEditor';
+       
+       /**
+        * @see wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
+        */
+       protected $allowGuestAccess = array('getSmilies');
+       
+       /**
+        * active smiley category
+        * @var wcf\data\smiley\category\SmileyCategory
+        */
+       public $smileyCategory = null;
+       
+       /**
+        * Validates smiley category id.
+        */
+       public function validateGetSmilies() {
+               $this->smileyCategory = new SmileyCategory($this->getSingleObject()->getDecoratedObject());
+               
+               if ($this->smileyCategory->isDisabled) throw new IllegalLinkException();
+       }
+       
+       /**
+        * Returns parsed template for smiley category's smilies.
+        * 
+        * @return      array
+        */
+       public function getSmilies() {
+               $this->smileyCategory->loadSmilies();
+               
+               WCF::getTPL()->assign(array(
+                       'smilies' => $this->smileyCategory
+               ));
+               
+               return array(
+                       'smileyCategoryID' => $this->smileyCategory->categoryID,
+                       'template' => WCF::getTPL()->fetch('__messageFormSmilies')
+               );
+       }
+}
diff --git a/wcfsetup/install/files/lib/form/MessageForm.class.php b/wcfsetup/install/files/lib/form/MessageForm.class.php
new file mode 100644 (file)
index 0000000..d029cfd
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+namespace wcf\form;
+use wcf\data\smiley\SmileyCache;
+use wcf\system\attachment\AttachmentHandler;
+use wcf\system\bbcode\BBCodeParser;
+use wcf\system\bbcode\PreParser;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\message\censorship\Censorship;
+use wcf\system\WCF;
+use wcf\util\MessageUtil;
+use wcf\util\StringUtil;
+
+/**
+ * MessageForm is an abstract form implementation for a message with optional captcha suppport.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage form
+ * @category   Community Framework
+ */
+abstract class MessageForm extends RecaptchaForm {
+       /**
+        * name of the permission which contains the allowed BBCodes
+        * @var string
+        */
+       public $allowedBBCodesPermission = 'user.message.allowedBBCodes';
+       
+       /**
+        * attachment handler
+        * @var wcf\system\attachment\AttachmentHandler
+        */
+       public $attachmentHandler = null;
+       
+       /**
+        * object id for attachments
+        * @var integer
+        */
+       public $attachmentObjectID = 0;
+       
+       /**
+        * object type for attachments, if left blank, attachment support is disabled
+        * @var integer
+        */
+       public $attachmentObjectType = '';
+       
+       /**
+        * parent object id for attachments
+        * @var integer
+        */
+       public $attachmentParentObjectID = 0;
+       
+       /**
+        * list of available content languages
+        * @var array<wcf\data\language\Language>
+        */
+       public $availableContentLanguages = array();
+       
+       /**
+        * list of default smilies
+        * @var array<wcf\data\smiley\Smiley>
+        */
+       public $defaultSmilies = array();
+       
+       /**
+        * enables bbcodes
+        * @var boolean
+        */
+       public $enableBBCodes = 1;
+       
+       /**
+        * enables html
+        * @var boolean
+        */
+       public $enableHtml = 0;
+       
+       /**
+        * enables multilingualism
+        * @var boolean
+        */
+       public $enableMultilingualism = false;
+       
+       /**
+        * enables smilies
+        * @var boolean
+        */
+       public $enableSmilies = 1;
+       
+       /**
+        * content language id
+        * @var integer
+        */
+       public $languageID = null;
+       
+       /**
+        * maximum text length
+        * @var integer
+        */
+       public $maxTextLength = 0;
+       
+       /**
+        * pre parses the message
+        * @var boolean
+        */
+       public $preParse = 1;
+       
+       /**
+        * required permission to use BBCodes
+        * @var boolean
+        */
+       public $permissionCanUseBBCodes = 'user.message.canUseBBCodes';
+       
+       /**
+        * required permission to use HTML
+        * @var boolean
+        */
+       public $permissionCanUseHtml = 'user.message.canUseHtml';
+       
+       /**
+        * required permission to use smilies
+        * @var boolean
+        */
+       public $permissionCanUseSmilies = 'user.message.canUseSmilies';
+       
+       /**
+        * shows the signature
+        * @var boolean
+        */
+       public $showSignature = 0;
+       
+       /**
+        * enables the 'showSignature' setting
+        * @var boolean
+        */
+       public $showSignatureSetting = 1;
+       
+       /**
+        * list of smiley categories
+        * @var array<wcf\data\smiley\category\SmileyCategory>
+        */
+       public $smileyCategories = array();
+       
+       /**
+        * message subject
+        * @var string
+        */
+       public $subject = '';
+       
+       /**
+        * message text
+        * @var string
+        */
+       public $text = '';
+       
+       /**
+        * temp hash
+        * @var string
+        */
+       public $tmpHash = '';
+       
+       /**
+        * @see wcf\form\IPage::readParameters()
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['tmpHash'])) {
+                       $this->tmpHash = $_REQUEST['tmpHash'];
+               }
+               if (empty($this->tmpHash)) {
+                       $this->tmpHash = StringUtil::getRandomID();
+               }
+               
+               if ($this->enableMultilingualism) {
+                       $this->availableContentLanguages = LanguageFactory::getInstance()->getContentLanguages();
+                       if (WCF::getUser()->userID) {
+                               foreach ($this->availableContentLanguages as $key => $value) {
+                                       if (!in_array($key, WCF::getUser()->getLanguageIDs())) unset($this->availableContentLanguages[$key]);
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * @see wcf\form\IForm::readFormParameters()
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               if (isset($_POST['subject'])) $this->subject = StringUtil::trim($_POST['subject']);
+               if (isset($_POST['text'])) $this->text = MessageUtil::stripCrap(StringUtil::trim($_POST['text']));
+               
+               // settings
+               $this->enableSmilies = $this->enableHtml = $this->enableBBCodes = $this->preParse = $this->showSignature = 0;
+               if (isset($_POST['preParse'])) $this->preParse = intval($_POST['preParse']);
+               if (isset($_POST['enableSmilies']) && WCF::getSession()->getPermission($this->permissionCanUseSmilies)) $this->enableSmilies = intval($_POST['enableSmilies']);
+               if (isset($_POST['enableHtml']) && WCF::getSession()->getPermission($this->permissionCanUseHtml)) $this->enableHtml = intval($_POST['enableHtml']);
+               if (isset($_POST['enableBBCodes']) && WCF::getSession()->getPermission($this->permissionCanUseBBCodes)) $this->enableBBCodes = intval($_POST['enableBBCodes']);
+               if (isset($_POST['showSignature'])) $this->showSignature = intval($_POST['showSignature']);
+               
+               // multilingualism
+               if (isset($_POST['languageID'])) $this->languageID = intval($_POST['languageID']);
+       }
+       
+       /**
+        * @see wcf\form\IForm::validate()
+        */
+       public function validate() {
+               // subject
+               $this->validateSubject();
+               
+               // text
+               $this->validateText();
+               
+               // multilingualism
+               $this->validateContentLanguage();
+               
+               parent::validate();
+       }
+       
+       /**
+        * Validates the message subject.
+        */
+       protected function validateSubject() {
+               if (empty($this->subject)) {
+                       throw new UserInputException('subject');
+               }
+               
+               if (StringUtil::length($this->subject) > 255) {
+                       throw new UserInputException('subject', 'tooLong');
+               }
+               
+               // search for censored words
+               if (ENABLE_CENSORSHIP) {
+                       $result = Censorship::getInstance()->test($this->subject);
+                       if ($result) {
+                               WCF::getTPL()->assign('censoredWords', $result);
+                               throw new UserInputException('subject', 'censoredWordsFound');
+                       }
+               }
+       }
+       
+       /**
+        * Validates the message text.
+        */
+       protected function validateText() {
+               if (empty($this->text)) {
+                       throw new UserInputException('text');
+               }
+               
+               // check text length
+               if ($this->maxTextLength != 0 && StringUtil::length($this->text) > $this->maxTextLength) {
+                       throw new UserInputException('text', 'tooLong');
+               }
+               
+               if ($this->enableBBCodes && $this->allowedBBCodesPermission) {
+                       $disallowedBBCodes = BBCodeParser::getInstance()->validateBBCodes($this->text, explode(',', WCF::getSession()->getPermission($this->allowedBBCodesPermission)));
+                       if (!empty($disallowedBBCodes)) {
+                               WCF::getTPL()->assign('disallowedBBCodes', $disallowedBBCodes);
+                               throw new UserInputException('text', 'disallowedBBCodes');
+                       }
+               }
+               
+               // search for censored words
+               if (ENABLE_CENSORSHIP) {
+                       $result = Censorship::getInstance()->test($this->text);
+                       if ($result) {
+                               WCF::getTPL()->assign('censoredWords', $result);
+                               throw new UserInputException('text', 'censoredWordsFound');
+                       }
+               }
+       }
+       
+       /**
+        * Validates content language id.
+        */
+       protected function validateContentLanguage() {
+               if (!$this->languageID || !$this->enableMultilingualism || empty($this->availableContentLanguages)) {
+                       $this->languageID = null;
+                       return;
+               }
+               
+               if (!isset($this->availableContentLanguages[$this->languageID])) {
+                       throw new UserInputException('languageID', 'notValid');
+               }
+       }
+       
+       /**
+        * @see wcf\form\IForm::save()
+        */
+       public function save() {
+               parent::save();
+               
+               // parse URLs
+               if ($this->preParse == 1) {
+                       // BBCodes are enabled
+                       if ($this->enableBBCodes) {
+                               if ($this->allowedBBCodesPermission) {
+                                       $this->text = PreParser::getInstance()->parse($this->text, explode(',', WCF::getSession()->getPermission($this->allowedBBCodesPermission)));
+                               }
+                               else {
+                                       $this->text = PreParser::getInstance()->parse($this->text);
+                               }
+                       }
+                       // BBCodes are disabled, thus no allowed BBCodes
+                       else {
+                               $this->text = PreParser::getInstance()->parse($this->text, array());
+                       }
+               }
+       }
+       
+       /**
+        * @see wcf\page\IPage::readData()
+        */
+       public function readData() {
+               // get attachments
+               if (MODULE_ATTACHMENT && $this->attachmentObjectType) {
+                       $this->attachmentHandler = new AttachmentHandler($this->attachmentObjectType, $this->attachmentObjectID, $this->tmpHash, $this->attachmentParentObjectID);
+               }
+               
+               if (empty($_POST)) {
+                       $this->enableBBCodes = (ENABLE_BBCODES_DEFAULT_VALUE && WCF::getSession()->getPermission($this->permissionCanUseBBCodes)) ? 1 : 0;
+                       $this->enableHtml = (ENABLE_HTML_DEFAULT_VALUE && WCF::getSession()->getPermission($this->permissionCanUseHtml)) ? 1 : 0;
+                       $this->enableSmilies = (ENABLE_SMILIES_DEFAULT_VALUE && WCF::getSession()->getPermission($this->permissionCanUseSmilies)) ? 1 : 0;
+                       $this->preParse = PRE_PARSE_DEFAULT_VALUE;
+                       $this->showSignature = SHOW_SIGNATURE_DEFAULT_VALUE;
+                       $this->languageID = WCF::getLanguage()->languageID;
+               }
+               
+               parent::readData();
+               
+               // get default smilies
+               if (MODULE_SMILEY) {
+                       $this->smileyCategories = SmileyCache::getInstance()->getCategories();
+                       foreach ($this->smileyCategories as $index => $category) {
+                               $category->loadSmilies();
+                               
+                               // remove empty categories
+                               if (!count($category) || $category->isDisabled) {
+                                       unset($this->smileyCategories[$index]);
+                               }
+                       }
+                       
+                       $firstCategory = reset($this->smileyCategories);
+                       if ($firstCategory) {
+                               $this->defaultSmilies = SmileyCache::getInstance()->getCategorySmilies($firstCategory->categoryID ?: null);
+                       }
+               }
+       }
+       
+       /**
+        * @see wcf\page\IPage::assignVariables();
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign(array(
+                       'attachmentHandler' => $this->attachmentHandler,
+                       'attachmentObjectID' => $this->attachmentObjectID,
+                       'attachmentObjectType' => $this->attachmentObjectType,
+                       'attachmentParentObjectID' => $this->attachmentParentObjectID,
+                       'availableContentLanguages' => $this->availableContentLanguages,
+                       'defaultSmilies' => $this->defaultSmilies,
+                       'enableBBCodes' => $this->enableBBCodes,
+                       'enableHtml' => $this->enableHtml,
+                       'enableSmilies' => $this->enableSmilies,
+                       'languageID' => ($this->languageID ?: 0),
+                       'maxTextLength' => $this->maxTextLength,
+                       'preParse' => $this->preParse,
+                       'showSignature' => $this->showSignature,
+                       'showSignatureSetting' => $this->showSignatureSetting,
+                       'smileyCategories' => $this->smileyCategories,
+                       'subject' => $this->subject,
+                       'text' => $this->text,
+                       'tmpHash' => $this->tmpHash
+               ));
+               
+               if ($this->allowedBBCodesPermission) {
+                       WCF::getTPL()->assign('allowedBBCodes', explode(',', WCF::getSession()->getPermission($this->allowedBBCodesPermission)));
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/page/AbstractFeedPage.class.php b/wcfsetup/install/files/lib/page/AbstractFeedPage.class.php
new file mode 100644 (file)
index 0000000..537c0ad
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+namespace wcf\page;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+
+/**
+ * Generates RSS 2-Feeds.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage page
+ * @category   Community Framework
+ */
+abstract class AbstractFeedPage extends AbstractAuthedPage {
+       /**
+        * @see wcf\page\AbstractPage::$templateName
+        */
+       public $templateName = 'rssFeed';
+       
+       /**
+        * application name
+        * @var string
+        */
+       public $application = 'wcf';
+       
+       /**
+        * @see wcf\page\AbstractPage::$useTemplate
+        */
+       public $useTemplate = false;
+       
+       /**
+        * parsed contents of $_REQUEST['id']
+        * @var array<integer>
+        */
+       public $objectIDs = array();
+       
+       /**
+        * list of feed-entries for the current page
+        * @var wcf\data\DatabaseObjectList
+        */
+       public $items = null;
+       
+       /**
+        * feed title
+        * @var string
+        */
+       public $title = '';
+       
+       /**
+        * @see wcf\page\IPage::assignVariables()
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign(array(
+                       'items' => $this->items,
+                       'title' => $this->title
+               ));
+       }
+       
+       /**
+        * @see wcf\page\IPage::readParameters()
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) {
+                       if (is_array($_REQUEST['id'])) {
+                               // ?id[]=1337&id[]=9001
+                               $this->objectIDs = ArrayUtil::toIntegerArray($_REQUEST['id']);
+                       }
+                       else {
+                               // ?id=1337 or ?id=1337,9001
+                               $this->objectIDs = ArrayUtil::toIntegerArray(explode(',', $_REQUEST['id']));
+                       }
+               }
+       }
+       
+       /**
+        * @see wcf\page\IPage::show()
+        */
+       public function show() {
+               parent::show();
+               
+               // set correct content-type
+               @header('Content-Type: application/rss+xml');
+               
+               // show template
+               WCF::getTPL()->display($this->templateName, $this->application, false);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/bbcode/AttachmentBBCode.class.php b/wcfsetup/install/files/lib/system/bbcode/AttachmentBBCode.class.php
new file mode 100644 (file)
index 0000000..c374eb1
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+namespace wcf\system\bbcode;
+use wcf\data\attachment\GroupedAttachmentList;
+use wcf\system\request\LinkHandler;
+use wcf\util\StringUtil;
+
+/**
+ * Parses the [attach] bbcode tag.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.bbcode
+ * @category   Community Framework
+ */
+class AttachmentBBCode extends AbstractBBCode {
+       /**
+        * list of attachments
+        * @var wcf\data\attachment\GroupedAttachmentList
+        */
+       protected static $attachmentList = null;
+       
+       /**
+        * active object id
+        * @var integer
+        */
+       protected static $objectID = 0;
+       
+       /**
+        * @see wcf\system\bbcode\IBBCode::getParsedTag()
+        */
+       public function getParsedTag(array $openingTag, $content, array $closingTag, BBCodeParser $parser) {
+               // get attachment id
+               $attachmentID = 0;
+               if (isset($openingTag['attributes'][0])) {
+                       $attachmentID = $openingTag['attributes'][0];
+               }
+               
+               // get attachment for active object
+               $attachments = array();
+               if (self::$attachmentList !== null) {
+                       $attachments = self::$attachmentList->getGroupedObjects(self::$objectID);
+               }
+               
+               if (isset($attachments[$attachmentID])) {
+                       $attachment = $attachments[$attachmentID];
+                       
+                       // mark attachment as embedded
+                       $attachment->markAsEmbedded();
+                       
+                       if ($attachment->showAsImage() && $parser->getOutputType() == 'text/html') {
+                               // image
+                               $linkParameters = array(
+                                       'object' => $attachment 
+                               );
+                               if ($attachment->hasThumbnail()) {
+                                       $linkParameters['thumbnail'] = 1; 
+                               }
+                               
+                               // get alignment
+                               $alignment = (isset($openingTag['attributes'][1]) ? $openingTag['attributes'][1] : '');
+                               $result = '<img src="'.StringUtil::encodeHTML(LinkHandler::getInstance()->getLink('Attachment', $linkParameters)).'"'.(!$attachment->hasThumbnail() ? ' class="jsResizeImage"' : '').' style="width: '.($attachment->hasThumbnail() ? $attachment->thumbnailWidth : $attachment->width).'px; height: '.($attachment->hasThumbnail() ? $attachment->thumbnailHeight: $attachment->height).'px;'.(!empty($alignment) ? ' float:' . ($alignment == 'left' ? 'left' : 'right') . '; margin: ' . ($alignment == 'left' ? '0 15px 7px 0' : '0 0 7px 15px' ) : '').'" alt="" />';
+                               if ($attachment->hasThumbnail() && $attachment->canDownload()) {
+                                       $result = '<a href="'.StringUtil::encodeHTML(LinkHandler::getInstance()->getLink('Attachment', array('object' => $attachment))).'" title="'.StringUtil::encodeHTML($attachment->filename).'" class="jsImageViewer">'.$result.'</a>';
+                               }
+                               return $result;
+                       }
+                       else {
+                               // file
+                               return StringUtil::getAnchorTag(LinkHandler::getInstance()->getLink('Attachment', array(
+                                       'object' => $attachment
+                               )), ((!empty($content) && $content != $attachmentID) ? $content : $attachment->filename));
+                       }
+               }
+               
+               // fallback
+               return StringUtil::getAnchorTag(LinkHandler::getInstance()->getLink('Attachment', array(
+                       'id' => $attachmentID
+               )));
+       }
+       
+       /**
+        * Sets the attachment list.
+        * 
+        * @param       wcf\data\attachment\GroupedAttachmentList       $attachments
+        */
+       public static function setAttachmentList(GroupedAttachmentList $attachmentList) {
+               self::$attachmentList = $attachmentList;
+       }
+       
+       /**
+        * Sets the active object id.
+        * 
+        * @param       integer         $objectID
+        */
+       public static function setObjectID($objectID) {
+               self::$objectID = $objectID;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php b/wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php
new file mode 100644 (file)
index 0000000..dccb069
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+namespace wcf\system\message;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\data\IMessage;
+use wcf\data\IMessageQuickReplyAction;
+use wcf\system\bbcode\PreParser;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\SystemException;
+use wcf\system\exception\UserInputException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\ClassUtil;
+
+/**
+ * Manages quick replies and stored messages.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.message
+ * @category   Community Framework
+ */
+class QuickReplyManager extends SingletonFactory {
+       /**
+        * list of allowed bbcodes
+        * @var array<string>
+        */
+       public $allowedBBodes = null;
+       
+       /**
+        * container object
+        * @var wcf\data\DatabaseObject
+        */
+       public $container = null;
+       
+       /**
+        * object id
+        * @var integer
+        */
+       public $objectID = 0;
+       
+       /**
+        * object type
+        * @var string
+        */
+       public $type = '';
+       
+       /**
+        * Returns a stored message from session.
+        * 
+        * @param       string          $type
+        * @param       integer         $objectID
+        * @return      string
+        */
+       public function getMessage($type, $objectID) {
+               $this->type = $type;
+               $this->objectID = $objectID;
+               
+               // allow manipulation before fetching data
+               EventHandler::getInstance()->fireAction($this, 'getMessage');
+               
+               $message = WCF::getSession()->getVar('quickReply-'.$this->type.'-'.$this->objectID);
+               return ($message === null ? '' : $message);
+       }
+       
+       /**
+        * Stores a message in session.
+        * 
+        * @param       string          $type
+        * @param       integer         $objectID
+        * @param       string          $message
+        */
+       public function setMessage($type, $objectID, $message) {
+               WCF::getSession()->register('quickReply-'.$type.'-'.$objectID, $message);
+       }
+       
+       /**
+        * Removes a stored message from session.
+        * 
+        * @param       string          $type
+        * @param       integer         $objectID
+        */
+       public function removeMessage($type, $objectID) {
+               WCF::getSession()->unregister('quickReply-'.$this->type.'-'.$objectID);
+       }
+       
+       /**
+        * Sets the allowed bbcodes.
+        * 
+        * @param       array<string>           $allowedBBCodes
+        */
+       public function setAllowedBBCodes(array $allowedBBCodes = null) {
+               $this->allowedBBodes = $allowedBBCodes;
+       }
+       
+       /**
+        * Validates parameters for current request.
+        * 
+        * @param       wcf\system\message\IMessageQuickReplyAction     $object
+        * @param       array<array>                                    $parameters
+        * @param       string                                          $containerClassName
+        * @param       string                                          $containerDecoratorClassName
+        */
+       public function validateParameters(IMessageQuickReplyAction $object, array &$parameters, $containerClassName, $containerDecoratorClassName = '') {
+               if (!isset($parameters['data']['message']) || empty($parameters['data']['message'])) {
+                       throw new UserInputException('message');
+               }
+               
+               $parameters['lastPostTime'] = (isset($parameters['lastPostTime'])) ? intval($parameters['lastPostTime']) : 0;
+               if (!$parameters['lastPostTime']) {
+                       throw new UserInputException('lastPostTime');
+               }
+               
+               $parameters['pageNo'] = (isset($parameters['pageNo'])) ? intval($parameters['pageNo']) : 0;
+               if (!$parameters['pageNo']) {
+                       throw new UserInputException('pageNo');
+               }
+               
+               $parameters['objectID'] = (isset($parameters['objectID'])) ? intval($parameters['objectID']) : 0;
+               if (!$parameters['objectID']) {
+                       throw new UserInputException('objectID');
+               }
+               
+               $this->container = new $containerClassName($parameters['objectID']);
+               if (!empty($containerDecoratorClassName)) {
+                       if (!ClassUtil::isInstanceOf($containerDecoratorClassName, 'wcf\data\DatabaseObjectDecorator')) {
+                               throw new SystemException("'".$containerDecoratorClassName."' does not extend 'wcf\data\DatabaseObjectDecorator'");
+                       }
+                       
+                       $this->container = new $containerDecoratorClassName($this->container);
+               }
+               $object->validateContainer($this->container);
+               
+               // validate message
+               $object->validateMessage($this->container, $parameters['data']['message']);
+               
+               // check for message quote ids
+               $parameters['removeQuoteIDs'] = (isset($parameters['removeQuoteIDs']) && is_array($parameters['removeQuoteIDs'])) ? ArrayUtil::trim($parameters['removeQuoteIDs']) : array();
+       }
+       
+       /**
+        * Creates a new message and returns the parsed template.
+        * 
+        * @param       wcf\data\IMessageQuickReplyAction       $object
+        * @param       array<array>                            $parameters
+        * @param       string                                  $containerActionClassName
+        * @param       string                                  $sortOrder
+        * @param       string                                  $templateName
+        * @param       string                                  $application
+        * @return      array
+        */
+       public function createMessage(IMessageQuickReplyAction $object, array &$parameters, $containerActionClassName, $sortOrder, $templateName, $application = 'wcf') {
+               $tableIndexName = call_user_func(array($this->container, 'getDatabaseTableIndexName'));
+               $parameters['data'][$tableIndexName] = $parameters['objectID'];
+               $parameters['data']['enableSmilies'] = WCF::getSession()->getPermission('user.message.canUseSmilies');
+               $parameters['data']['enableHtml'] = 0;
+               $parameters['data']['enableBBCodes'] = WCF::getSession()->getPermission('user.message.canUseBBCodes');
+               $parameters['data']['showSignature'] = (WCF::getUser()->userID ? WCF::getUser()->showSignature : 0);
+               $parameters['data']['time'] = TIME_NOW;
+               $parameters['data']['userID'] = WCF::getUser()->userID;
+               $parameters['data']['username'] = WCF::getUser()->username;
+               
+               // pre-parse message text
+               $parameters['data']['message'] = PreParser::getInstance()->parse($parameters['data']['message'], $this->allowedBBodes);
+               
+               $message = $object->create();
+               if ($message instanceof IMessage && !$message->isVisible()) {
+                       return array(
+                               'isVisible' => false
+                       );
+               }
+               
+               // resolve the page no
+               list($pageNo, $count) = $object->getPageNo($this->container);
+               
+               // we're still on current page
+               if ($pageNo == $parameters['pageNo']) {
+                       // check for additional messages
+                       $messageList = $object->getMessageList($this->container, $parameters['lastPostTime']);
+                               
+                       // calculate start index
+                       $startIndex = $count - (count($messageList) - 1);
+                       
+                       WCF::getTPL()->assign(array(
+                               'attachmentList' => $messageList->getAttachmentList(),
+                               'container' => $this->container,
+                               'objects' => $messageList,
+                               'startIndex' => $startIndex,
+                               'sortOrder' => $sortOrder,
+                       ));
+                       
+                       // assign 'to top' link
+                       if (isset($parameters['anchor'])) {
+                               WCF::getTPL()->assign('anchor', $parameters['anchor']);
+                       }
+                       
+                       // update visit time (messages shouldn't occur as new upon next visit)
+                       if (ClassUtil::isInstanceOf($containerActionClassName, 'wcf\data\IVisitableObjectAction')) {
+                               $containerAction = new $containerActionClassName(array(($this->container instanceof DatabaseObjectDecorator ? $this->container->getDecoratedObject() : $this->container)), 'markAsRead');
+                               $containerAction->executeAction();
+                       }
+                       
+                       return array(
+                               'lastPostTime' => $message->time,
+                               'template' => WCF::getTPL()->fetch($templateName, $application)
+                       );
+               }
+               else {
+                       // redirect
+                       return array(
+                               'url' => $object->getRedirectUrl($this->container, $message)
+                       );
+               }
+       }
+       
+       /**
+        * Returns the container object.
+        * 
+        * @return      wcf\data\DatabaseObject
+        */
+       public function getContainer() {
+               return $this->container;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/message/censorship/Censorship.class.php b/wcfsetup/install/files/lib/system/message/censorship/Censorship.class.php
new file mode 100644 (file)
index 0000000..fae3227
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+namespace wcf\system\message\censorship;
+use wcf\system\SingletonFactory;
+use wcf\util\ArrayUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Finds censored words.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.message.censorship
+ * @category   Community Framework
+ */
+class Censorship extends SingletonFactory {
+       /**
+        * censored words
+        * @var array<string>
+        */
+       protected $censoredWords = array();
+       
+       /**
+        * word delimiters
+        * @var string
+        */
+       protected $delimiters = '[\s\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]';
+       
+       /**
+        * list of words
+        * @var array<string>
+        */
+       protected $words = array();
+       
+       /**
+        * list of matches
+        * @var array
+        */
+       protected $matches = array();
+       
+       /**
+        * @see wcf\system\SingletonFactory::init()
+        */
+       protected function init() {
+               // get words which should be censored
+               $censoredWords = explode("\n", StringUtil::unifyNewlines(StringUtil::toLowerCase(CENSORED_WORDS)));
+               
+               // format censored words
+               $this->censoredWords = ArrayUtil::trim($censoredWords);
+       }
+       
+       /**
+        * Returns censored words from a text.
+        * 
+        * @param       string          $text
+        * @return      mixed           $matches / false
+        */
+       public function test($text) {
+               // reset values
+               $this->matches = $this->words = array();
+               
+               // string to lower case
+               $text = StringUtil::toLowerCase($text);
+               
+               // ignore bbcode tags
+               $text = preg_replace('~\[/?[a-z]+[^\]]*\]~i', '', $text);
+               
+               // split the text in single words
+               $this->words = preg_split("!".$this->delimiters."+!", $text, -1, PREG_SPLIT_NO_EMPTY);
+               
+               // check each word if it's censored.
+               for ($i = 0, $count = count($this->words); $i < $count; $i++) {
+                       $word = $this->words[$i];
+                       foreach ($this->censoredWords as $censoredWord) {
+                               // check for direct matches ("badword" == "badword")
+                               if ($censoredWord == $word) {
+                                       // store censored word
+                                       if (isset($this->matches[$word])) {
+                                               $this->matches[$word]++;
+                                       }
+                                       else {
+                                               $this->matches[$word] = 1;
+                                       }
+                                               
+                                       continue 2;
+                               }
+                               // check for asterisk matches ("*badword*" == "FooBadwordBar")
+                               else if (StringUtil::indexOf($censoredWord, '*') !== false) {
+                                       $censoredWord = StringUtil::replace('\*', '.*', preg_quote($censoredWord));
+                                       if (preg_match('!^'.$censoredWord.'$!', $word)) {
+                                               // store censored word
+                                               if (isset($this->matches[$word])) {
+                                                       $this->matches[$word]++;
+                                               }
+                                               else {
+                                                       $this->matches[$word] = 1;
+                                               }
+                                               
+                                               continue 2;
+                                       }
+                               }
+                               // check for partial matches ("~badword~" == "bad-word")
+                               else if (StringUtil::indexOf($censoredWord, '~') !== false) {
+                                       $censoredWord = StringUtil::replace('~', '', $censoredWord);
+                                       if (($position = StringUtil::indexOf($censoredWord, $word)) !== false) {
+                                               if ($position > 0) {
+                                                       // look behind
+                                                       if (!$this->lookBehind($i - 1, StringUtil::substring($censoredWord, 0, $position))) {
+                                                               continue;
+                                                       }
+                                               }
+                                               
+                                               if ($position + StringUtil::length($word) < StringUtil::length($censoredWord)) {
+                                                       // look ahead
+                                                       if (($newIndex = $this->lookAhead($i + 1, StringUtil::substring($censoredWord, $position + StringUtil::length($word))))) {
+                                                               $i = $newIndex;
+                                                       }
+                                                       else {
+                                                               continue;
+                                                       }
+                                               }
+                                               
+                                               // store censored word
+                                               if (isset($this->matches[$censoredWord])) {
+                                                       $this->matches[$censoredWord]++;
+                                               }
+                                               else {
+                                                       $this->matches[$censoredWord] = 1;
+                                               }
+                                               
+                                               continue 2;
+                                       }
+                               }
+                       }
+               }
+               
+               // at least one censored word was found
+               if (count($this->matches) > 0) {
+                       return $this->matches;
+               }
+               // text is clean
+               else {
+                       return false;
+               }
+       }
+       
+       /**
+        * Looks behind in the word list.
+        * 
+        * @param       integer         $index
+        * @param       string          $search
+        * @return      boolean
+        */
+       protected function lookBehind($index, $search) {
+               if (isset($this->words[$index])) {
+                       if (StringUtil::indexOf($this->words[$index], $search) === (StringUtil::length($this->words[$index]) - StringUtil::length($search))) {
+                               return true;
+                       }
+                       else if (StringUtil::indexOf($search, $this->words[$index]) === (StringUtil::length($search) - StringUtil::length($this->words[$index]))) {
+                               return $this->lookBehind($index - 1, 0, (StringUtil::length($search) - StringUtil::length($this->words[$index])));
+                       }
+               }
+               
+               return false;
+       }
+       
+       /**
+        * Looks ahead in the word list.
+        * 
+        * @param       integer         $index
+        * @param       string          $search
+        * @return      mixed
+        */
+       protected function lookAhead($index, $search) {
+               if (isset($this->words[$index])) {
+                       if (StringUtil::indexOf($this->words[$index], $search) === 0) {
+                               return $index;
+                       }
+                       else if (StringUtil::indexOf($search, $this->words[$index]) === 0) {
+                               return $this->lookAhead($index + 1, StringUtil::substring($search, StringUtil::length($this->words[$index])));
+                       }
+               }
+               
+               return false;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php
new file mode 100644 (file)
index 0000000..885889b
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+namespace wcf\system\message\quote;
+use wcf\data\user\UserProfile;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Default implementation for quote handlers.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.message.quote
+ * @category   Community Framework
+ */
+abstract class AbstractMessageQuoteHandler extends SingletonFactory implements IMessageQuoteHandler {
+       /**
+        * template name
+        * @var string
+        */
+       public $templateName = 'messageQuoteList';
+       
+       /**
+        * list of quoted message
+        * @var array<wcf\system\message\quote\QuotedMessage>
+        */
+       public $quotedMessages = array();
+       
+       /**
+        * @see wcf\system\message\quote\IMessageQuoteHandler::render()
+        */
+       public function render(array $data, $supportPaste = false) {
+               $messages = $this->getMessages($data);
+               $userIDs = $userProfiles = array();
+               foreach ($messages as $message) {
+                       $userID = $message->getUserID();
+                       if ($userID) {
+                               $userIDs[] = $userID;
+                       }
+               }
+               
+               if (!empty($userIDs)) {
+                       $userIDs = array_unique($userIDs);
+                       $userProfiles = UserProfile::getUserProfiles($userIDs);
+               }
+               
+               WCF::getTPL()->assign(array(
+                       'messages' => $this->getMessages($data),
+                       'supportPaste' => $supportPaste,
+                       'userProfiles' => $userProfiles
+               ));
+               
+               return WCF::getTPL()->fetch($this->templateName);
+       }
+       
+       /**
+        * @see wcf\system\message\quote\IMessageQuoteHandler::renderQuotes()
+        */
+       public function renderQuotes(array $data, $render = true) {
+               $messages = $this->getMessages($data);
+               
+               $renderedQuotes = array();
+               foreach ($messages as $message) {
+                       foreach ($message as $quoteID => $quote) {
+                               if ($render) {
+                                       $renderedQuotes[] = MessageQuoteManager::getInstance()->renderQuote($message->object, $quote);
+                               }
+                               else {
+                                       $quotedMessage = $message->getFullQuote($quoteID);
+                                       $renderedQuotes[] = MessageQuoteManager::getInstance()->renderQuote($message->object, ($quotedMessage === null ? $quote : $quotedMessage));
+                               }
+                       }
+               }
+               
+               return $renderedQuotes;
+       }
+       
+       /**
+        * Returns a list of QuotedMessage objects.
+        * 
+        * @param       array<array>    $data
+        * @return      array<wcf\system\message\quote\QuotedMessage>
+        */
+       abstract protected function getMessages(array $data);
+}
diff --git a/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php
new file mode 100644 (file)
index 0000000..53149b2
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace wcf\system\message\quote;
+
+/**
+ * Default interface for quote handlers.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.message.quote
+ * @category   Community Framework
+ */
+interface IMessageQuoteHandler {
+       /**
+        * Renders a template for given quotes.
+        * 
+        * @param       array           $data
+        * @param       boolean         $supportPaste
+        * @return      string
+        */
+       public function render(array $data, $supportPaste = false);
+       
+       /**
+        * Renders a list of quotes for insertation.
+        * 
+        * @param       array<array>    $data
+        * @param       boolean         $render
+        * @return      array<string>
+        */
+       public function renderQuotes(array $data, $render = true);
+}
diff --git a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php
new file mode 100644 (file)
index 0000000..af878cc
--- /dev/null
@@ -0,0 +1,564 @@
+<?php
+namespace wcf\system\message\quote;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\IMessage;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\exception\SystemException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Manages message quotes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.message.quote
+ * @category   Community Framework
+ */
+class MessageQuoteManager extends SingletonFactory {
+       /**
+        * current object ids
+        * @var array<integer>
+        */
+       protected $objectIDs = array();
+       
+       /**
+        * current object type name
+        * @var string
+        */
+       protected $objectType = '';
+       
+       /**
+        * list of object types
+        * @var array<wcf\data\object\type\ObjectType>
+        */
+       protected $objectTypes = array();
+       
+       /**
+        * primary application's package id
+        * @var integer
+        */
+       protected $packageID = 0;
+       
+       /**
+        * list of stored quotes
+        * @var array<array>
+        */
+       protected $quotes = array();
+       
+       /**
+        * list of quote messages by quote id
+        * @var array<string>
+        */
+       protected $quoteData = array();
+       
+       /**
+        * message id for quoting
+        * @var integer
+        */
+       protected $quoteMessageID = 0;
+       
+       /**
+        * list of quote ids to be removed
+        * @var array<string>
+        */
+       protected $removeQuoteIDs = array();
+       
+       /**
+        * @see wcf\system\SingletonFactory::init()
+        */
+       protected function init() {
+               $this->packageID = ApplicationHandler::getInstance()->getPrimaryApplication()->packageID;
+               
+               // load stored quotes from session
+               $messageQuotes = WCF::getSession()->getVar('__messageQuotes'.$this->packageID);
+               if (is_array($messageQuotes)) {
+                       $this->quotes = (isset($messageQuotes['quotes'])) ? $messageQuotes['quotes'] : array();
+                       $this->quoteData = (isset($messageQuotes['quoteData'])) ? $messageQuotes['quoteData'] : array();
+                       $this->removeQuoteIDs = (isset($messageQuotes['removeQuoteIDs'])) ? $messageQuotes['removeQuoteIDs'] : array();
+               }
+               
+               // load object types
+               $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.message.quote');
+               foreach ($objectTypes as $objectType) {
+                       $this->objectTypes[$objectType->objectType] = $objectType;
+               }
+       }
+       
+       /**
+        * Adds a quote unless it is already stored. If you want to quote a whole
+        * message while maintaing the original markup, pass $obj->getExcerpt() for
+        * $message and $obj->getMessage() for $fullQuote.
+        * 
+        * @param       string          $objectType
+        * @param       integer         $parentObjectID
+        * @param       integer         $objectID
+        * @param       string          $message
+        * @param       string          $fullQuote
+        * @param       boolean
+        */
+       public function addQuote($objectType, $parentObjectID, $objectID, $message, $fullQuote = '') {
+               if (!isset($this->objectTypes[$objectType])) {
+                       throw new SystemException("Object type '".$objectType."' is unknown");
+               }
+               
+               if (!isset($this->quotes[$objectType])) {
+                       $this->quotes[$objectType] = array();
+               }
+               
+               if (!isset($this->quotes[$objectType][$objectID])) {
+                       $this->quotes[$objectType][$objectID] = array();
+               }
+               
+               $quoteID = $this->getQuoteID($objectType, $objectID, $message, $fullQuote);
+               if (!isset($this->quotes[$objectType][$objectID][$quoteID])) {
+                       $this->quotes[$objectType][$objectID][$quoteID] = 0;
+                       $this->quoteData[$quoteID] = $message;
+                       
+                       // save parent object id
+                       if ($parentObjectID) {
+                               if (!isset($this->quoteData['parents'])) {
+                                       $this->quoteData['parents'] = array();
+                               }
+                               
+                               if (!isset($this->quoteData['parents'][$objectType])) {
+                                       $this->quoteData['parents'][$objectType] = array();
+                               }
+                               
+                               if (!isset($this->quoteData['parents'][$objectType][$parentObjectID])) {
+                                       $this->quoteData['parents'][$objectType][$parentObjectID] = array();
+                               }
+                               
+                               $this->quoteData['parents'][$objectType][$parentObjectID][] = $objectID;
+                               $this->quoteData[$quoteID.'_pID'] = $parentObjectID;
+                       }
+                       
+                       if (!empty($fullQuote)) {
+                               $this->quotes[$objectType][$objectID][$quoteID] = 1;
+                               $this->quoteData[$quoteID.'_fq'] = $fullQuote;
+                       }
+                       
+                       $this->updateSession();
+                       
+                       return true;
+               }
+               
+               return false;
+       }
+       
+       /**
+        * Returns the quote id for given quote.
+        * 
+        * @param       string          $objectType
+        * @param       integer         $objectID
+        * @param       string          $message
+        * @param       string          $fullQuote
+        * @return      string
+        */
+       public function getQuoteID($objectType, $objectID, $message, $fullQuote = '') {
+               return substr(sha1($objectType.'|'.$objectID.'|'.$message.'|'.$fullQuote), 0, 8);
+       }
+       
+       /**
+        * Removes a quote from storage.
+        * 
+        * @param       string          $quoteID
+        */
+       public function removeQuote($quoteID) {
+               if (!isset($this->quoteData[$quoteID])) {
+                       return false;
+               }
+               
+               foreach ($this->quotes as $objectType => $objectIDs) {
+                       foreach ($objectIDs as $objectID => $quoteIDs) {
+                               foreach ($quoteIDs as $qID => $isFullQuote) {
+                                       if ($qID == $quoteID) {
+                                               unset($this->quotes[$objectType][$objectID][$qID]);
+                                               
+                                               // clean-up structure
+                                               if (empty($this->quotes[$objectType][$objectID])) {
+                                                       unset($this->quotes[$objectType][$objectID]);
+                                                       
+                                                       if (empty($this->quotes[$objectType])) {
+                                                               unset($this->quotes[$objectType]);
+                                                       }
+                                               }
+                                               
+                                               unset($this->quoteData[$quoteID]);
+                                               if ($isFullQuote) {
+                                                       unset($this->quoteData[$quoteID.'_fq']);
+                                               }
+                                               
+                                               // remove parent object id reference
+                                               if (isset($this->quoteData[$quoteID.'_pID'])) {
+                                                       $parentObjectID = $this->quoteData[$quoteID.'_pID'];
+                                                       if (!isset($this->quotes[$objectType][$objectID])) {
+                                                               if (isset($this->quoteData['parents'][$objectType][$parentObjectID][$objectID])) {
+                                                                       unset($this->quoteData['parents'][$objectType][$parentObjectID][$objectID]);
+                                                                       
+                                                                       // cleanup
+                                                                       if (empty($this->quoteData['parents'][$objectType][$parentObjectID])) {
+                                                                               unset($this->quoteData['parents'][$objectType][$parentObjectID]);
+                                                                       
+                                                                               if (empty($this->quoteData['parents'][$objectType])) {
+                                                                                       unset($this->quoteData['parents'][$objectType]);
+                                                                                               
+                                                                                       if (empty($this->quoteData['parents'])) {
+                                                                                               unset($this->quoteData['parents']);
+                                                                                       }
+                                                                               }
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                               
+                                               $this->updateSession();
+                                               
+                                               return true;
+                                       }
+                               }
+                       }
+               }
+               
+               return false;
+       }
+       
+       /**
+        * Returns a list of quotes.
+        * 
+        * @param       boolean         supportPaste
+        */
+       public function getQuotes($supportPaste = false) {
+               $template = '';
+               
+               foreach ($this->quotes as $objectType => $objectData) {
+                       $quoteHandler = call_user_func(array($this->objectTypes[$objectType]->className, 'getInstance'));
+                       $template .= $quoteHandler->render($objectData, $supportPaste);
+               }
+               
+               return $template;
+       }
+       
+       /**
+        * Returns a list of quotes by object type and id.
+        * 
+        * @param       string          $objectType
+        * @param       array<integer>  $objectIDs
+        * @param       boolean         $markForRemoval
+        * @return      array<string>
+        */
+       public function getQuotesByObjectIDs($objectType, array $objectIDs, $markForRemoval = true) {
+               if (!isset($this->quotes[$objectType])) {
+                       return array();
+               }
+               
+               $data = array();
+               $removeQuoteIDs = array();
+               foreach ($this->quotes[$objectType] as $objectID => $quoteIDs) {
+                       if (in_array($objectID, $objectIDs)) {
+                               $data[$objectID] = $quoteIDs;
+                               
+                               // mark quotes for removal
+                               if ($markForRemoval) {
+                                       $removeQuoteIDs = array_merge($removeQuoteIDs, array_keys($quoteIDs));
+                               }
+                       }
+               }
+               
+               // no quotes found
+               if (empty($data)) {
+                       return array();
+               }
+               
+               // mark quotes for removal
+               if (!empty($removeQuoteIDs)) {
+                       $this->markQuotesForRemoval($removeQuoteIDs);
+               }
+               
+               $quoteHandler = call_user_func(array($this->objectTypes[$objectType]->className, 'getInstance'));
+               return $quoteHandler->renderQuotes($data);
+       }
+       
+       /**
+        * Returns a list of quotes by object type and parent object id.
+        * 
+        * @param       string          $objectType
+        * @param       integer         $parentObjectID
+        * @param       boolean         $markForRemoval
+        * @return      array<string>
+        */
+       public function getQuotesByParentObjectID($objectType, $parentObjectID, $markForRemoval = true) {
+               if (!isset($this->quoteData['parents'][$objectType][$parentObjectID])) {
+                       return array();
+               }
+               
+               $data = array();
+               $removeQuoteIDs = array();
+               foreach ($this->quoteData['parents'][$objectType][$parentObjectID] as $objectID) {
+                       if (isset($this->quotes[$objectType][$objectID])) {
+                               $data[$objectID] = $this->quotes[$objectType][$objectID];
+                               
+                               // mark quotes for removal
+                               if ($markForRemoval) {
+                                       $removeQuoteIDs = array_merge($removeQuoteIDs, array_keys($data[$objectID]));
+                               }
+                       }
+               }
+               
+               // no quotes found
+               if (empty($data)) {
+                       return array();
+               }
+               
+               // mark quotes for removal
+               if (!empty($removeQuoteIDs)) {
+                       $this->markQuotesForRemoval($removeQuoteIDs);
+               }
+               
+               $quoteHandler = call_user_func(array($this->objectTypes[$objectType]->className, 'getInstance'));
+               return $quoteHandler->renderQuotes($data, false);
+       }
+       
+       /**
+        * Returns a quote by id.
+        * 
+        * @param       string          $quoteID
+        * @param       boolean         $useFullQuote
+        * @return      string
+        */
+       public function getQuote($quoteID, $useFullQuote = true) {
+               if ($useFullQuote && isset($this->quoteData[$quoteID.'_fq'])) {
+                       return $this->quoteData[$quoteID.'_fq'];
+               }
+               else if (isset($this->quoteData[$quoteID])) {
+                       return $this->quoteData[$quoteID];
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Returns the object id by quote id.
+        * 
+        * @param       string          $quoteID
+        * @return      integer
+        */
+       public function getObjectID($quoteID) {
+               if (isset($this->quoteData[$quoteID])) {
+                       foreach ($this->quotes as $objectIDs) {
+                               foreach ($objectIDs as $objectID => $quoteIDs) {
+                                       if (isset($quoteIDs[$quoteID])) {
+                                               return $objectID;
+                                       }
+                               }
+                       }
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Marks quote ids for removal.
+        * 
+        * @param       array<string>   $quoteIDs
+        */
+       public function markQuotesForRemoval(array $quoteIDs) {
+               foreach ($quoteIDs as $index => $quoteID) {
+                       if (!isset($this->quoteData[$quoteID]) || in_array($quoteID, $this->removeQuoteIDs)) {
+                               unset($quoteIDs[$index]);
+                       }
+               }
+               
+               if (!empty($quoteIDs)) {
+                       $this->removeQuoteIDs = array_merge($this->removeQuoteIDs, $quoteIDs);
+                       $this->updateSession();
+               }
+       }
+       
+       /**
+        * Renders a quote for given message.
+        * 
+        * @param       wcf\data\IMessage       $message
+        * @param       string                  $text
+        * @return      string
+        */
+       public function renderQuote(IMessage $message, $text) {
+               $escapedUsername = StringUtil::replace(array("\\", "'"), array("\\\\", "\'"), $message->getUsername());
+               $escapedLink = StringUtil::replace(array("\\", "'"), array("\\\\", "\'"), $message->getLink());
+               
+               return "[quote='".$escapedUsername."','".$escapedLink."']".$text."[/quote]";
+       }
+       
+       /**
+        * Removes quotes marked for removal.
+        */
+       public function removeMarkedQuotes() {
+               if (!empty($this->removeQuoteIDs)) {
+                       foreach ($this->removeQuoteIDs as $quoteID) {
+                               $this->removeQuote($quoteID);
+                       }
+                       
+                       // reset list of quote ids marked for removal
+                       $this->removeQuoteIDs = array();
+                       
+                       $this->updateSession();
+               }
+       }
+       
+       /**
+        * Returns the number of stored quotes.
+        * 
+        * @return      integer
+        */
+       public function countQuotes() {
+               $count = 0;
+               foreach ($this->quoteData as $quoteID => $quote) {
+                       if (strlen($quoteID) == 8) {
+                               $count++;
+                       }
+               }
+               
+               return $count;
+       }
+       
+       /**
+        * Returns a list of full quotes by object id for given object types.
+        * 
+        * @param       array<string>           $objectTypes
+        * @return      array<array>
+        */
+       public function getFullQuoteObjectIDs(array $objectTypes) {
+               $objectIDs = array();
+               
+               foreach ($objectTypes as $objectType) {
+                       if (!isset($this->objectTypes[$objectType])) {
+                               throw new SystemException("Object type '".$objectType."' is unknown");
+                       }
+                       
+                       $objectIDs[$objectType] = array();
+                       if (isset($this->quotes[$objectType])) {
+                               foreach ($this->quotes[$objectType] as $objectID => $quotes) {
+                                       foreach ($quotes as $quoteID => $isFullQuote) {
+                                               if ($isFullQuote) {
+                                                       $objectIDs[$objectType][] = $objectID;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
+               return $objectIDs;
+       }
+       
+       /**
+        * Sets object type and object ids.
+        * 
+        * @param       string          $objectType
+        * @param       array<integer>  $objectIDs
+        */
+       public function initObjects($objectType, array $objectIDs) {
+               if (!isset($this->objectTypes[$objectType])) {
+                       throw new SystemException("Object type '".$objectType."' is unknown");
+               }
+               
+               $this->objectIDs = ArrayUtil::toIntegerArray($objectIDs);
+               $this->objectType = $objectType;
+       }
+       
+       /**
+        * Reads the quote message id.
+        */
+       public function readParameters() {
+               if (isset($_REQUEST['quoteMessageID'])) $this->quoteMessageID = intval($_REQUEST['quoteMessageID']);
+       }
+       
+       /**
+        * Reads a list of quote ids to remove.
+        */
+       public function readFormParameters() {
+               if (isset($_REQUEST['__removeQuoteIDs']) && is_array($_REQUEST['__removeQuoteIDs'])) {
+                       $quoteIDs = ArrayUtil::trim($_REQUEST['__removeQuoteIDs']);
+                       foreach ($quoteIDs as $index => $quoteID) {
+                               if (!isset($this->quoteData[$quoteID])) {
+                                       unset($quoteIDs[$index]);
+                               }
+                       }
+                       
+                       if (!empty($quoteIDs)) {
+                               $this->removeQuoteIDs = array_merge($this->removeQuoteIDs, $quoteIDs);
+                       }
+               }
+       }
+       
+       /**
+        * Removes quotes after saving current message.
+        */
+       public function saved() {
+               $this->removeMarkedQuotes();
+       }
+       
+       /**
+        * Assigns variables on page load.
+        */
+       public function assignVariables() {
+               $fullQuoteObjectIDs = array();
+               if (!empty($this->objectType) && !empty($this->objectIDs) && isset($this->quotes[$this->objectType])) {
+                       foreach ($this->quotes[$this->objectType] as $objectID => $quotes) {
+                               if (!in_array($objectID, $this->objectIDs)) {
+                                       continue;
+                               }
+                               
+                               foreach ($quotes as $isFullQuote) {
+                                       if ($isFullQuote) {
+                                               $fullQuoteObjectIDs[] = $objectID;
+                                               break;
+                                       }
+                               }
+                       }
+               }
+               
+               WCF::getTPL()->assign(array(
+                       '__quoteCount' => $this->countQuotes(),
+                       '__quoteFullQuote' => $fullQuoteObjectIDs,
+                       '__quoteRemove' => $this->removeQuoteIDs
+               ));
+       }
+       
+       /**
+        * Returns quote message id.
+        * 
+        * @return      integer
+        */
+       public function getQuoteMessageID() {
+               return $this->quoteMessageID;
+       }
+       
+       /**
+        * Removes orphaned quote ids
+        * 
+        * @param       array<integer>          $quoteIDs
+        */
+       public function removeOrphanedQuotes(array $quoteIDs) {
+               foreach ($quoteIDs as $quoteID) {
+                       $this->removeQuote($quoteID);
+               }
+               
+               $this->updateSession();
+       }
+       
+       /**
+        * Updates data stored in session,
+        */
+       protected function updateSession() {
+               WCF::getSession()->register('__messageQuotes'.$this->packageID, array(
+                       'quotes' => $this->quotes,
+                       'quoteData' => $this->quoteData,
+                       'removeQuoteIDs' => $this->removeQuoteIDs
+               ));
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php b/wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php
new file mode 100644 (file)
index 0000000..27825b7
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+namespace wcf\system\message\quote;
+use wcf\data\IMessage;
+
+/**
+ * Wrapper class for quoted messages.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage system.message.quote
+ * @category   Community Framework
+ */
+class QuotedMessage implements \Countable, \Iterator {
+       /**
+        * list of full quotes for insertation
+        * @var array<string>
+        */
+       public $fullQuotes = array();
+       
+       /**
+        * quotable database object
+        * @var wcf\data\IQuotableDatabaseObject
+        */
+       public $object = null;
+       
+       /**
+        * list of quotes (shortend)
+        * @var array<string>
+        */
+       public $quotes = array();
+       
+       /**
+        * current iterator index
+        * @var integer
+        */
+       protected $index = 0;
+       
+       /**
+        * list of index to object relation
+        * @var array<integer>
+        */
+       protected $indexToObject = null;
+       
+       /**
+        * Creates a new QuotedMessage object.
+        * 
+        * @param       wcf\data\IMessage       $object
+        */
+       public function __construct(IMessage $object) {
+               $this->object = $object;
+       }
+       
+       /**
+        * Adds a quote for this message.
+        * 
+        * @param       string          $quoteID
+        * @param       string          $quote
+        * @param       string          $fullQuote
+        */
+       public function addQuote($quoteID, $quote, $fullQuote) {
+               $this->fullQuotes[$quoteID] = $fullQuote;
+               $this->quotes[$quoteID] = $quote;
+               $this->indexToObject[] = $quoteID;
+       }
+       
+       /**
+        * @see wcf\data\ITitledObject::getTitle()
+        */
+       public function __toString() {
+               return $this->object->getTitle();
+       }
+       
+       /**
+        * Forwards calls to the decorated object.
+        * 
+        * @param       string          $name
+        * @param       mixed           $value
+        * @return      mixed
+        */
+       public function __call($name, $value) {
+               return $this->object->$name();
+       }
+       
+       /**
+        * Returns the full quote by quote id.
+        *
+        * @param       string          $quoteID
+        * @return      string
+        */
+       public function getFullQuote($quoteID) {
+               if (isset($this->fullQuotes[$quoteID])) {
+                       return $this->fullQuotes[$quoteID];
+               }
+               
+               return null;
+       }
+       
+       /**
+        * @see \Countable::count()
+        */
+       public function count() {
+               return count($this->quotes);
+       }
+       
+       /**
+        * @see \Iterator::current()
+        */
+       public function current() {
+               $objectID = $this->indexToObject[$this->index];
+               return $this->quotes[$objectID];
+       }
+       
+       /**
+        * CAUTION: This methods does not return the current iterator index,
+        * rather than the object key which maps to that index.
+        *
+        * @see \Iterator::key()
+        */
+       public function key() {
+               return $this->indexToObject[$this->index];
+       }
+       
+       /**
+        * @see \Iterator::next()
+        */
+       public function next() {
+               ++$this->index;
+       }
+       
+       /**
+        * @see \Iterator::rewind()
+        */
+       public function rewind() {
+               $this->index = 0;
+       }
+       
+       /**
+        * @see \Iterator::valid()
+        */
+       public function valid() {
+               return isset($this->indexToObject[$this->index]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/util/MessageUtil.class.php b/wcfsetup/install/files/lib/util/MessageUtil.class.php
new file mode 100644 (file)
index 0000000..834ab37
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+namespace wcf\util;
+use wcf\system\Callback;
+use wcf\system\Regex;
+
+/**
+ * Contains message-related functions.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf.message
+ * @subpackage util
+ * @category   Community Framework
+ */
+class MessageUtil {
+       /**
+        * Strips session links, html entities and \r\n from the given text.
+        * 
+        * @param       string          $text
+        * @return      string
+        */
+       public static function stripCrap($text) {
+               // strip session links, security tokens and access tokens       
+               $text = Regex::compile('(?<=\?|&)([st]=[a-f0-9]{40}|at=\d+-[a-f0-9]{40})')->replace($text, '');
+               
+               // convert html entities (utf-8)
+               $text = Regex::compile('&#(3[2-9]|[4-9][0-9]|\d{3,5});')->replace($text, new Callback(function ($matches) {
+                       return StringUtil::getCharacter(intval($matches[1]));
+               }));
+               
+               // unify new lines
+               $text = StringUtil::unifyNewlines($text);
+               
+               return $text;
+       }
+}
diff --git a/wcfsetup/install/files/style/message.less b/wcfsetup/install/files/style/message.less
new file mode 100644 (file)
index 0000000..494417e
--- /dev/null
@@ -0,0 +1,971 @@
+/* ### message groups ### */
+.messageGroupList {
+       .columnSubject {
+               > .labelList {
+                       float: right;
+                       padding-left: 7px;
+               }
+               
+               > h3 {
+                       > .messageGroupLink {
+                               font-size: @wcfTitleFontSize;
+                       }
+                       
+                       > .badge.label {
+                               top: -2px;
+                       }
+               } 
+               
+               > small {
+                       display: block;
+               }
+               
+               > nav {
+                       font-size: @wcfSmallFontSize;
+                       
+                       > ul > li {
+                               display: inline;
+                       }
+               }
+       }
+       
+       tr {
+               &.new .columnSubject > h3 > .messageGroupLink {
+                       font-weight: bold;
+               }
+               
+               &.new .columnAvatar div > p > img,
+               &:hover .columnAvatar div > p > img {
+                       opacity: 1;
+               }
+               
+               &.messageDisabled {
+                       color: @wcfDisabledColor;
+                       
+                       > td {
+                               background-color: @wcfDisabledBackgroundColor !important;
+                       }
+                       
+                       a:not(.badge) {
+                               color: @wcfDisabledColor;
+                       }
+               }
+               
+               &.messageDeleted {
+                       color: @wcfDeletedColor;
+                       
+                       > td {
+                               background-color: @wcfDeletedBackgroundColor !important;
+                       }
+                       
+                       a:not(.badge) {
+                               color: @wcfDeletedColor;
+                       }
+               }
+               
+               .columnSubject .statusDisplay .pageNavigation {
+                       opacity: 0;
+                       
+                       .transition(opacity, .2s);
+               }
+               
+               &:hover .columnSubject .statusDisplay .pageNavigation {
+                       opacity: 1;
+               }
+               
+               &.new .columnAvatar > div {
+                       &:after {
+                               color: @wcfLinkColor;
+                               content: "\f069";
+                               font-family: FontAwesome;
+                               font-weight: normal !important;
+                               font-style: normal !important;
+                               font-size: 14px;
+                               position: absolute;
+                               text-decoration: none !important;
+                               top: -4px;
+                               right: -2px;
+                               
+                               .textShadow(@wcfContainerBackgroundColor);
+                       }
+               }
+       }
+       
+       .columnAvatar {
+               div {
+                       position: relative;
+                       width: 40px;
+                       height: 38px;
+                       
+                       > p > img {
+                               opacity: .6;
+                               
+                               .transition(opacity, .2s);
+                       }
+               }
+               
+               .myAvatar {
+                       position: absolute;
+                       width: 16px;
+                       height: 16px;
+                       bottom: -2px;
+                       left: 24px;
+                       opacity: 1;
+                       
+                       .boxShadow(0, 0, rgba(0, 0, 0, .3), 3px);
+               }
+       }
+       
+       .columnLastPost {
+               white-space: nowrap;
+               
+               > div > div > small {
+                       color: @wcfDimmedColor;
+               }
+       }
+}
+
+/* ### messages ### */
+@media only screen and (min-width: 801px) {
+       .messageList {
+               .messageGroupStarter {
+                       position: relative;
+                       
+                       > .message:after {
+                               content: "\f005";
+                               font-family: FontAwesome;
+                               font-size: 14px;
+                               display: block;
+                               left: 4px;
+                               position: absolute;
+                               top: 2px;
+                               
+                               .textShadow(@wcfSidebarBackgroundColor);
+                       }
+                       
+                       > .message.messageSidebarOrientationRight:after {
+                               left: auto;
+                               right: 4px;
+                       }
+               }
+       }
+}
+
+.message {
+       background-color: @wcfContainerHoverBackgroundColor;
+       border: 1px solid @wcfContainerBorderColor;
+       //overflow: hidden; /* todo: fixes floating issues when using message on pages with a sidebar */
+       position: relative;
+       
+       &:hover {
+               .messageHeader .messageQuickOptions > li > a {
+                       opacity: 1;
+               }
+               
+               .messageOptions nav {
+                       opacity: 1;
+               }
+       }
+       
+       &.messageDisabled {
+               background-color: @wcfDisabledBackgroundColor;
+               
+               .messageSidebar {
+                       color: @wcfDisabledColor;
+                       
+                       a {
+                               color: @wcfDisabledColor;
+                       }
+               }
+       }
+       
+       &.messageDeleted {
+               background-color: @wcfDeletedBackgroundColor;
+               
+               .messageSidebar {
+                       color: @wcfDeletedColor;
+                       
+                       a {
+                               color: @wcfDeletedColor;
+                       }
+               }
+       }
+       
+       &.jsMarked {
+               background-color: @wcfSelectedBackgroundColor;
+               
+               .messageSidebar {
+                       color: @wcfSelectedColor;
+                       
+                       a {
+                               color: @wcfSelectedColor;
+                       }
+               }
+       }
+       
+       .messageOptions {
+               font-size: @wcfSmallFontSize;
+               position: relative;
+               
+               &.forceHidden nav {
+                       opacity: 0 !important;
+               }
+               
+               &.forceOpen nav {
+                       opacity: 1;
+               }
+               
+               nav {
+                       bottom: -2px;
+                       opacity: 0;
+                       position: absolute;
+                       right: -22px;
+                       text-align: right;
+                       
+                       .transition(opacity, .1s);
+                       
+                       ul.smallButtons {
+                               > li {
+                                       a.button {
+                                               .borderRadius(0);
+                                       }
+                               }
+                       }
+               }
+       }
+       
+       .messageHeader {
+               .messageQuickOptions {
+                       float: right;
+                       
+                       > li {
+                               display: inline-block;
+                               
+                               > a {
+                                       opacity: .6;
+                                       
+                                       .transition(opacity, .2s);
+                                       
+                                       > span.icon {
+                                               color: @wcfDimmedColor;
+                                               
+                                               .transition(color, .2s);
+                                       }
+                                       
+                                       &:hover {
+                                               > span.icon {
+                                                       color: @wcfLinkColor;
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       input[type=checkbox] {
+                               position: relative;
+                               top: 1px;
+                       }
+               }
+               
+               .permalink {
+                       color: @wcfDimmedColor;
+               }
+       }
+       
+       &.dividers {
+               .userCredits {
+                       border-top: 1px solid @wcfContainerBorderColor;
+               }
+       }
+}
+
+.touch .message .messageOptions nav {
+       opacity: 1;
+}
+
+@media only screen and (max-width: 800px) {
+       .message {
+               border-width: 1px 0;
+       }
+}
+
+/* sidebars orientations */
+.message.messageSidebarOrientationLeft {
+       .messageContent {
+               border-left: 1px solid @wcfContainerBorderColor;
+               margin: 0 0 0 211px;
+       }
+       
+       .messageSidebar {
+               float: left;
+       }
+}
+
+.message.messageSidebarOrientationRight {
+       .messageContent {
+               border-right: 1px solid @wcfContainerBorderColor;
+               margin: 0 211px 0 0;
+       }
+       
+       .messageSidebar {
+               float: right;
+       }
+}
+
+/* pointer */
+.message.messageSidebarOrientationLeft,
+.message.messageSidebarOrientationRight {
+       .messageHeader {
+               &:before,
+               &:after {
+                       border-width: 20px;
+                       content: "";
+                       display: block;
+                       height: 0;
+                       position: absolute;
+                       top: 35px;
+                       width: 0;
+               }
+               
+               &:before {
+                       z-index: 100;
+               }
+               
+               &:after {
+                       z-index: 101;
+               }
+       }
+}
+
+.message.messageSidebarOrientationLeft {
+       .messageHeader {
+               &:before,
+               &:after {
+                       border-style: inset solid inset none;
+               }
+               
+               &:before {
+                       border-color: transparent @wcfContainerBorderColor transparent transparent;
+                       left: -20px;
+               }
+               
+               &:after {
+                       border-color: transparent @wcfContainerBackgroundColor transparent transparent;
+                       left: -19px;
+               }
+       }
+}
+
+.message.messageSidebarOrientationRight {
+       .messageHeader {
+               &:before,
+               &:after {
+                       border-style: inset none inset solid;
+               }
+               
+               &:before {
+                       border-color: transparent transparent transparent @wcfContainerBorderColor;
+                       right: -20px;
+               }
+               
+               &:after {
+                       border-color: transparent transparent transparent @wcfContainerBackgroundColor;
+                       right: -19px;
+               }
+       }
+}
+
+/* new message badge */
+.message .newMessageBadge {
+       color: @wcfTabularBoxColor;
+       display: block;
+       font-size: @wcfSmallFontSize;
+       font-weight: bold;
+       padding: 6px 10px;
+       position: absolute;
+       text-transform: uppercase;
+       top: 24px;
+                       
+       .boxShadow(1px, 1px, rgba(0, 0, 0, .2), 3px);
+       .linearGradient(darken(@wcfTabularBoxBackgroundColor, 10%), @wcfTabularBoxBackgroundColor, darken(@wcfTabularBoxBackgroundColor, 10%));
+       .textShadow(darken(@wcfTabularBoxBackgroundColor, 10%));
+       
+       &:before {
+               border-bottom: 4px solid darken(@wcfTabularBoxBackgroundColor, 20%);
+               content: "";
+               display: block;
+               position: absolute;
+               top: -4px;
+       }
+}
+
+.message.messageSidebarOrientationLeft .newMessageBadge {
+       left: -219px;
+       
+       .borderRadius(0, 5px, 5px, 0);
+       
+       &:before {
+               border-left: 6px solid transparent;
+               left: 0;
+       }
+}
+
+.message.messageSidebarOrientationRight        .newMessageBadge {
+       right: -219px;
+}
+
+.message.messageReduced .newMessageBadge {
+       right: -7px;
+       top: 21px;
+}
+
+.message.messageSidebarOrientationRight,
+.message.messageReduced {
+       .newMessageBadge {
+               .borderRadius(5px, 0, 0, 5px);
+               
+               &:before {
+                       border-right: 6px solid transparent;
+                       right: 0;
+               }
+       }
+}
+.message .messageBody {
+       color: @wcfColor;
+       display: block;
+       line-height: 1.5;
+       padding: @wcfGapMedium @wcfGapLarge 1px;
+       /*position: relative;*/
+       
+       > div:not(.messageFooter) {
+               border-top: 1px dotted @wcfContainerBorderColor;
+               overflow: hidden;
+               padding: @wcfGapMedium 0;
+       }
+       
+       > footer {
+               padding-bottom: @wcfGapMedium;
+       }
+       
+       > .messageSignature {
+               color: @wcfDimmedColor;
+       }
+       
+       .messageFooter {
+               > *:not(:first-child) {
+                       margin-top: @wcfGapSmall;
+               }
+               
+               > .messageFooterNote {
+                       border-left: 2px solid @wcfContainerBorderColor;
+                       color: @wcfDimmedColor;
+                       font-size: @wcfSmallFontSize;
+                       padding: @wcfGapTiny @wcfGapSmall;
+                               
+                       @messageFooterNoteGradientColor: fade(@wcfContainerBorderColor, 20%);
+                       .linearGradientNative(~"left top, @{messageFooterNoteGradientColor} 0%, transparent 40%");
+               }
+       }
+}
+
+.message .messageContent {
+       background-color: @wcfContainerBackgroundColor;
+       
+       .messageHeader {
+               padding: @wcfGapMedium @wcfGapLarge 0;
+               position: relative;
+               
+               .messageHeadline {
+                       > h1 {
+                               color: @wcfColor;
+                               font-size: @wcfSubHeadlineFontSize;
+                               font-weight: bold;
+                               overflow: hidden;
+                               padding-right: 21px; // reserved space for new badge
+                               text-overflow: ellipsis;
+                               
+                               .textShadow(@wcfContainerBackgroundColor);
+                               
+                               + p {
+                                       margin-top: 2px;
+                               }
+                       }
+                       
+                       > p {
+                               font-size: @wcfSmallFontSize;
+                               
+                               > .likesBadge {
+                                       font-size: 100%;
+                                       margin: -2px 0 -1px 4px;
+                               }
+                               
+                               > .username:after {
+                                       content: " - ";
+                               }
+                       }
+               }
+               
+               .box32 > .messageHeadline > p:first-child {
+                       font-size: 100%;
+                       
+                       > .username {
+                               font-size: @wcfTitleFontSize;
+                               font-weight: bold;
+                               
+                               .textShadow(@wcfContainerBackgroundColor);
+                       }
+                       
+                       > .username {
+                               display: block;
+                       }
+                       
+                       > .username:after {
+                               content: "";
+                       }
+                       
+                       > .likesBadge {
+                               font-size: @wcfSmallFontSize;
+                               top: -1px;
+                       }
+               }
+       }
+}
+
+.message .messageSidebar {
+       line-height: 1.3;
+       margin-bottom: -1px;
+       padding: @wcfGapMedium @wcfGapLarge @wcfGapLarge;
+       position: relative;
+       text-align: center;
+       width: 170px; /* Width toggle */
+       
+       &:after {
+               clear: both;
+               content: '';
+               display: block;
+       }
+       
+       header .username {
+               color: @wcfLinkColor;
+               font-size: @wcfTitleFontSize;
+               font-weight: bold;
+               padding: 0 3px 1px;
+               
+               .textShadow(@wcfContainerHoverBackgroundColor);
+               
+               a {
+                       text-decoration: none;
+               }
+       }
+       
+       .userTitle {
+               margin: 7px 0 0;
+       }
+       
+       .userRank {
+               margin: 2px 0 0;
+       }
+       
+       .userAvatar {
+               display: inline-block;
+               margin: @wcfGapSmall 0 0;
+               position: relative;
+               text-align: left;
+               
+               > .badgeOnline {
+                       color: rgba(238, 255, 238, 1);
+                       bottom: 7px;
+                       left: -5px;
+                       position: absolute;
+                       text-transform: uppercase;
+                       
+                       .borderRadius(0, 5px, 5px, 0);
+                       .boxShadow(1px, 1px, rgba(0, 0, 0, .2), 3px);
+                       .linearGradient(darken(rgba(0, 153, 0, 1), 10%), rgba(0, 153, 0, 1), darken(rgba(0, 153, 0, 1), 10%));
+                       .textShadow(darken(rgba(0, 153, 0, 1), 10%));
+                       
+                       &:before {
+                               border-bottom: 4px solid darken(rgba(0, 153, 0, 1), 20%);
+                               border-left: 6px solid transparent;
+                               content: "";
+                               display: block;
+                               left: 0;
+                               position: absolute;
+                               top: -4px;
+                       }
+               }
+       }
+       
+       .userCredits {
+               font-size: @wcfSmallFontSize;
+               margin: @wcfGapSmall 0 0;
+               overflow: hidden;
+               padding: @wcfGapSmall 0 0;
+               
+               .dataList {
+                       > dt {
+                               width: 50%;
+                       }
+                       
+                       > dd {
+                               margin-left: 53%;
+                       }
+               }
+       }
+}
+
+.message:not(.messageReduced) .messageOptions {
+       .clearfix();
+}
+
+.message:not(.messageReduced) .messageBody {
+       .clearfix();
+}
+
+li:nth-child(2n+1) .message {
+       &.messageSidebarOrientationLeft .messageHeader:after {
+               border-right-color: @wcfContainerAccentBackgroundColor;
+       }
+       
+       &.messageSidebarOrientationRight .messageHeader:after {
+               border-left-color: @wcfContainerAccentBackgroundColor;
+       }
+       
+       .messageContent {
+               background-color: @wcfContainerAccentBackgroundColor;
+       }
+}
+
+.messageReduced {
+       .messageOptions > .breadcrumbs {
+               bottom: 10px;
+               left: 0;
+               opacity: 1;
+               position: relative;
+       }
+}
+
+.messageCollapsed {
+       color: @wcfDimmedColor;
+       opacity: .8;
+       padding: @wcfGapMedium @wcfGapLarge;
+
+       .transition(opacity, .1s);
+       
+       &:hover {
+               opacity: 1;
+       }
+       
+       &.messageCollapsedExpandable {
+               cursor: pointer;
+       }
+       
+       h1 {
+               font-size: @wcfSmallFontSize;
+       }
+       
+       .messageCounter {
+               padding-top: 3px;
+       }
+       
+       &.jsMarked {
+               background-color: @wcfSelectedBackgroundColor !important;
+               color: @wcfColor;
+               
+               a {
+                       color: @wcfColor;
+               }
+       }
+}
+
+@media only screen and (max-width: 800px) {
+       .messageCollapsed {
+               padding: @wcfGapSmall;
+       }
+}
+
+@media only screen and (min-width: 641px) and (max-width: 800px) {
+       .messageCollapsed {
+               padding: @wcfGapSmall @wcfGapMedium;
+       }
+}
+
+/* quick reply and inline editor */
+.messageBody > span.icon-spinner {
+       left: 50%;
+       margin: -21px -21px 0 0;
+       position: absolute;
+       top: 50%;
+}
+
+#messageQuickReply {
+       .formSubmit {
+               padding-bottom: @wcfGapMedium;
+       }
+}
+
+/* message quotes */
+#showQuotes {
+       bottom: @wcfGapLarge + @wcfGapTiny;
+       cursor: pointer;
+       opacity: .7;
+       position: fixed;
+       right: @wcfGapLarge + @wcfGapTiny;
+       
+       .transition(opacity, .2s);
+       
+       &:hover {
+               opacity: 1;
+       }
+}
+
+#messageQuoteList {
+       max-width: 800px !important;
+
+       li {
+               &:not(:first-child) {
+                       margin-top: @wcfGapSmall;
+               }
+               
+               > span {
+                       float: left;
+                       width: 40px;
+                       
+                       > input {
+                               vertical-align: bottom;
+                       }
+                       
+                       > span {
+                               cursor: pointer;
+                               vertical-align: middle;
+                       }
+               }
+               
+               div.jsQuote {
+                       margin-left: 60px;
+               }
+               
+               div.jsFullQuote {
+                       display: none;
+               }
+       }
+}
+
+#quoteManagerCopy {
+       cursor: pointer;
+       
+       .pointer {
+               border-width: 5px 5px 0;
+               bottom: -5px;
+               margin-left: -5px;
+               top: auto;
+       }
+}
+
+/* share buttons */
+.messageShareButtons {
+       > ul > li {
+               display: inline-block;
+               
+               > a {
+                       text-decoration: none;
+                       
+                       > .icon {
+                               height: 28px;
+                       }
+               }
+               
+               > .badge {
+                       background-color: @wcfContainerBackgroundColor;
+                       border: 1px solid @wcfContainerBorderColor;
+                       color: @wcfColor;
+                       line-height: 23px;
+                       padding: 0 7px;
+                       position: relative;
+                       vertical-align: 1px;
+                       
+                       .borderRadius(3px);
+                       
+                       &:before {
+                               border: 6px solid @wcfContainerBorderColor;
+                               border-color: transparent @wcfContainerBorderColor transparent transparent;
+                               content: "";
+                               display: block;
+                               height: 0;
+                               margin-top: -6px;
+                               position: absolute;
+                               right: 100%;
+                               top: 50%;
+                               width: 0;
+                       }
+                       
+                       &:after {
+                               border: 6px solid @wcfContainerBackgroundColor;
+                               border-color: transparent @wcfContainerBackgroundColor transparent transparent;
+                               content: "";
+                               display: block;
+                               height: 0;
+                               margin-right: -1px;
+                               margin-top: -6px;
+                               position: absolute;
+                               right: 100%;
+                               top: 50%;
+                               width: 0;
+                       }
+               }
+       }
+       
+       .jsShareFacebook {
+               > a > .icon {
+                       color: rgb(59, 89, 152);
+               }
+       }
+       
+       .jsShareTwitter {
+               > a > .icon {
+                       color: rgb(64, 153, 255);
+               }
+       }
+       
+       .jsShareGoogle {
+               > a > .icon {
+                       color: rgb(211, 72, 54);
+               }
+       }
+       
+       .jsShareReddit {
+               > a > img {
+                       vertical-align: -7px;
+                       padding: 0 4px;
+               }
+       }
+}
+
+.contentNavigation > .messageShareButtons {
+       float: right;
+       margin-right: @wcfGapMedium;
+       margin-top: 0;
+}
+
+/* ckeditor fixes */
+.cke_editor_text {
+       border-style: solid !important;
+       padding: 0 !important;
+}
+
+.cke_source {
+       padding: 8px !important;
+}
+
+.cke_combo__fontsize .cke_combo_text {
+       width: auto !important;
+}
+
+@media only screen and (max-width: 800px) {
+       .message.messageSidebarOrientationLeft,
+       .message.messageSidebarOrientationRight {
+               .messageContent {
+                       border: 0;
+                       margin: 0;
+               }
+               
+               .messageSidebar {
+                       float: none;
+               }
+               
+               .messageHeader {
+                       &:before,
+                       &:after {
+                               display: none;
+                       }
+               }
+       }
+       
+       .message .messageHeader .messageQuickOptions,
+       .message .messageBody .messageSignature,
+       .message .messageSidebar .userCredits {
+               display: none;
+       }       
+       
+       .message .messageSidebar {
+               padding: 7px;
+               text-align: left;
+               width: auto;
+               
+               > div {
+                       margin-left: 40px;
+               }
+               
+               .userAvatar {
+                       left: 7px;
+                       position: absolute;
+                       top: 0;
+                       
+                       img {
+                               height: 32px !important;
+                               width: 32px !important;
+                       }
+                       
+                       > .badgeOnline {
+                               display: none;
+                       }
+               }
+               
+               .userTitle {
+                       margin-top: -2px;
+               }
+       }
+       
+       /* reduce paddings */
+       .message .messageContent .messageHeader {
+               padding: 7px 7px 0;
+       }
+       
+       .message .messageBody {
+               padding: 7px;
+       }
+       
+       .message .messageBody > div:not(.messageFooter) {
+               padding: 7px 0;
+       }
+       
+       .message .messageBody > footer {
+               padding: 0;
+               position: absolute;
+               right: 7px;
+               top: 7px;
+       }
+       
+       .message .messageOptions nav {
+               opacity: 1;
+               position: static;
+               text-align: left;
+       }
+       
+       .message.messageReduced .messageOptions {
+               display: none;
+       }
+       
+       .message .newMessageBadge {
+               display: none;
+       }
+}
+
+@media only screen and (min-width: 641px) and (max-width: 800px) {
+       .message .messageSidebar,
+       .message .messageContent .messageHeader,
+       .message .messageBody {
+               padding-left: @wcfGapMedium;
+               padding-right: @wcfGapMedium;
+       }
+       
+       .message .messageSidebar {
+               .userAvatar {
+                       left: @wcfGapMedium;
+               }
+       }
+       
+       .message .messageBody > footer {
+               right: @wcfGapMedium;
+       }
+}
diff --git a/wcfsetup/install/files/style/poll.less b/wcfsetup/install/files/style/poll.less
new file mode 100644 (file)
index 0000000..3d9be7d
--- /dev/null
@@ -0,0 +1,74 @@
+#pollOptionContainer .sortableList {
+       padding: @wcfGapSmall 0;
+       
+       .sortableNode {
+               margin-top: @wcfGapSmall;
+               
+               .sortableButtonContainer > img {
+                       cursor: pointer;
+                       margin-right: @wcfGapMedium;
+               }
+       }
+}
+
+.pollContainer {
+       float: left;
+       margin: 0 @wcfGapMedium @wcfGapSmall 0;
+       max-width: 50%;
+       min-width: 300px;
+       
+       > .formSubmit {
+               background-color: @wcfContainerAccentBackgroundColor;
+               border-top: 1px solid @wcfContainerBorderColor;
+               margin: @wcfGapMedium -@wcfGapLarge -@wcfGapMedium -@wcfGapLarge;
+               padding: 10px 0;
+       }
+}
+
+.pollResultList {
+       li {
+               margin-bottom: 8px;
+               padding: 1px 0;
+               position: relative;
+               z-index: 0;
+               
+               .transition(background-color, .1s);
+               
+               &:last-child {
+                       margin-bottom: 0px;
+               }
+               
+               &:hover {
+                       background-color: @wcfContainerAccentBackgroundColor;
+                       
+                       .borderRadius(0, 5px, 5px, 0);
+               }
+               
+               .pollMeter {
+                       background-color: @wcfContainerHoverBackgroundColor;
+                       height: 100%;
+                       left: 0;
+                       position: absolute;
+                       top: 0;
+                       z-index: -1;
+                       
+                       .borderRadius(0, 5px, 5px, 0);
+               }
+               
+               .caption {
+                       color: @wcfLinkColor;
+                       padding: 2px 0;
+                       
+                       .optionName {
+                               display: inline-block;
+                               padding: 0 2.5em 0 @wcfGapSmall;
+                       }
+                       
+                       .relativeVotes {
+                               position: absolute;
+                               right: 7px;
+                               top: 3px;
+                       }
+               }
+       }
+}
\ No newline at end of file
index 9e0bc0b60f0aa31976a59319aa1d48379f480412..761302b382f5d2fcce17130aca621a1f80203fb7 100644 (file)
                <item name="wcf.acp.group.option.admin.content.bbcode.canManageBBCode"><![CDATA[Kann BBCodes verwalten]]></item>
                <item name="wcf.acp.group.option.category.admin.content.smiley"><![CDATA[Smileys]]></item>
                <item name="wcf.acp.group.option.admin.content.smiley.canManageSmiley"><![CDATA[Kann Smileys verwalten]]></item>
+               <item name="wcf.acp.group.option.user.message.canUseSmilies"><![CDATA[Kann Smileys benutzen]]></item>
+               <item name="wcf.acp.group.option.user.message.canUseHtml"><![CDATA[Kann HTML benutzen]]></item>
+               <item name="wcf.acp.group.option.user.message.canUseBBCodes"><![CDATA[Kann BBCodes benutzen]]></item>
+               <item name="wcf.acp.group.option.user.message.allowedBBCodes"><![CDATA[Erlaubte BBCodes]]></item>
+               <item name="wcf.acp.group.option.user.message.allowedBBCodes.description"><![CDATA[Die hier ausgewählten BBCodes dürfen von Mitglieder dieser Benutzergruppe verwendet werden.]]></item>
        </category>
        
        <category name="wcf.acp.index">
                <item name="wcf.acp.option.attachment_storage.description"><![CDATA[Sie können optional einen vom Standard abweichenden Speicherort für die Dateianhänge definieren. Bereits existierende Dateianhänge müssen bei einer Änderung des Speicherortes manuell in den neuen Ort übertragen werden.]]></item>
                <item name="wcf.acp.option.module_attachment"><![CDATA[Dateianhänge]]></item>
                <item name="wcf.acp.option.module_smiley"><![CDATA[Smileys]]></item>
+               <item name="wcf.acp.option.category.message.censorship"><![CDATA[Zensur-Funktion]]></item>
+               <item name="wcf.acp.option.censored_words"><![CDATA[Zu zensierende Wörter]]></item>
+               <item name="wcf.acp.option.censored_words.description"><![CDATA[Ein Wort pro Zeile. Sollte bei der Erstellung einer Nachricht eines dieser Wörter verwendet werden, so wird die Erstellung verweigert.<br />
+<em>Verwenden Sie „*“, um Wortteile zu finden: „wolt*“ findet auch „woltlab“</em><br />
+<em>Verwenden Sie „~“, um Worttrennungen zu finden: „wolt~“ findet auch „wolt-lab“</em>]]></item>
+               <item name="wcf.acp.option.enable_censorship"><![CDATA[Zensur aktivieren]]></item>
+               <item name="wcf.acp.option.enable_censorship.description"><![CDATA[Aktiviert die Zensur der unten angegebenen Wörter in allen von Benutzern geschriebenen Texten.]]></item>
+               <item name="wcf.acp.option.category.message.general.share"><![CDATA[Teilen]]></item>
+               <item name="wcf.acp.option.enable_bbcodes_default_value"><![CDATA[Darstellung von BBCodes aktivieren [Vorgabewert]]]></item>
+               <item name="wcf.acp.option.enable_html_default_value"><![CDATA[Darstellung von HTML aktivieren [Vorgabewert]]]></item>
+               <item name="wcf.acp.option.enable_smilies_default_value"><![CDATA[Darstellung von Smileys aktivieren [Vorgabewert]]]></item>
+               <item name="wcf.acp.option.pre_parse_default_value"><![CDATA[URLs automatisch erkennen [Vorgabewert]]]></item>
+               <item name="wcf.acp.option.show_signature_default_value"><![CDATA[Signatur anzeigen [Vorgabewert]]]></item>
+               <item name="wcf.acp.option.enable_share_buttons"><![CDATA[Buttons zum Teilen von Inhalten anzeigen]]></item>
+               <item name="wcf.acp.option.share_buttons_show_count"><![CDATA[Anzahl der Teilungen anzeigen]]></item>
        </category>
        
        <category name="wcf.acp.package">
@@ -1269,6 +1289,46 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getAllowedExtensions()
                <item name="wcf.imageViewer.next"><![CDATA[Nächstes Bild]]></item>
                <item name="wcf.imageViewer.previous"><![CDATA[Vorheriges Bild]]></item>
        </category>
+       
+       <category name="wcf.message">
+               <item name="wcf.message.bbcode.code.copy"><![CDATA[Inhalt kopieren]]></item>
+               <item name="wcf.message.quote.insertAllQuotes"><![CDATA[Alle Zitate einfügen]]></item>
+               <item name="wcf.message.quote.insertQuote"><![CDATA[Zitat einfügen]]></item>
+               <item name="wcf.message.quote.insertSelectedQuotes"><![CDATA[Markierte Zitate einfügen]]></item>
+               <item name="wcf.message.quote.manageQuotes"><![CDATA[Zitate verwalten]]></item>
+               <item name="wcf.message.quote.quoteSelected"><![CDATA[Auswahl zitieren]]></item>
+               <item name="wcf.message.quote.showQuotes"><![CDATA[Zitate (#count#)]]></item>
+               <item name="wcf.message.quote.quoteMessage"><![CDATA[Zitieren]]></item>
+               <item name="wcf.message.quote.removeAllQuotes"><![CDATA[Alle Zitate entfernen]]></item>
+               <item name="wcf.message.quote.removeSelectedQuotes"><![CDATA[Markierte Zitate entfernen]]></item>
+               <item name="wcf.message.settings"><![CDATA[Einstellungen]]></item>
+               <item name="wcf.message.settings.enableBBCodes"><![CDATA[Darstellung von BBCodes aktivieren]]></item>
+               <item name="wcf.message.settings.enableBBCodes.description"><![CDATA[Sie können BBCodes zur Formatierung nutzen, sofern diese Option aktiviert ist.]]></item>
+               <item name="wcf.message.settings.enableHtml"><![CDATA[Darstellung von HTML aktivieren]]></item>
+               <item name="wcf.message.settings.enableHtml.description"><![CDATA[Wenn Sie diese Option aktiviert haben, können Sie HTML zur Formatierung verwenden.]]></item>
+               <item name="wcf.message.settings.enableSmilies"><![CDATA[Darstellung von Smileys aktivieren]]></item>
+               <item name="wcf.message.settings.enableSmilies.description"><![CDATA[Smiley-Code wird in Ihrem Beitrag automatisch als Smiley-Grafik dargestellt.]]></item>
+               <item name="wcf.message.settings.preParse"><![CDATA[URLs automatisch erkennen]]></item>
+               <item name="wcf.message.settings.preParse.description"><![CDATA[Internet-Adressen werden automatisch erkannt und umgewandelt.]]></item>
+               <item name="wcf.message.settings.showSignature"><![CDATA[Signatur anzeigen]]></item>
+               <item name="wcf.message.settings.showSignature.description"><![CDATA[Die im Profil eingestellte Signatur wird an diese Nachricht angehängt.]]></item>
+               <item name="wcf.message.share"><![CDATA[Teilen]]></item>
+               <item name="wcf.message.share.facebook"><![CDATA[Facebook]]></item>
+               <item name="wcf.message.share.google"><![CDATA[Google Plus]]></item>
+               <item name="wcf.message.share.permalink"><![CDATA[Permalink]]></item>
+               <item name="wcf.message.share.permalink.bbcode"><![CDATA[BBCode]]></item>
+               <item name="wcf.message.share.permalink.html"><![CDATA[HTML]]></item>
+               <item name="wcf.message.share.reddit"><![CDATA[Reddit]]></item>
+               <item name="wcf.message.share.twitter"><![CDATA[Twitter]]></item>
+               <item name="wcf.message.smilies"><![CDATA[Smileys]]></item>
+               <item name="wcf.message.button.extendedReply"><![CDATA[Erweiterte Antwort]]></item>
+               <item name="wcf.message.button.extendedEdit"><![CDATA[Erweiterte Bearbeitung]]></item>
+               <item name="wcf.message.new"><![CDATA[Neu]]></item>
+               <item name="wcf.message.error.censoredWordsFound"><![CDATA[Ihre Nachricht enthält folgende zensierte Wörter: {implode from=$censoredWords key=censoredWord item=number}{$censoredWord}{if $number > 1} ({#$number}×){/if}{/implode}]]></item>
+               <item name="wcf.message.error.disallowedBBCodes"><![CDATA[Ihre Nachricht enthält die folgenden BBCodes, die Sie nicht verwenden dürfen: {implode from=$disallowedBBCodes item=disallowedBBCode}{$disallowedBBCode}{/implode}]]></item>
+               <item name="wcf.message.error.editorAlreadyInUse"><![CDATA[Der Editor ist bereits aktiv, beenden Sie die Bearbeitung bevor Sie fortfahren.]]></item>
+               <item name="wcf.message.error.tooLong"><![CDATA[Ihre Nachricht ist zu lang. Es stehen maximal {#$maxTextLength} Zeichen zur Verfügung.]]></item>
+       </category>
                
        <category name="wcf.page">
                <item name="wcf.page.pageNo"><![CDATA[Seite {#$pageNo}]]></item>
index d3fd5ca184d3b4a66478ef18d7a9454b20ebf5b7..794b2c0cece0c0dd66de80d1efcdc49ebb4555be 100644 (file)
@@ -245,6 +245,11 @@ Examples for medium ID detection:
                <item name="wcf.acp.group.option.admin.content.bbcode.canManageBBCode"><![CDATA[Can manage BBCodes]]></item>
                <item name="wcf.acp.group.option.category.admin.content.smiley"><![CDATA[Smilies]]></item>
                <item name="wcf.acp.group.option.admin.content.smiley.canManageSmiley"><![CDATA[Can manage smilies]]></item>
+               <item name="wcf.acp.group.option.user.message.canUseSmilies"><![CDATA[Can use smilies]]></item>
+               <item name="wcf.acp.group.option.user.message.canUseHtml"><![CDATA[Can use HTML]]></item>
+               <item name="wcf.acp.group.option.user.message.canUseBBCodes"><![CDATA[Can use BBCodes]]></item>
+               <item name="wcf.acp.group.option.user.message.allowedBBCodes"><![CDATA[Allowed BBCodes]]></item>
+               <item name="wcf.acp.group.option.user.message.allowedBBCodes.description"><![CDATA[Selected BBCodes may be used by members of this group.]]></item>
        </category>
        
        <category name="wcf.acp.index">
@@ -541,6 +546,21 @@ Examples for medium ID detection:
                <item name="wcf.acp.option.attachment_storage.description"><![CDATA[Changes storage location for attachments. Heads up! You are responsible to move already existing attachments to the new location.]]></item>
                <item name="wcf.acp.option.module_attachment"><![CDATA[Attachments]]></item>
                <item name="wcf.acp.option.module_smiley"><![CDATA[Smilies]]></item>
+               <item name="wcf.acp.option.category.message.censorship"><![CDATA[Censorship]]></item>
+               <item name="wcf.acp.option.censored_words"><![CDATA[Censored Words]]></item>
+               <item name="wcf.acp.option.censored_words.description"><![CDATA[One word per Line. Using at least one of these words within a message causes an immediate rejection.<br />
+<em>Use “*” to match parts: “wolt*” matches “woltlab”</em><br />
+<em>Use “~” to find splitted parts: “wolt~” matches “wolt-lab”</em>]]></item>
+               <item name="wcf.acp.option.enable_censorship"><![CDATA[Enable Censorship]]></item>
+               <item name="wcf.acp.option.enable_censorship.description"><![CDATA[Enables censorship for all messages containing the words below.]]></item>
+               <item name="wcf.acp.option.category.message.general.share"><![CDATA[Share]]></item>
+               <item name="wcf.acp.option.enable_bbcodes_default_value"><![CDATA[Enable BBCodes [default value]]]></item>
+               <item name="wcf.acp.option.enable_html_default_value"><![CDATA[Enable HTML [default value]]]></item>
+               <item name="wcf.acp.option.enable_smilies_default_value"><![CDATA[Enable smilies [default value]]]></item>
+               <item name="wcf.acp.option.pre_parse_default_value"><![CDATA[Detect URLs [default value]]]></item>
+               <item name="wcf.acp.option.show_signature_default_value"><![CDATA[Show signatures [default value]]]></item>
+               <item name="wcf.acp.option.enable_share_buttons"><![CDATA[Show content share button]]></item>
+               <item name="wcf.acp.option.share_buttons_show_count"><![CDATA[Show number of shares]]></item>
        </category>
        
        <category name="wcf.acp.package">
@@ -1267,6 +1287,46 @@ Allowed extensions: {', '|implode:$attachmentHandler->getAllowedExtensions()}]]>
                <item name="wcf.imageViewer.previous"><![CDATA[Previous Image]]></item>
        </category>
                
+       <category name="wcf.message">
+               <item name="wcf.message.bbcode.code.copy"><![CDATA[Copy Contents]]></item>
+               <item name="wcf.message.quote.insertAllQuotes"><![CDATA[Insert All Quotes]]></item>
+               <item name="wcf.message.quote.insertQuote"><![CDATA[Insert Quote]]></item>
+               <item name="wcf.message.quote.insertSelectedQuotes"><![CDATA[Insert Marked Quotes]]></item>
+               <item name="wcf.message.quote.manageQuotes"><![CDATA[Manage Quotes]]></item>
+               <item name="wcf.message.quote.quoteSelected"><![CDATA[Quote Selection]]></item>
+               <item name="wcf.message.quote.showQuotes"><![CDATA[Quotes (#count#)]]></item>
+               <item name="wcf.message.quote.quoteMessage"><![CDATA[Quote]]></item>
+               <item name="wcf.message.quote.removeAllQuotes"><![CDATA[Remove All Quotes]]></item>
+               <item name="wcf.message.quote.removeSelectedQuotes"><![CDATA[Removed Marked Quotes]]></item>
+               <item name="wcf.message.settings"><![CDATA[Settings]]></item>
+               <item name="wcf.message.settings.enableBBCodes"><![CDATA[Enable BBCodes]]></item>
+               <item name="wcf.message.settings.enableBBCodes.description"><![CDATA[Allows BBCode usage once enabled.]]></item>
+               <item name="wcf.message.settings.enableHtml"><![CDATA[Enable HTML]]></item>
+               <item name="wcf.message.settings.enableHtml.description"><![CDATA[Allows HTML usage once enabled.]]></item>
+               <item name="wcf.message.settings.enableSmilies"><![CDATA[Enable smilies]]></item>
+               <item name="wcf.message.settings.enableSmilies.description"><![CDATA[Smilies will be displayed as a smiley-image.]]></item>
+               <item name="wcf.message.settings.preParse"><![CDATA[Detect links]]></item>
+               <item name="wcf.message.settings.preParse.description"><![CDATA[Links to webpages are automatically detected.]]></item>
+               <item name="wcf.message.settings.showSignature"><![CDATA[Show signature]]></item>
+               <item name="wcf.message.settings.showSignature.description"><![CDATA[Appends signature to this message, can be edited in your profile.]]></item>
+               <item name="wcf.message.share"><![CDATA[Share]]></item>
+               <item name="wcf.message.share.facebook"><![CDATA[Facebook]]></item>
+               <item name="wcf.message.share.google"><![CDATA[Google Plus]]></item>
+               <item name="wcf.message.share.permalink"><![CDATA[Permalink]]></item>
+               <item name="wcf.message.share.permalink.bbcode"><![CDATA[BBCode]]></item>
+               <item name="wcf.message.share.permalink.html"><![CDATA[HTML]]></item>
+               <item name="wcf.message.share.reddit"><![CDATA[Reddit]]></item>
+               <item name="wcf.message.share.twitter"><![CDATA[Twitter]]></item>
+               <item name="wcf.message.smilies"><![CDATA[Smilies]]></item>
+               <item name="wcf.message.button.extendedReply"><![CDATA[More Options]]></item>
+               <item name="wcf.message.button.extendedEdit"><![CDATA[More Options]]></item>
+               <item name="wcf.message.new"><![CDATA[New]]></item>
+               <item name="wcf.message.error.censoredWordsFound"><![CDATA[Message contains censored words: {implode from=$censoredWords key=censoredWord item=number}{$censoredWord}{if $number > 1} ({#$number}×){/if}{/implode}]]></item>
+               <item name="wcf.message.error.disallowedBBCodes"><![CDATA[Message contains disallowed BBCodes: {implode from=$disallowedBBCodes item=disallowedBBCode}{$disallowedBBCode}{/implode}]]></item>
+               <item name="wcf.message.error.editorAlreadyInUse"><![CDATA[Editor is already in use, please finish editing before continuing.]]></item>
+               <item name="wcf.message.error.tooLong"><![CDATA[Message is too long, must be below {#$maxTextLength} characters.]]></item>
+       </category>
+       
        <category name="wcf.page">
                <item name="wcf.page.pageNo"><![CDATA[Page {#$pageNo}]]></item>
                <item name="wcf.page.offline"><![CDATA[Page is currently in maintenance mode{if OFFLINE_MESSAGE != ''}:{else}.{/if}]]></item>