Showing 697 changed files with 111,436 additions and 3,693 deletions.
@@ -0,0 +1,13 @@
target/
work/

# Ignore Intellij Idea project files
*.iml
*.ipr
.idea
atlassian-ide-plugin.xml

# Ignore Eclipse project files
.settings
.project
.classpath
@@ -10,7 +10,7 @@ build-openfire:
clean:
cd build && ant clean

dbkg:
dpkg:
cd build && ant installer.debian

eclipse: .settings .classpath .project
@@ -0,0 +1,41 @@
Openfire
========

About
-----

[Openfire] is a XMPP server licensed under the Open Source Apache License.


[Openfire] - an [Ignite Realtime] community project.

Bug Reporting
-------------

Only a few users have access for for filling bugs in the tracker. New
users should:

1. Create a forums account (only e-mail is a requirement, you can skip all the other fields).
2. Login to a forum account
3. Press New in your toolbar and choose Discussion
4. Choose the [Openfire Dev forum](http://community.igniterealtime.org/community/developers/openfire) of Openfire and add the tag 'bug_report' to your new post

Please search for your issues in the bug tracker before reporting.

Resources
---------

- Bug Tracker: http://issues.igniterealtime.org/browse/OF
- Nightly Builds: http://www.igniterealtime.org/downloads/nightly_openfire.jsp

Ignite Realtime
===============

[Ignite Realtime] is an Open Source community composed of end-users and developers around the world who
are interested in applying innovative, open-standards-based Real Time Collaboration to their businesses and organizations.
We're aimed at disrupting proprietary, non-open standards-based systems and invite you to participate in what's already one
of the biggest and most active Open Source communities.

[Openfire]: http://www.igniterealtime.org/projects/openfire/index.jsp
[Ignite Realtime]: http://www.igniterealtime.org
[XMPP (Jabber)]: http://xmpp.org/
@@ -24,4 +24,9 @@ plugin.dev.dir=
# This property is set by default in the build.xml file as c:\Program Files\install4j so
# if you used the standard location you won't need to edit the property below.
#
# installer.install4j.home=
# installer.install4j.home=

#
# Path to a bundled JRE you wish to use with the installer.rpm ant target
#
jre.bundle.location=
@@ -1,10 +1,6 @@
<?xml version="1.0"?>

<!--
$RCSfile: build.xml,v $
$Revision$
$Date$
This software is published under the terms of the Apache License, Version 2.0,
a copy of which is included in this distribution.
-->
@@ -61,7 +57,7 @@

<property name="version.major" value="3"/>
<property name="version.minor" value="9"/>
<property name="version.revision" value="1"/>
<property name="version.revision" value="2"/>
<property name="version.extra" value=""/> <!-- For 'beta' or 'alpha' -->
<property name="dist.prefix" value="openfire"/>

@@ -709,6 +705,14 @@
<copy todir="${target.openfireHome}">
<fileset dir="${src.dir}" includes="bin/**/*"/>
</copy>
<!-- Make stuff under bin executable -->
<chmod perm="+x">
<fileset dir="${target.openfireHome}/bin" includes="**/*">
<exclude name="**/*.bat"/>
<exclude name="**/*.rc"/>
<exclude name="**/*-sysconfig"/>
</fileset>
</chmod>
<fixcrlf srcdir="${target.openfireHome}/bin" eol="lf" eof="remove" includes="*.sh,extra/*"/>

<!-- Create a logs dir in the binary release -->
@@ -1106,7 +1110,7 @@
<equals arg1="${bundle.jre}" arg2="true"/>
<then>
<!-- Include bundled jre -->
<copy todir="${target.rpm}/SOURCES" file="${basedir}/build/rpm/jre-dist.tar.gz" />
<copy todir="${target.rpm}/SOURCES" file="${jre.bundle.location}" />
</then>
</if>
<copy todir="${target.rpm}/SOURCES" file="${release.dest.dir}/${release.fullname.src}.tar.gz" />
@@ -1117,7 +1121,7 @@
<then>
<rpm specFile="openfire.spec"
topDir="${target.rpm}"
command="-ba --target i386 --define 'OPENFIRE_VERSION ${version}' --define 'OPENFIRE_SOURCE ${release.fullname.src}.tar.gz' --define 'OPENFIRE_BUILDDATE ${rpm.builddate}'"
command="-ba --target i386 --define 'JRE_BUNDLE ${jre.bundle.location}' --define 'OPENFIRE_VERSION ${version}' --define 'OPENFIRE_SOURCE ${release.fullname.src}.tar.gz' --define 'OPENFIRE_BUILDDATE ${rpm.builddate}'"
failOnError="true"
/>

@@ -1551,7 +1555,7 @@
be manually added to this list.
-->
<property name="pack200.excludes"
value="gnujaxp.jar,mail.jar,activation.jar,bouncycastle.jar,tangosol.jar"/>
value="gnujaxp.jar,mail.jar,activation.jar,bcpg-jdk15on.jar,bcpkix-jdk15on.jar,bcprov-jdk15on.jar,tangosol.jar"/>

<if>
<equals arg1="${pack200.enabled}" arg2="true" />
@@ -2,14 +2,14 @@ Source: openfire
Section: net
Priority: optional
Maintainer: Ignite Realtime Community <admin@igniterealtime.org>
Build-Depends: debhelper (>= 5), cdbs, patchutils, sun-java6-jdk | openjdk-6-jdk, ant
Build-Depends: debhelper (>= 5), cdbs, patchutils, sun-java6-jdk | oracle-j2sdk1.7 | openjdk-6-jdk | openjdk-7-jdk, ant
Standards-Version: 3.7.2
Homepage: http://www.igniterealtime.org

Package: openfire
Section: net
Priority: optional
Pre-Depends: sun-java5-jre | sun-java6-jre | default-jre-headless | openjdk-6-jre
Pre-Depends: sun-java6-jre | default-jre-headless | openjdk-6-jre | openjdk-7-jre | oracle-java7-jre
Architecture: all
Description: A high performance XMPP (Jabber) server.
Openfire is an instant messaging server that implements the XMPP
@@ -1,4 +1,5 @@
/etc/openfire/openfire.xml
/etc/openfire/security.xml
/etc/openfire/security/keystore
/etc/openfire/security/truststore
/etc/openfire/security/client.truststore
@@ -22,22 +22,11 @@

# Attempt to locate JAVA_HOME
if [ -z $JAVA_HOME ]; then
JAVA_HOMES="/usr/lib/jvm/default-java \
/usr/lib/jvm/java-7-sun \
/usr/lib/jvm/java-6-sun \
/usr/lib/jvm/java-1.5.0-sun \
/usr/lib/jvm/java-7-openjdk-amd64 \
/usr/lib/jvm/java-7-openjdk-i386 \
/usr/lib/jvm/java-7-openjdk \
/usr/lib/jvm/java-6-openjdk-amd64 \
/usr/lib/jvm/java-6-openjdk-i386 \
/usr/lib/jvm/java-6-openjdk"
for t in $JAVA_HOMES ; do
if [ -d $t ] ; then
JAVA_HOME=$t
break;
fi
done
JAVA_HOME=$(LC_ALL=C update-alternatives --display java \
| grep best \
| grep -oe \/.*\/bin\/java \
| sed 's/\/jre\/.*//g')
echo "best java alternative in: "$JAVA_HOME
fi

PATH=/sbin:/bin:/usr/sbin:/usr/bin:${JAVA_HOME}/bin
@@ -22,5 +22,6 @@ install/openfire::
cp $(TARGET)/lib/log4j.xml $(ETCDIR)
cp -r $(TARGET)/resources/database $(OPENFIRE)/resources/database
cp $(TARGET)/conf/openfire.xml $(ETCDIR)
cp $(TARGET)/conf/security.xml $(ETCDIR)
cp -r $(TARGET)/resources/security $(ETCDIR)/security
cp -r $(TARGET)/plugins $(VARDIR)/plugins
@@ -50,9 +50,11 @@
<fileEntry mountPoint="34" file="${compiler:RELEASE_FULL_PATH}/resources/security/truststore" overwrite="0" shared="false" mode="644" uninstallMode="1" />
<fileEntry mountPoint="34" file="${compiler:RELEASE_FULL_PATH}/resources/security/keystore" overwrite="0" shared="false" mode="644" uninstallMode="1" />
<fileEntry mountPoint="46" file="${compiler:RELEASE_FULL_PATH}/conf/openfire.xml" overwrite="0" shared="false" mode="644" uninstallMode="1" />
<fileEntry mountPoint="46" file="${compiler:RELEASE_FULL_PATH}/conf/security.xml" overwrite="0" shared="false" mode="644" uninstallMode="1" />
<dirEntry mountPoint="1" file="${compiler:RELEASE_FULL_PATH}" overwrite="4" shared="false" mode="644" uninstallMode="0" excludeSuffixes="" dirMode="755">
<exclude>
<entry location="conf/openfire.xml" launcher="false" />
<entry location="conf/security.xml" launcher="false" />
<entry location="resources/security/keystore" launcher="false" />
<entry location="resources/security/truststore" launcher="false" />
</exclude>
@@ -137,7 +139,7 @@
<versionLine x="20" y="40" text="version ${compiler:sys.version}" font="Arial" fontSize="8" fontColor="0,0,0" fontWeight="500" />
</text>
</splashScreen>
<java mainClass="org.jivesoftware.openfire.starter.ServerStarter" vmParameters="" arguments="-DopenfireHome=&quot;${launcher:sys.launcherDirectory}../&quot; -Dopenfire.lib.dir=&quot;$app_home/lib&quot;" allowVMPassthroughParameters="true" preferredVM="server">
<java mainClass="org.jivesoftware.openfire.starter.ServerStarter" vmParameters="-DopenfireHome=&quot;${launcher:sys.launcherDirectory}../&quot; -Dopenfire.lib.dir=&quot;$app_home/lib&quot;" arguments="" allowVMPassthroughParameters="true" preferredVM="server">
<classPath>
<scanDirectory location="lib" failOnError="true" />
</classPath>
@@ -462,7 +464,7 @@
</uninstallerStartup>
</installerGui>
<mediaSets>
<win32 name="Windows" id="3" mediaFileName="" installDir="${compiler:WINDOWS_INSTALL_DIR}" overridePrincipalLanguage="true" requires64bit="false" runPostProcessor="false" postProcessor="" failOnPostProcessorError="false" includedJRE="windows-x86-1.7.0_51" manualJREEntry="false" bundleType="1" jreURL="" jreFtpURL="" jreShared="false" customInstallBaseDir="" createUninstallIcon="true" contentFilesType="1" downloadURL="" runAsAdmin="false">
<win32 name="Windows" id="3" mediaFileName="" installDir="${compiler:WINDOWS_INSTALL_DIR}" overridePrincipalLanguage="true" requires64bit="false" runPostProcessor="false" postProcessor="" failOnPostProcessorError="false" includedJRE="windows-x86-1.7.0_55" manualJREEntry="false" bundleType="1" jreURL="" jreFtpURL="" jreShared="false" customInstallBaseDir="" createUninstallIcon="true" contentFilesType="1" downloadURL="" runAsAdmin="false">
<excludedLaunchers>
<launcher id="22" />
</excludedLaunchers>
@@ -479,7 +481,7 @@
<excludedInstallerScreens />
<excludedUninstallerScreens />
</win32>
<linuxRPM name="Linux RPM" id="18" mediaFileName="" installDir="/opt/${compiler:UNIX_INSTALL_DIR}" overridePrincipalLanguage="true" requires64bit="false" runPostProcessor="false" postProcessor="" failOnPostProcessorError="false" includedJRE="linux-x86-1.7.0_51" manualJREEntry="false" os="linux" arch="i386">
<linuxRPM name="Linux RPM" id="18" mediaFileName="" installDir="/opt/${compiler:UNIX_INSTALL_DIR}" overridePrincipalLanguage="true" requires64bit="false" runPostProcessor="false" postProcessor="" failOnPostProcessorError="false" includedJRE="linux-x86-1.7.0_55" manualJREEntry="false" os="linux" arch="i386">
<excludedLaunchers>
<launcher id="2" />
<launcher id="12" />
BIN +11.8 KB (100%) build/lib/dist/bcpg-jdk15on.jar
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN +76.9 KB (110%) build/lib/dist/mysql.jar
Binary file not shown.
BIN +138 KB (130%) build/lib/dist/postgres.jar
Binary file not shown.
@@ -3,7 +3,9 @@ Name | Version
ant.jar | Jetty 6.1.0 (1.6.5) | Apache 2.0
ant-contrib.jar | 1.0b1 | Apache 2.0
ant-subdirtask.jar | Revision 1.4 (CVS) |
bouncycastle.jar | JDK 1.5, 146 (bcprov-jdk15-146.jar) | See http://www.bouncycastle.org/licence.html
bcpg-jdk15on.jar | 1.50 | See http://www.bouncycastle.org/licence.html
bcpkix-jdk15on.jar | 1.50 | See http://www.bouncycastle.org/licence.html
bcprov-jdk15on.jar | 1.50 | See http://www.bouncycastle.org/licence.html
cglib.jar | 2.1.3 (JMock 2.1.0) |
commons-lang.jar | 2.3 | Apache 2.0
commons-logging.jar | Jetty 5.1.10 | Apache 2.0
@@ -46,10 +48,10 @@ mail.jar | 1.4.1 (JavaMail)
mina-core.jar | 1.1.8 (https://svn.apache.org/repos/asf/mina/branches/1.1) | Apache 2.0
mina-filter-compression.jar | 1.1.8 (https://svn.apache.org/repos/asf/mina/branches/1.1) | Apache 2.0
mina-filter-ssl.jar | 1.1.8-SNAPSHOT (see note #2) | Apache 2.0
mysql.jar | 5.1.28 | GPL
mysql.jar | 5.1.30 | GPL
objenesis | 1.0 (JMock 2.1.0) | BSD (http://www.jmock.org/license.html)
pack200task.jar | August 5, 2004 | LGPL
postgres.jar | 8.3-604.jdbc3 | BSD (http://jdbc.postgresql.org/license.html)
postgres.jar | 9.3-1101.jdbc4 | BSD (http://jdbc.postgresql.org/license.html)
proxool.jar | 0.9.0RC3+ (see note #1) | Apache 1.1 (http://proxool.sourceforge.net/licence.html)
rome.jar | 0.9 | Apache 2.0
rome-fetcher.jar | 0.9 | Apache 2.0
@@ -3,6 +3,9 @@
if [ -f /tmp/openfireInstallBackup/openfire.xml ]; then
/bin/mv /tmp/openfireInstallBackup/openfire.xml /usr/local/openfire/conf/openfire.xml
fi
if [ -f /tmp/openfireInstallBackup/security.xml ]; then
/bin/mv /tmp/openfireInstallBackup/security.xml /usr/local/openfire/conf/security.xml
fi

if [ -f /tmp/openfireInstallBackup/keystore ]; then
/bin/mv /tmp/openfireInstallBackup/keystore /usr/local/openfire/resources/security/keystore
@@ -15,6 +15,9 @@ if [ -d /usr/local/openfire ]; then
if [ -f /usr/local/openfire/conf/openfire.xml ]; then
/bin/cp /usr/local/openfire/conf/openfire.xml /tmp/openfireInstallBackup/openfire.xml
fi
if [ -f /usr/local/openfire/conf/security.xml ]; then
/bin/cp /usr/local/openfire/conf/security.xml /tmp/openfireInstallBackup/security.xml
fi
if [ -f /usr/local/openfire/resources/security/keystore ]; then
/bin/cp /usr/local/openfire/resources/security/keystore /tmp/openfireInstallBackup/keystore
fi
Binary file not shown.
@@ -5,7 +5,7 @@ Release: 1
BuildRoot: %{_builddir}/%{name}-root
Source0: %{OPENFIRE_SOURCE}
%ifnarch noarch
Source1: jre-dist.tar.gz
Source1: %{JRE_BUNDLE}
%endif
Group: Applications/Communications
Vendor: Jive Software
@@ -113,6 +113,7 @@ exit 0
%{homedir}/bin/embedded-db-viewer.sh
%dir %{homedir}/conf
%config(noreplace) %{homedir}/conf/openfire.xml
%config(noreplace) %{homedir}/conf/security.xml
%config(noreplace) %{homedir}/conf/crowd.properties
%dir %{homedir}/lib
%{homedir}/lib/*.jar
@@ -5,6 +5,10 @@ if [ -f /tmp/openfireInstallBackup/openfire.xml ]; then
/bin/mv /tmp/openfireInstallBackup/openfire.xml /opt/openfire/conf/openfire.xml
/bin/chown daemon:daemon /opt/openfire/conf/openfire.xml
fi
if [ -f /tmp/openfireInstallBackup/security.xml ]; then
/bin/mv /tmp/openfireInstallBackup/security.xml /opt/openfire/conf/security.xml
/bin/chown daemon:daemon /opt/openfire/conf/security.xml
fi

if [ -f /tmp/openfireInstallBackup/keystore ]; then
/bin/mv /tmp/openfireInstallBackup/keystore /opt/openfire/resources/security/keystore
@@ -8,6 +8,9 @@ if [ -d "/opt/openfire" ]; then
if [ -f /opt/openfire/conf/openfire.xml ]; then
/bin/cp -f /opt/openfire/conf/openfire.xml /tmp/openfireInstallBackup/openfire.xml
fi
if [ -f /opt/openfire/conf/security.xml ]; then
/bin/cp -f /opt/openfire/conf/security.xml /tmp/openfireInstallBackup/security.xml
fi
if [ -f /opt/openfire/resources/security/keystore ]; then
/bin/cp -f /opt/openfire/resources/security/keystore /tmp/openfireInstallBackup/keystore
fi
@@ -162,6 +162,167 @@ <h1>Openfire Changelog</h1>

<div id="pageBody">

<h2>3.9.2 -- <span style="font-weight: normal;">May 1, 2014</span></h2>

<h2>Bug</h2>
<ul>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-2'>OF-2</a>] - LocalOutgoingServerSession logs connection failures over verbosely
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-24'>OF-24</a>] - &quot;Issue with IQ subscription=&quot;remove&quot;
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-114'>OF-114</a>] - Clearing cache can lock up MUC
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-183'>OF-183</a>] - Bad-namespace prefix is actually invalid-namespace?
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-193'>OF-193</a>] - Last logouts are not recorded when server is shut down
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-297'>OF-297</a>] - fix: mutual roster deletion problem
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-303'>OF-303</a>] - fix Flexible Offline Message Retrieval (XEP-0013) support
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-455'>OF-455</a>] - Some unicode pattern in status message can break the session connection
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-471'>OF-471</a>] - Error integrity of the compressed stream
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-544'>OF-544</a>] - MUC change affiliation/role - admin IQ item processing bug
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-562'>OF-562</a>] - Broadcasting roles for MUC are not loaded correctly from DB
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-633'>OF-633</a>] - Current OfflineMessageStore logic discards valid MUC invites
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-640'>OF-640</a>] - log4j doesn&#39;t pick up ${openfireHome}
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-669'>OF-669</a>] - Visually failed first login to Admin Console
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-686'>OF-686</a>] - Anonymous registration permits name with javascript payload
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-687'>OF-687</a>] - MUC topic permits javascript payloads
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-692'>OF-692</a>] - Node column in ofSecurityAuditLog table should accept NULL entries
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-693'>OF-693</a>] - openfire init script target reload should not call restart
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-699'>OF-699</a>] - Race condition during cluster initialization (Hazelcast plugin)
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-705'>OF-705</a>] - Admin console (XSS) vulnerability lets attacker change admin password or create new admin
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-706'>OF-706</a>] - Openfire does not close the stream with a stream error if the namespace is not &#39;http://etherx.jabber.org/streams&#39;
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-717'>OF-717</a>] - The BOSH implementation should include a &#39;from&#39; attribute in its session creation response.
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-720'>OF-720</a>] - Roster deletion of userB by userA should not remove userA from userB&#39;s roster
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-722'>OF-722</a>] - Openfire should save XEP-0184 delivery receipts as offline message
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-725'>OF-725</a>] - Openfire must return a service-unavailable error when blocking an IQ of type get or set because of a privacy list. OF should return error if a message stanza is blocked
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-731'>OF-731</a>] - HybridUserProvider does not initialize correctly
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-733'>OF-733</a>] - OF should not silently close a connection, when receiving a message without &#39;to&#39; attribute
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-734'>OF-734</a>] - Openfire cannot deal with SASL &lt;abort/&gt;
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-735'>OF-735</a>] - Openfire should return &lt;invalid-mechanism/&gt; SASL failure, when requesting an unknown mechanism
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-741'>OF-741</a>] - Debian Installer should allow Java7 as a prereq
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-742'>OF-742</a>] - MUC Service sends &quot;disturbing&quot; service messages.
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-743'>OF-743</a>] - MUC room does not return its identity or features, when querying for room info
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-745'>OF-745</a>] - Use TLS-dialback even if that mechanism is not advertised
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-746'>OF-746</a>] - Use update-alternatives to set JAVA_HOME on debian
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-751'>OF-751</a>] - NPE on PubSubEngine#shutdown on server shutdown
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-753'>OF-753</a>] - Improve init script to work with opensuse and fix logic with PID file
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-755'>OF-755</a>] - Monitoring plugin database fixes
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-756'>OF-756</a>] - Fix Postgres purge process error
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-757'>OF-757</a>] - Allow s2s message of subdomain of XMPP domain when no components are found
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-759'>OF-759</a>] - Update bundled postgresql driver to PostgreSQL 9.3 JDBC4 (build 1101)
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-760'>OF-760</a>] - MUC service does not include &quot;self-presence&quot; status code 110
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-761'>OF-761</a>] - OF must return &lt;jid-malformed/&gt; instead of &lt;bad-request/&gt; when joining a MUC room without nickname
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-769'>OF-769</a>] - Fix typo in monitoring plugin
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-770'>OF-770</a>] - CVE-2014-2741 Uncontrolled Resource Consumption with XMPP-Layer Compression
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-772'>OF-772</a>] - IQ type=&quot;result&quot; getting java.lang.IllegalArgumentException
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-774'>OF-774</a>] - Needless code in AuthorizationManager
</li>
</ul>

<h2>Improvement</h2>
<ul>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-163'>OF-163</a>] - fix RosterItemProvider.getItems() for Oracle
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-298'>OF-298</a>] - EntityCapabilityManager should not use a clustered cache
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-309'>OF-309</a>] - Privacy Lists drop messages silently
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-411'>OF-411</a>] - Admin or owner should be able to join a room when it has reached maximum occupants number
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-464'>OF-464</a>] - Verify if there were packets pending to be sent and decide what to do with them
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-569'>OF-569</a>] - Add deluser adhoc command
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-592'>OF-592</a>] - build.xml_chmod_executables.patch
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-594'>OF-594</a>] - PluginServlet.java_support_registering_servlets_programmatically.patch
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-729'>OF-729</a>] - Upgrade Hazelcast plugin to latest release version (3.1.x)
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-730'>OF-730</a>] - Migrate operational configuration properties from openfire.xml to DB
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-749'>OF-749</a>] - Upgrade bouncycastle library from 1.49 to 1.50 to keep up with JitsiVideobridge
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-764'>OF-764</a>] - Group chat history (MUC) should match configuration after server restart
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-771'>OF-771</a>] - MUC service should flush recent history before shutting down
</li>
</ul>

<h2>New Feature</h2>
<ul>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-125'>OF-125</a>] - Restrict discovery of rooms based on users membership
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-206'>OF-206</a>] - Add HybridUserProvider
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-347'>OF-347</a>] - The domain should add support for Last Activity requests
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-638'>OF-638</a>] - Add support for XEP-0202: Entity Time
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-682'>OF-682</a>] - Add Portuguese translation
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-714'>OF-714</a>] - Add ability to encrypt properties so they are encrypted in the db and do not appear in the admin console.
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-758'>OF-758</a>] - Add support for XEP-0280 &quot;Message Carbons&quot;
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-775'>OF-775</a>] - Improve logging of invalid presence show
</li>
</ul>

<h2>Task</h2>
<ul>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-728'>OF-728</a>] - Update installation package with the latest Java JRE
</li>
</ul>

<h2>Sub-task</h2>
<ul>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-10'>OF-10</a>] - Pubsub event message with SHIM information holding multiple subscriptions should have the name=&#39;SubID&#39;.
</li>
<li>[<a href='http://issues.igniterealtime.org/browse/OF-103'>OF-103</a>] - [MUC] Allow nicknames to be used more than once in the same room
</li>
</ul>

<h2>3.9.1 -- <span style="font-weight: normal;">Feb 6, 2014</span></h2>

@@ -133,25 +133,27 @@ start() {
[ $RETVAL -eq 0 -a -d /var/lock/subsys ] && touch /var/lock/subsys/openfire

sleep 1 # allows prompt to return

PID=$(findPID)
echo $PID > $OPENFIRE_PIDFILE

cd $OLD_PWD
}

stop() {
# Stop daemons.
echo -n "Shutting down openfire: "

PID=$(findPID)
if [ -n "$PID" ]; then
if [ -n "$FUNCTIONS_FOUND" ]; then
echo $PID > $OPENFIRE_PIDFILE
# delay copied from restart
killproc -p $OPENFIRE_PIDFILE -d 10
rm -f $OPENFIRE_PIDFILE
else
if [ -f "$OPENFIRE_PIDFILE" ]; then
killproc -p $OPENFIRE_PIDFILE -d 10
rm -f $OPENFIRE_PIDFILE
else
PID=$(findPID)
if [ -n $PID ]; then
kill $PID
else
echo "Openfire is not running."
fi
else
echo "Openfire is not running."
fi

RETVAL=$?
@@ -200,14 +202,11 @@ case "$1" in
condrestart)
condrestart
;;
reload)
restart
;;
status)
status
;;
*)
echo "Usage $0 {start|stop|restart|status|condrestart|reload}"
echo "Usage $0 {start|stop|restart|status|condrestart}"
RETVAL=1
esac

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file stores security-related properties needed by Openfire.
You may edit this file to manage encrypted properties and
encryption configuration value. Note however that you should not
edit this file while Openfire is running, or it may be overwritten.
It is important to note that Openfire will store encrypted property
values securely "at rest" (e.g. in the database or XML), but the
values will be managed as clear text strings in memory at runtime for
interoperability and performance reasons. Encrypted property values
are not visible via the Openfire console, but they may be edited or
deleted as needed.
-->
<security>

<encrypt>
<!-- This can be set to "AES" or "Blowfish" (default) at setup time -->
<algorithm>Blowfish</algorithm>
<key>
<!--
If this is a new server setup, you may set a custom encryption key
by setting a value for the <new /> encryption key element only.
To change the encryption key, provide values for both new and old
encryption keys here. The "old" key must match the unencrypted value
of the "current" key. The server will update the existing property
values in the database, re-encrypting them using the new key. After
the encrypted properties have been updated, the new key will itself
be encrypted and re-written into this file as <current />.
Note that if the current encryption key becomes invalid, any property
values secured by the original key will be inaccessible as well.
The key value can be any string, and it will be hashed, filled, and/or
truncated to produce a compatible key for the corresponding algorithm.
Note that leading and trailing spaces will be ignored. A strong key
will contain sixteen characters or more.
<old></old>
<new></new>
-->
<current />
</key>
<property>
<!--
This list includes the names of properties that have been marked for
encryption. Any XML properties (from openfire.xml) that are listed here
will be encrypted automatically upon first use. Other properties
(already in the database) can be added to this list at runtime via the
"System Properties" page in the Openfire console.
-->
<name>database.defaultProvider.username</name>
<name>database.defaultProvider.password</name>
<name>ldap.adminDN</name>
<name>ldap.adminPassword</name>
<name>clearspace.uri</name>
<name>clearspace.sharedSecret</name>
</property>
</encrypt>

<!--
Any other property defined in this file will be treated as an encrypted
property. The value (in clear text) will be encrypted and migrated into
the Openfire database during the next startup. The property name will
be added to the list of encrypted properties and the clear text value
will be removed from this file.
<foo><bar>Secr3t$tr1ng!</bar></foo>
-->

</security>
@@ -518,6 +518,23 @@
## Added key: 'session.details.node'
## Added key: 'session.details.local'
## Added key: 'session.details.remote'
##
## 3.9.2
## Added key: 'server.properties.encryption'
## Added key: 'server.properties.encrypted'
## Added key: 'server.properties.encrypt'
## Added key: 'server.properties.alt_encrypt'
## Added key: 'server.properties.encrypt_property_true'
## Added key: 'server.properties.encrypt_property_false'
## Added key: 'setup.host.settings.encryption_algorithm'
## Added key: 'setup.host.settings.encryption_algorithm_info'
## Added key: 'setup.host.settings.encryption_aes'
## Added key: 'setup.host.settings.encryption_blowfish'
## Added key: 'setup.host.settings.encryption_key'
## Added key: 'setup.host.settings.encryption_key_info'
## Added key: 'setup.host.settings.encryption_key_invalid'
## Added key: 'server.properties.delete_confirm'
## Added key: 'server.properties.encrypt_confirm'

# Openfire

@@ -1499,7 +1516,7 @@ server.db_stats.no_queries=No queries
# Server properties Page

server.properties.title=System Properties
server.properties.info=Below is a list of the system properties. Values for password sensitive fields are \
server.properties.info=Below is a list of the system properties. Values for encrypted and sensitive fields are \
hidden. Long property names and values are clipped. Hold your mouse over the property name to see \
the full value or to see both the full name and value, click the edit icon next to the property.
server.properties.system=System Properties
@@ -1519,6 +1536,15 @@ server.properties.new_property=Add new property
server.properties.enter_property_name=Please enter a valid property name.
server.properties.enter_property_value=Please enter a property value.
server.properties.max_character=1000 character max.
server.properties.encryption=Property Encryption
server.properties.encrypted=Property encrypted successfully.
server.properties.encrypt=Encrypt
server.properties.alt_encrypt=Click to encrypt this property
server.properties.alt_encrypted=Property is encrypted
server.properties.encrypt_property_true=Encrypt this property value
server.properties.encrypt_property_false=Do not encrypt this property value
server.properties.delete_confirm=Are you sure you want to delete this property?
server.properties.encrypt_confirm=Are you sure you want to encrypt this property?

# Server props Page

@@ -2145,6 +2171,13 @@ setup.host.settings.secure_port=Secure Admin Console Port:
setup.host.settings.invalid_port=Invalid port number
setup.host.settings.port_number=Port number for the web-based admin console (default is 9090).
setup.host.settings.secure_port_number=Port number for the web-based admin console through SSL (default is 9091).
setup.host.settings.encryption_algorithm=Property Encryption via:
setup.host.settings.encryption_algorithm_info=Select an encryption engine/algorithm to use when encrypting properties.
setup.host.settings.encryption_aes=AES
setup.host.settings.encryption_blowfish=Blowfish
setup.host.settings.encryption_key=Property Encryption Key:
setup.host.settings.encryption_key_info=For additional security, you may optionally define a custom encryption key.
setup.host.settings.encryption_key_invalid=You must input the same encryption key value twice to ensure that you have typed the hidden value correctly.

# Setup index Page

Large diffs are not rendered by default.

@@ -35,6 +35,7 @@
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.util.LocaleUtils;
@@ -376,9 +377,29 @@ else if (recipientJID.getNode() == null ||
}
}
else {
// JID is of the form <node@domain/resource> or belongs to a remote server
// or to an uninstalled component
routingTable.routePacket(recipientJID, packet, false);
ClientSession session = sessionManager.getSession(packet.getFrom());
boolean isAcceptable = true;
if (session instanceof LocalClientSession) {
// Check if we could process IQ stanzas from the recipient.
// If not, return a not-acceptable error as per XEP-0016:
// If the user attempts to send an outbound stanza to a contact and that stanza type is blocked, the user's server MUST NOT route the stanza to the contact but instead MUST return a <not-acceptable/> error
IQ dummyIQ = packet.createCopy();
dummyIQ.setFrom(packet.getTo());
dummyIQ.setTo(packet.getFrom());
if (!((LocalClientSession) session).canProcess(dummyIQ)) {
packet.setTo(session.getAddress());
packet.setFrom((JID) null);
packet.setError(PacketError.Condition.not_acceptable);
session.process(packet);
isAcceptable = false;
}
}

if (isAcceptable) {
// JID is of the form <node@domain/resource> or belongs to a remote server
// or to an uninstalled component
routingTable.routePacket(recipientJID, packet, false);
}
}
}
catch (Exception e) {
@@ -20,13 +20,20 @@

package org.jivesoftware.openfire;

import java.util.List;
import java.util.StringTokenizer;

import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.dom4j.QName;
import org.jivesoftware.openfire.carbons.Sent;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.forward.Forwarded;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
@@ -82,14 +89,20 @@ public void route(Message packet) {
throw new NullPointerException();
}
ClientSession session = sessionManager.getSession(packet.getFrom());

try {
// Invoke the interceptors before we process the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true, false);
if (session == null || session.getStatus() == Session.STATUS_AUTHENTICATED) {
JID recipientJID = packet.getTo();

// If the server receives a message stanza with no 'to' attribute, it MUST treat the message as if the 'to' address were the bare JID <localpart@domainpart> of the sending entity.
if (recipientJID == null) {
recipientJID = packet.getFrom().asBareJID();
}

// Check if the message was sent to the server hostname
if (recipientJID != null && recipientJID.getNode() == null && recipientJID.getResource() == null &&
if (recipientJID.getNode() == null && recipientJID.getResource() == null &&
serverName.equals(recipientJID.getDomain())) {
if (packet.getElement().element("addresses") != null) {
// Message includes multicast processing instructions. Ask the multicastRouter
@@ -104,13 +117,55 @@ public void route(Message packet) {
return;
}

try {
// Deliver stanza to requested route
routingTable.routePacket(recipientJID, packet, false);
boolean isAcceptable = true;
if (session instanceof LocalClientSession) {
// Check if we could process messages from the recipient.
// If not, return a not-acceptable error as per XEP-0016:
// If the user attempts to send an outbound stanza to a contact and that stanza type is blocked, the user's server MUST NOT route the stanza to the contact but instead MUST return a <not-acceptable/> error
Message dummyMessage = packet.createCopy();
dummyMessage.setFrom(packet.getTo());
dummyMessage.setTo(packet.getFrom());
if (!((LocalClientSession) session).canProcess(dummyMessage)) {
packet.setTo(session.getAddress());
packet.setFrom((JID)null);
packet.setError(PacketError.Condition.not_acceptable);
session.process(packet);
isAcceptable = false;
}
}
catch (Exception e) {
log.error("Failed to route packet: " + packet.toXML(), e);
routingFailed(recipientJID, packet);
if (isAcceptable) {
boolean isPrivate = packet.getElement().element(QName.get("private", "urn:xmpp:carbons:2")) != null;
try {
// Deliver stanza to requested route
routingTable.routePacket(recipientJID, packet, false);
} catch (Exception e) {
log.error("Failed to route packet: " + packet.toXML(), e);
routingFailed(recipientJID, packet);
}

// Sent carbon copies to other resources of the sender:
// When a client sends a <message/> of type "chat"
if (packet.getType() == Message.Type.chat && !isPrivate) { // && session.isMessageCarbonsEnabled() ??? // must the own session also be carbon enabled?
List<JID> routes = routingTable.getRoutes(packet.getFrom().asBareJID(), null);
for (JID route : routes) {
// The sending server SHOULD NOT send a forwarded copy to the sending full JID if it is a Carbons-enabled resource.
if (!route.equals(session.getAddress())) {
ClientSession clientSession = sessionManager.getSession(route);
if (clientSession != null && clientSession.isMessageCarbonsEnabled()) {
Message message = new Message();
// The wrapping message SHOULD maintain the same 'type' attribute value
message.setType(packet.getType());
// the 'from' attribute MUST be the Carbons-enabled user's bare JID
message.setFrom(packet.getFrom().asBareJID());
// and the 'to' attribute SHOULD be the full JID of the resource receiving the copy
message.setTo(route);
// The content of the wrapping message MUST contain a <sent/> element qualified by the namespace "urn:xmpp:carbons:2", which itself contains a <forwarded/> qualified by the namespace "urn:xmpp:forward:0" that contains the original <message/> stanza.
message.addExtension(new Sent(new Forwarded(packet)));
clientSession.process(message);
}
}
}
}
}
}
else {
@@ -20,22 +20,9 @@

package org.jivesoftware.openfire;

import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.QName;
import org.dom4j.io.SAXReader;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
@@ -55,6 +42,16 @@
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Represents the user's offline message storage. A message store holds messages that were
* sent to the user while they were unavailable. The user can retrieve their messages by
@@ -126,13 +123,8 @@ public void addMessage(Message message) {
if (message == null) {
return;
}
// ignore empty bodied message (typically chat-state notifications).
if (message.getBody() == null || message.getBody().length() == 0) {
// allow empty pubsub messages (OF-191)
if (message.getChildElement("event", "http://jabber.org/protocol/pubsub#event") == null)
{
return;
}
if(!shouldStoreMessage(message)) {
return;
}
JID recipient = message.getTo();
String username = recipient.getNode();
@@ -470,4 +462,61 @@ public void stop() {
// Remove this module as a user event listener
UserEventDispatcher.removeListener(this);
}

/**
* Decide whether a message should be stored offline according to XEP-0160 and XEP-0334.
*
* @param message
* @return <code>true</code> if the message should be stored offline, <code>false</code> otherwise.
*/
static boolean shouldStoreMessage(final Message message) {
// XEP-0334: Implement the <no-store/> hint to override offline storage
if (message.getChildElement("no-store", "urn:xmpp:hints") != null) {
return false;
}

switch (message.getType()) {
case chat:
// XEP-0160: Messages with a 'type' attribute whose value is "chat" SHOULD be stored offline, with the exception of messages that contain only Chat State Notifications (XEP-0085) [7] content

// Iterate through the child elements to see if we can find anything that's not a chat state notification or
// real time text notification
Iterator<?> it = message.getElement().elementIterator();

while (it.hasNext()) {
Object item = it.next();

if (item instanceof Element) {
Element el = (Element) item;

if (!el.getNamespaceURI().equals("http://jabber.org/protocol/chatstates")
&& !(el.getQName().equals(QName.get("rtt", "urn:xmpp:rtt:0")))
) {
return true;
}
}
}

return false;

case groupchat:
case headline:
// XEP-0160: "groupchat" message types SHOULD NOT be stored offline
// XEP-0160: "headline" message types SHOULD NOT be stored offline
return false;

case error:
// XEP-0160: "error" message types SHOULD NOT be stored offline,
// although a server MAY store advanced message processing errors offline
if (message.getChildElement("amp", "http://jabber.org/protocol/amp") == null) {
return false;
}
break;

default:
// XEP-0160: Messages with a 'type' attribute whose value is "normal" (or messages with no 'type' attribute) SHOULD be stored offline.
break;
}
return true;
}
}
@@ -96,6 +96,10 @@ public void storeOffline(Message message) {
PrivacyList list =
PrivacyListManager.getInstance().getDefaultPrivacyList(recipientJID.getNode());
if (list != null && list.shouldBlockPacket(message)) {
Message result = message.createCopy();
result.setTo(message.getFrom());
result.setError(PacketError.Condition.service_unavailable);
XMPPServer.getInstance().getRoutingTable().routePacket(message.getFrom(), result, true);
return;
}

@@ -189,6 +193,9 @@ public void initialize(XMPPServer server) {
router = server.getPacketRouter();
serverAddress = new JID(server.getServerInfo().getXMPPDomain());

JiveGlobals.migrateProperty("xmpp.offline.quota");
JiveGlobals.migrateProperty("xmpp.offline.type");

String quota = JiveGlobals.getProperty("xmpp.offline.quota");
if (quota != null && quota.length() > 0) {
OfflineMessageStrategy.quota = Integer.parseInt(quota);
@@ -1,7 +1,4 @@
/**
* $RCSfile$
* $Revision: 3144 $
* $Date: 2005-12-01 14:20:11 -0300 (Thu, 01 Dec 2005) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
@@ -66,23 +63,7 @@
import org.jivesoftware.openfire.filetransfer.DefaultFileTransferManager;
import org.jivesoftware.openfire.filetransfer.FileTransferManager;
import org.jivesoftware.openfire.filetransfer.proxy.FileTransferProxy;
import org.jivesoftware.openfire.handler.IQAuthHandler;
import org.jivesoftware.openfire.handler.IQBindHandler;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.handler.IQLastActivityHandler;
import org.jivesoftware.openfire.handler.IQOfflineMessagesHandler;
import org.jivesoftware.openfire.handler.IQPingHandler;
import org.jivesoftware.openfire.handler.IQPrivacyHandler;
import org.jivesoftware.openfire.handler.IQPrivateHandler;
import org.jivesoftware.openfire.handler.IQRegisterHandler;
import org.jivesoftware.openfire.handler.IQRosterHandler;
import org.jivesoftware.openfire.handler.IQSessionEstablishmentHandler;
import org.jivesoftware.openfire.handler.IQSharedGroupHandler;
import org.jivesoftware.openfire.handler.IQTimeHandler;
import org.jivesoftware.openfire.handler.IQVersionHandler;
import org.jivesoftware.openfire.handler.IQvCardHandler;
import org.jivesoftware.openfire.handler.PresenceSubscribeHandler;
import org.jivesoftware.openfire.handler.PresenceUpdateHandler;
import org.jivesoftware.openfire.handler.*;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.mediaproxy.MediaProxyService;
import org.jivesoftware.openfire.muc.MultiUserChatManager;
@@ -350,16 +331,19 @@ public void removeServerListener(XMPPServerListener listener) {
private void initialize() throws FileNotFoundException {
locateOpenfire();

name = JiveGlobals.getProperty("xmpp.domain", "127.0.0.1").toLowerCase();
startDate = new Date();

try {
host = InetAddress.getLocalHost().getHostName();
}
catch (UnknownHostException ex) {
Log.warn("Unable to determine local hostname.", ex);
}
if (host == null) {
host = "127.0.0.1";
}

version = new Version(3, 9, 1, Version.ReleaseStatus.Release, -1);
version = new Version(3, 9, 2, Version.ReleaseStatus.Release, -1);
if ("true".equals(JiveGlobals.getXMLProperty("setup"))) {
setupMode = false;
}
@@ -379,6 +363,15 @@ private void initialize() throws FileNotFoundException {
Log.error(e.getMessage(), e);
}

JiveGlobals.migrateProperty("xmpp.domain");
name = JiveGlobals.getProperty("xmpp.domain", host).toLowerCase();

org.jivesoftware.util.Log.setDebugEnabled(JiveGlobals.getXMLProperty("log.debug.enabled", false));

// Update server info
xmppServerInfo = new XMPPServerInfoImpl(name, host, version, startDate);


initialized = true;
}

@@ -463,20 +456,13 @@ public void run() {
finishSetup.start();
// We can now safely indicate that setup has finished
setupMode = false;

// Update server info
xmppServerInfo = new XMPPServerInfoImpl(name, host, version, startDate, getConnectionManager());
}
}

public void start() {
try {
initialize();

startDate = new Date();
// Store server info
xmppServerInfo = new XMPPServerInfoImpl(name, host, version, startDate, getConnectionManager());

// Create PluginManager now (but don't start it) so that modules may use it
File pluginDir = new File(openfireHome, "plugins");
pluginManager = new PluginManager(pluginDir);
@@ -548,6 +534,7 @@ private void loadModules() {
loadModule(IQRegisterHandler.class.getName());
loadModule(IQRosterHandler.class.getName());
loadModule(IQTimeHandler.class.getName());
loadModule(IQEntityTimeHandler.class.getName());
loadModule(IQvCardHandler.class.getName());
loadModule(IQVersionHandler.class.getName());
loadModule(IQLastActivityHandler.class.getName());
@@ -571,6 +558,8 @@ private void loadModules() {
loadModule(InternalComponentManager.class.getName());
loadModule(MultiUserChatManager.class.getName());
loadModule(ClearspaceManager.class.getName());
loadModule(IQMessageCarbonsHandler.class.getName());

// Load this module always last since we don't want to start listening for clients
// before the rest of the modules have been started
loadModule(ConnectionManagerImpl.class.getName());
@@ -115,11 +115,33 @@ private static void initProvider() {
* only provided for special-case logic.
*
* @return the current UserProvider.
* @deprecated Prefer using the corresponding factory method, rather than
* invoking methods on the provider directly
*/
public static AuthProvider getAuthProvider() {
return authProvider;
}

/**
* Returns whether the currently-installed AuthProvider is instance of a specific class.
* @param c the class to compare with
* @return true - if the currently-installed AuthProvider is instance of c, false otherwise.
*/
public static boolean isProviderInstanceOf(Class<?> c) {
return c.isInstance(authProvider);
}

/**
* Returns true if the currently installed {@link AuthProvider} supports password
* retrieval. Certain implementation utilize password hashes and other authentication
* mechanisms that do not require the original password.
*
* @return true if plain password retrieval is supported.
*/
public static boolean supportsPasswordRetrieval() {
return authProvider.supportsPasswordRetrieval();
}

/**
* Returns true if the currently installed {@link AuthProvider} supports authentication
* using plain-text passwords according to JEP-0078. Plain-text authentication is
@@ -156,6 +178,21 @@ public static String getPassword(String username) throws UserNotFoundException,
return authProvider.getPassword(username.toLowerCase());
}

/**
* Sets the users's password. This method should throw an UnsupportedOperationException
* if this operation is not supported by the backend user store.
*
* @param username the username of the user.
* @param password the new plaintext password for the user.
* @throws UserNotFoundException if the given user could not be loaded.
* @throws UnsupportedOperationException if the provider does not
* support the operation (this is an optional operation).
*/
public static void setPassword(String username, String password) throws UserNotFoundException,
UnsupportedOperationException, ConnectionException, InternalUnauthenticatedException {
authProvider.setPassword(username, password);
}

/**
* Authenticates a user with a username and plain text password and returns and
* AuthToken. If the username and password do not match the record of
@@ -60,7 +60,6 @@

private static ArrayList<AuthorizationPolicy> authorizationPolicies = new ArrayList<AuthorizationPolicy>();
private static ArrayList<AuthorizationMapping> authorizationMapping = new ArrayList<AuthorizationMapping>();
private static AuthorizationManager instance = new AuthorizationManager();

static {
// Convert XML based provider setup to Database based
@@ -117,9 +116,8 @@
}
}

private AuthorizationManager() {

}
// static utility class; do not instantiate
private AuthorizationManager() { }

/**
* Returns the currently-installed AuthorizationProvider. Warning: You
@@ -133,15 +131,6 @@ private AuthorizationManager() {
return authorizationPolicies;
}

/**
* Returns a singleton AuthorizationManager instance.
*
* @return a AuthorizationManager instance.
*/
public static AuthorizationManager getInstance() {
return instance;
}

/**
* Authorize the authenticated used to the requested username. This uses the
* selected the selected AuthenticationProviders.
@@ -0,0 +1,17 @@
package org.jivesoftware.openfire.carbons;

import org.jivesoftware.openfire.forward.Forwarded;
import org.xmpp.packet.PacketExtension;

/**
* The implementation of the {@code <received xmlns="urn:xmpp:carbons:2"/>} extension.
* It indicates, that a message has been received by another resource.
*
* @author Christian Schudt
*/
public final class Received extends PacketExtension {
public Received(Forwarded forwarded) {
super("received", "urn:xmpp:carbons:2");
element.add(forwarded.getElement());
}
}
@@ -0,0 +1,17 @@
package org.jivesoftware.openfire.carbons;

import org.jivesoftware.openfire.forward.Forwarded;
import org.xmpp.packet.PacketExtension;

/**
* The implementation of the {@code <sent xmlns="urn:xmpp:carbons:2"/>} extension.
* It indicates, that a message has been sent by the same user from another resource.
*
* @author Christian Schudt
*/
public final class Sent extends PacketExtension {
public Sent(Forwarded forwarded) {
super("sent", "urn:xmpp:carbons:2");
element.add(forwarded.getElement());
}
}
@@ -527,7 +527,7 @@ public void setSharedSecret(String sharedSecret) {
* @return true if Clearspace is being used as the backend of Openfire.
*/
public static boolean isEnabled() {
return AuthFactory.getAuthProvider() instanceof ClearspaceAuthProvider;
return AuthFactory.isProviderInstanceOf(ClearspaceAuthProvider.class);
}

@Override
@@ -27,6 +27,7 @@
import org.jivesoftware.openfire.commands.admin.*;
import org.jivesoftware.openfire.commands.admin.group.*;
import org.jivesoftware.openfire.commands.admin.user.AddUser;
import org.jivesoftware.openfire.commands.admin.user.DeleteUser;
import org.jivesoftware.openfire.commands.admin.user.AuthenticateUser;
import org.jivesoftware.openfire.commands.admin.user.ChangeUserPassword;
import org.jivesoftware.openfire.commands.admin.user.UserProperties;
@@ -213,6 +214,7 @@ private void addDefaultCommands() {
addCommand(new UpdateGroup());
addCommand(new DeleteGroup());
addCommand(new AddUser());
addCommand(new DeleteUser());
addCommand(new AuthenticateUser());
addCommand(new ChangeUserPassword());
addCommand(new UserProperties());
@@ -90,7 +90,7 @@ public void execute(SessionData data, Element command) {
}

try {
AuthFactory.getAuthProvider().authenticate(user.getUsername(), password);
AuthFactory.authenticate(user.getUsername(), password);
}
catch (UnauthorizedException e) {
// Auth failed
@@ -0,0 +1,138 @@
/**
* $RCSfile$
* $Revision: 3144 $
* $Date: 2005-12-01 14:20:11 -0300 (Thu, 01 Dec 2005) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.commands.admin.user;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.dom4j.Element;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.commands.AdHocCommand;
import org.jivesoftware.openfire.commands.SessionData;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.JID;

/**
* Delete a user from Openfire if the provider is not read-only. See
* <a href="http://www.xmpp.org/extensions/xep-0133.html#delete-user">Service Administration:
* Delete User</a>
*
* @author John Georgiadis
*/
public class DeleteUser extends AdHocCommand {
@Override
public String getCode() {
return "http://jabber.org/protocol/admin#delete-user";
}

@Override
public String getDefaultLabel() {
return "Delete a User";
}

@Override
public int getMaxStages(SessionData data) {
return 1;
}

@Override
public void execute(SessionData sessionData, Element command) {
Element note = command.addElement("note");
// Check if users cannot be modified (backend is read-only)
if (UserManager.getUserProvider().isReadOnly()) {
note.addAttribute("type", "error");
note.setText("User provider is read only. Users cannot be deleted.");
return;
}
Map<String, List<String>> data = sessionData.getData();

// Let's create the jid and check that they are a local user
JID account;
try {
account = new JID(get(data, "accountjid", 0));
}
catch (NullPointerException npe) {
note.addAttribute("type", "error");
note.setText("JID required parameter.");
return;
}
if (!XMPPServer.getInstance().isLocal(account)) {
note.addAttribute("type", "error");
note.setText("Cannot delete remote user.");
return;
}

try {
User user = UserManager.getInstance().getUser(account.getNode());
UserManager.getInstance().deleteUser(user);
}
catch (UserNotFoundException e) {
note.addAttribute("type", "error");
note.setText("User not found.");
return;
}
// Answer that the operation was successful
note.addAttribute("type", "info");
note.setText("Operation finished successfully");
}

@Override
protected void addStageInformation(SessionData data, Element command) {
DataForm form = new DataForm(DataForm.Type.form);
form.setTitle("Deleting a user");
form.addInstruction("Fill out this form to delete a user.");

FormField field = form.addField();
field.setType(FormField.Type.hidden);
field.setVariable("FORM_TYPE");
field.addValue("http://jabber.org/protocol/admin");

field = form.addField();
field.setType(FormField.Type.jid_single);
field.setLabel("The Jabber ID for the account to be deleted");
field.setVariable("accountjid");
field.setRequired(true);

// Add the form to the command
command.add(form.getElement());
}

@Override
protected List<Action> getActions(SessionData data) {
return Arrays.asList(AdHocCommand.Action.complete);
}

@Override
protected AdHocCommand.Action getExecuteAction(SessionData data) {
return AdHocCommand.Action.complete;
}

@Override
public boolean hasPermission(JID requester) {
return (super.hasPermission(requester) || InternalComponentManager.getInstance().hasComponent(requester))
&& !UserManager.getUserProvider().isReadOnly();
}
}
@@ -81,6 +81,8 @@
static {
servlets = new ConcurrentHashMap<String, GenericServlet>();
}

public static final String PLUGINS_WEBROOT = "/plugins/";

@Override
public void init(ServletConfig config) throws ServletException {
@@ -226,6 +228,51 @@ public static void unregisterServlets(File webXML) {
}
}


/**
* Registers a live servlet for a plugin programmatically, does not
* initialize the servlet.
*
* @param pluginManager the plugin manager
* @param plugin the owner of the servlet
* @param servlet the servlet.
* @param relativeUrl the relative url where the servlet should be bound
* @return the effective url that can be used to initialize the servlet
*/
public static String registerServlet(PluginManager pluginManager,
Plugin plugin, GenericServlet servlet, String relativeUrl)
throws ServletException {

String pluginName = pluginManager.getPluginDirectory(plugin).getName();
PluginServlet.pluginManager = pluginManager;
if (servlet == null) {
throw new ServletException("Servlet is missing");
}
String pluginServletUrl = pluginName + relativeUrl;
servlets.put(pluginName + relativeUrl, servlet);
return PLUGINS_WEBROOT + pluginServletUrl;

}

/**
* Unregister a live servlet for a plugin programmatically. Does not call
* the servlet destroy method.
*
* @param plugin the owner of the servlet
* @param servletUrl the relative url where servlet has been bound
* @return the unregistered servlet, so that it can be destroyed
*/
public static GenericServlet unregisterServlet(Plugin plugin, String url)
throws ServletException {
String pluginName = pluginManager.getPluginDirectory(plugin).getName();
if (url == null) {
throw new ServletException("Servlet URL is missing");
}
String fullUrl = pluginName + url;
GenericServlet servlet = servlets.remove(fullUrl);
return servlet;
}

/**
* Handles a request for a JSP page. It checks to see if a servlet is mapped
* for the JSP URL. If one is found, request handling is passed to it. If no
@@ -109,8 +109,8 @@
private Map<String, EntityCapabilities> verAttributes;

private EntityCapabilitiesManager() {
entityCapabilitiesMap = CacheFactory.createCache("Entity Capabilities");
entityCapabilitiesUserMap = CacheFactory.createCache("Entity Capabilities Users");
entityCapabilitiesMap = CacheFactory.createLocalCache("Entity Capabilities");
entityCapabilitiesUserMap = CacheFactory.createLocalCache("Entity Capabilities Users");
verAttributes = new HashMap<String, EntityCapabilities>();
}

@@ -0,0 +1,26 @@
package org.jivesoftware.openfire.forward;

import org.dom4j.Element;
import org.dom4j.QName;
import org.xmpp.packet.Message;
import org.xmpp.packet.PacketExtension;

/**
* @author Christian Schudt
*/
public class Forwarded extends PacketExtension {
public Forwarded(Message message) {
super("forwarded", "urn:xmpp:forward:0");

message.getElement().setQName(QName.get("message", "jabber:client"));

for (Object element : message.getElement().elements()) {
if (element instanceof Element) {
Element el = (Element) element;
el.setQName(QName.get(el.getName(), "jabber:client"));
}
}

element.add(message.getElement());
}
}
@@ -0,0 +1,93 @@
/**
* $RCSfile$
* $Revision: 1747 $
* $Date: 2014-02-10 22:37:12 -0100 (Thu, 10 Feb 2014) $
*
* Copyright (C) 2004-2014 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jivesoftware.openfire.handler;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.xmpp.packet.IQ;

import javax.xml.bind.DatatypeConverter;
import java.util.*;

/**
* This IQ handler implements XEP-0202: Entity Time.
*/
public final class IQEntityTimeHandler extends IQHandler implements ServerFeaturesProvider {

private final IQHandlerInfo info;

public IQEntityTimeHandler() {
super("XEP-0202: Entity Time");
info = new IQHandlerInfo("time", "urn:xmpp:time");
}

@Override
public IQ handleIQ(IQ packet) {
IQ response = IQ.createResultIQ(packet);
Element timeElement = DocumentHelper.createElement(QName.get(info.getName(), info.getNamespace()));
timeElement.addElement("tzo").setText(formatsTimeZone(TimeZone.getDefault()));
timeElement.addElement("utc").setText(getUtcDate(new Date()));
response.setChildElement(timeElement);
return response;
}

@Override
public IQHandlerInfo getInfo() {
return info;
}

@Override
public Iterator<String> getFeatures() {
return Collections.singleton(info.getNamespace()).iterator();
}

/**
* Formats a {@link TimeZone} as specified in XEP-0082: XMPP Date and Time Profiles.
*
* @param tz The time zone.
* @return The formatted time zone.
*/
String formatsTimeZone(TimeZone tz) {
// package-private for test.
int seconds = Math.abs(tz.getRawOffset()) / 1000;
int hours = seconds / 3600;
int minutes = (seconds % 3600) / 60;
return (tz.getRawOffset() < 0 ? "-" : "+") + String.format("%02d:%02d", hours, minutes);
}

/**
* Gets the ISO 8601 formatted date (UTC) as specified in XEP-0082: XMPP Date and Time Profiles.
*
* @param date The date.
* @return The UTC formatted date.
*/
String getUtcDate(Date date) {
// package-private for test.
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
calendar.setTime(date);
// This makes sure the date is formatted as the xs:dateTime type.
return DatatypeConverter.printDateTime(calendar);
}
}
@@ -60,10 +60,20 @@ public IQ handleIQ(IQ packet) throws UnauthorizedException {
String username = packet.getTo() == null ? null : packet.getTo().getNode();

// Check if any of the usernames is null
if (sender == null || username == null) {
if (sender == null) {
reply.setError(PacketError.Condition.forbidden);
return reply;
}
if (username == null) {
// http://xmpp.org/extensions/xep-0012.html#server
// When the last activity query is sent to a server or component (i.e., to a JID of the form <domain.tld>),
// the information contained in the IQ reply reflects the uptime of the JID sending the reply.
// The seconds attribute specifies how long the host has been running since it was last (re-)started.
long uptime = XMPPServer.getInstance().getServerInfo().getLastStarted().getTime();
long lastActivityTime = (System.currentTimeMillis() - uptime) / 1000;
lastActivity.addAttribute("seconds", String.valueOf(lastActivityTime));
return reply;
}

try {
RosterItem item = rosterManager.getRoster(username).getRosterItem(packet.getFrom());
@@ -0,0 +1,85 @@
/**
* $RCSfile$
* $Revision: 1747 $
* $Date: 2005-08-04 18:36:36 -0300 (Thu, 04 Aug 2005) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jivesoftware.openfire.handler;

import org.dom4j.Element;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.session.ClientSession;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;

import java.util.Collections;
import java.util.Iterator;

/**
* This handler manages XEP-0280 Message Carbons.
*
* @author Christian Schudt
*/
public final class IQMessageCarbonsHandler extends IQHandler implements ServerFeaturesProvider {

private static final String NAMESPACE = "urn:xmpp:carbons:2";

private IQHandlerInfo info;

public IQMessageCarbonsHandler() {
super("XEP-0280: Message Carbons");
info = new IQHandlerInfo("", NAMESPACE);
}

@Override
public IQ handleIQ(IQ packet) {
Element enable = packet.getChildElement();
if (XMPPServer.getInstance().isLocal(packet.getFrom())) {
if (enable.getName().equals("enable")) {
ClientSession clientSession = sessionManager.getSession(packet.getFrom());
clientSession.setMessageCarbonsEnabled(true);
return IQ.createResultIQ(packet);

} else if (enable.getName().equals("disable")) {
ClientSession clientSession = sessionManager.getSession(packet.getFrom());
clientSession.setMessageCarbonsEnabled(false);
return IQ.createResultIQ(packet);
} else {
IQ error = IQ.createResultIQ(packet);
error.setError(PacketError.Condition.bad_request);
return error;
}
} else {
// if the request is from a client that is not hosted on this server.
IQ error = IQ.createResultIQ(packet);
error.setError(PacketError.Condition.not_allowed);
return error;
}
}

@Override
public IQHandlerInfo getInfo() {
return info;
}

@Override
public Iterator<String> getFeatures() {
return Collections.singleton(NAMESPACE).iterator();
}
}
@@ -50,6 +50,7 @@
import org.xmpp.forms.FormField;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;

/**
* Implements JEP-0013: Flexible Offline Message Retrieval. Allows users to request number of
@@ -115,7 +116,12 @@ else if (offlineRequest.element("fetch") != null) {
}
else if ("remove".equals(item.attributeValue("action"))) {
// User requested to delete specific message
messageStore.deleteMessage(from.getNode(), creationDate);
if (messageStore.getMessage(from.getNode(), creationDate) != null) {
messageStore.deleteMessage(from.getNode(), creationDate);
} else {
// If the requester is authorized but the node does not exist, the server MUST return a <item-not-found/> error.
reply.setError(PacketError.Condition.item_not_found);
}
}
}
}
@@ -22,6 +22,7 @@
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.xmpp.packet.IQ;
import org.xmpp.packet.IQ.Type;

/**
* Implements the XMPP Ping as defined by XEP-0199. This protocol offers an
@@ -55,7 +56,10 @@ public IQPingHandler() {
*/
@Override
public IQ handleIQ(IQ packet) {
return IQ.createResultIQ(packet);
if (packet.getType().equals(Type.get)) {
return IQ.createResultIQ(packet);
}
return null;
}

/*
@@ -157,6 +157,10 @@ public void initialize(XMPPServer server) {
// Add the registration form to the probe result.
probeResult.add(registrationForm.getElement());
}

JiveGlobals.migrateProperty("register.inband");
JiveGlobals.migrateProperty("register.password");

// See if in-band registration should be enabled (default is true).
registrationEnabled = JiveGlobals.getBooleanProperty("register.inband", true);
// See if users can change their passwords (default is true).
@@ -272,7 +272,20 @@ private void removeItem(org.jivesoftware.openfire.roster.Roster roster, JID send
if (localServer.isLocal(recipient)) { // Recipient is local so let's handle it here
try {
Roster recipientRoster = userManager.getUser(recipient.getNode()).getRoster();
recipientRoster.deleteRosterItem(sender, true);
// Instead of deleting the sender in the recipient's roster, update it.
// http://issues.igniterealtime.org/browse/OF-720
RosterItem rosterItem = recipientRoster.getRosterItem(sender);
// If the receiver doesn't have subscribed yet, delete the sender from the receiver's roster, too.
if (rosterItem.getRecvStatus().equals(RosterItem.RECV_SUBSCRIBE)) {
recipientRoster.deleteRosterItem(sender, true);
}
// Otherwise only update it, so that the sender is not deleted from the receivers roster.
else {
rosterItem.setAskStatus(RosterItem.ASK_NONE);
rosterItem.setRecvStatus(RosterItem.RECV_NONE);
rosterItem.setSubStatus(RosterItem.SUB_NONE);
recipientRoster.updateRosterItem(rosterItem);
}
}
catch (UserNotFoundException e) {
// Do nothing
@@ -130,6 +130,18 @@ public static HttpBindManager getInstance() {
private HttpBindManager() {
// JSP 2.0 uses commons-logging, so also override that implementation.
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");

JiveGlobals.migrateProperty(HTTP_BIND_ENABLED);
JiveGlobals.migrateProperty(HTTP_BIND_PORT);
JiveGlobals.migrateProperty(HTTP_BIND_SECURE_PORT);
JiveGlobals.migrateProperty(HTTP_BIND_THREADS);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_FOR);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_SERVER);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_HOST);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_HOST_NAME);
JiveGlobals.migrateProperty(HTTP_BIND_CORS_ENABLED);
JiveGlobals.migrateProperty(HTTP_BIND_CORS_ALLOW_ORIGIN);

PropertyEventDispatcher.addListener(new HttpServerPropertyListener());
this.httpSessionManager = new HttpSessionManager();
@@ -37,6 +37,7 @@
import org.eclipse.jetty.util.log.Log;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
@@ -73,6 +74,10 @@ public void sessionClosed(HttpSession session) {
* Creates a new HttpSessionManager instance.
*/
public HttpSessionManager() {

JiveGlobals.migrateProperty("xmpp.httpbind.worker.threads");
JiveGlobals.migrateProperty("xmpp.httpbind.worker.timeout");

this.sessionManager = SessionManager.getInstance();

// Configure a pooled executor to handle async routing for incoming packets
@@ -362,6 +367,7 @@ private String createSessionCreationResponse(HttpSession session) throws Documen
Element response = DocumentHelper.createElement("body");
response.addNamespace("", "http://jabber.org/protocol/httpbind");
response.addNamespace("stream", "http://etherx.jabber.org/streams");
response.addAttribute("from", session.getServerName());
response.addAttribute("authid", session.getStreamID().getID());
response.addAttribute("sid", session.getStreamID().getID());
response.addAttribute("secure", Boolean.TRUE.toString());
@@ -152,14 +152,24 @@
MUCRole getRole();

/**
* Obtain the role of a given user by nickname.
* Obtain the first role of a given user by nickname.
*
* @param nickname The nickname of the user you'd like to obtain (cannot be <tt>null</tt>)
* @return The user's role in the room
* @throws UserNotFoundException If there is no user with the given nickname
* @deprecated Prefer {@link #getOccupantsByNickname(String)} instead (a user may be connected more than once)
*/
MUCRole getOccupant(String nickname) throws UserNotFoundException;

/**
* Obtain the roles of a given user by nickname. A user can be connected to a room more than once.
*
* @param nickname The nickname of the user you'd like to obtain (cannot be <tt>null</tt>)
* @return The user's role in the room
* @throws UserNotFoundException If there is no user with the given nickname
*/
List<MUCRole> getOccupantsByNickname(String nickname) throws UserNotFoundException;

/**
* Obtain the roles of a given user in the room by his bare JID. A user can have several roles,
* one for each client resource from which the user has joined the room.
@@ -41,18 +41,25 @@
public class BroadcastPresenceRequest extends MUCRoomTask {
private Presence presence;

private boolean isJoinPresence;

public BroadcastPresenceRequest() {
}

public BroadcastPresenceRequest(LocalMUCRoom room, Presence message) {
public BroadcastPresenceRequest(LocalMUCRoom room, Presence message, boolean isJoinPresence) {
super(room);
this.presence = message;
this.isJoinPresence = isJoinPresence;
}

public Presence getPresence() {
return presence;
}

public boolean isJoinPresence() {
return isJoinPresence;
}

public Object getResult() {
return null;
}
@@ -247,11 +247,9 @@ private void handleItemsElement(MUCRole senderRole, List<Element> itemsList, IQ
}
else {
// The client is modifying the list of moderators/members/participants/outcasts
JID jid;
String nick;
String target;
boolean hasAffiliation = itemsList.get(0).attributeValue("affiliation") !=
null;
boolean hasAffiliation;

// Keep a registry of the updated presences
List<Presence> presences = new ArrayList<Presence>(itemsList.size());
@@ -260,61 +258,69 @@ private void handleItemsElement(MUCRole senderRole, List<Element> itemsList, IQ
for (Object anItem : itemsList) {
try {
item = (Element) anItem;
target = (hasAffiliation ? item.attributeValue("affiliation") : item
.attributeValue("role"));
affiliation = item.attributeValue("affiliation");
hasAffiliation = affiliation != null;
target = (hasAffiliation ? affiliation : item.attributeValue("role"));
List<JID> jids = new ArrayList<JID>();
// jid could be of the form "full JID" or "bare JID" depending if we are
// going to change a role or an affiliation
if (hasJID) {
jid = new JID(item.attributeValue("jid"));
jids.add(new JID(item.attributeValue("jid")));
nick = null;
} else {
// Get the JID based on the requested nick
nick = item.attributeValue("nick");
jid = room.getOccupant(nick).getUserAddress();
for (MUCRole role : room.getOccupantsByNickname(nick)) {
if (!jids.contains(role.getUserAddress())) {
jids.add(role.getUserAddress());
}
}
}

if ("moderator".equals(target)) {
// Add the user as a moderator of the room based on the full JID
presences.add(room.addModerator(jid, senderRole));
} else if ("owner".equals(target)) {
presences.addAll(room.addOwner(jid, senderRole));
} else if ("admin".equals(target)) {
presences.addAll(room.addAdmin(jid, senderRole));
} else if ("participant".equals(target)) {
// Add the user as a participant of the room based on the full JID
presences.add(room.addParticipant(jid,
item.elementTextTrim("reason"),
senderRole));
} else if ("visitor".equals(target)) {
// Add the user as a visitor of the room based on the full JID
presences.add(room.addVisitor(jid, senderRole));
} else if ("member".equals(target)) {
// Add the user as a member of the room based on the bare JID
boolean hadAffiliation = room.getAffiliation(jid) != MUCRole.Affiliation.none;
presences.addAll(room.addMember(jid, nick, senderRole));
// If the user had an affiliation don't send an invitation. Otherwise
// send an invitation if the room is members-only and skipping invites
// are not disabled system-wide xmpp.muc.skipInvite
if (!skipInvite && !hadAffiliation && room.isMembersOnly()) {
room.sendInvitation(jid, null, senderRole, null);
}
} else if ("outcast".equals(target)) {
// Add the user as an outcast of the room based on the bare JID
presences.addAll(room.addOutcast(jid, item.elementTextTrim("reason"), senderRole));
} else if ("none".equals(target)) {
if (hasAffiliation) {
// Set that this jid has a NONE affiliation based on the bare JID
presences.addAll(room.addNone(jid, senderRole));
} else {
// Kick the user from the room
if (MUCRole.Role.moderator != senderRole.getRole()) {
throw new ForbiddenException();
for (JID jid : jids) {
if ("moderator".equals(target)) {
// Add the user as a moderator of the room based on the full JID
presences.add(room.addModerator(jid, senderRole));
} else if ("owner".equals(target)) {
presences.addAll(room.addOwner(jid, senderRole));
} else if ("admin".equals(target)) {
presences.addAll(room.addAdmin(jid, senderRole));
} else if ("participant".equals(target)) {
// Add the user as a participant of the room based on the full JID
presences.add(room.addParticipant(jid,
item.elementTextTrim("reason"),
senderRole));
} else if ("visitor".equals(target)) {
// Add the user as a visitor of the room based on the full JID
presences.add(room.addVisitor(jid, senderRole));
} else if ("member".equals(target)) {
// Add the user as a member of the room based on the bare JID
boolean hadAffiliation = room.getAffiliation(jid) != MUCRole.Affiliation.none;
presences.addAll(room.addMember(jid, nick, senderRole));
// If the user had an affiliation don't send an invitation. Otherwise
// send an invitation if the room is members-only and skipping invites
// are not disabled system-wide xmpp.muc.skipInvite
if (!skipInvite && !hadAffiliation && room.isMembersOnly()) {
room.sendInvitation(jid, null, senderRole, null);
}
presences.add(room.kickOccupant(jid, senderRole.getUserAddress(),
item.elementTextTrim("reason")));
} else if ("outcast".equals(target)) {
// Add the user as an outcast of the room based on the bare JID
presences.addAll(room.addOutcast(jid, item.elementTextTrim("reason"), senderRole));
} else if ("none".equals(target)) {
if (hasAffiliation) {
// Set that this jid has a NONE affiliation based on the bare JID
presences.addAll(room.addNone(jid, senderRole));
} else {
// Kick the user from the room
if (MUCRole.Role.moderator != senderRole.getRole()) {
throw new ForbiddenException();
}
presences.add(room.kickOccupant(jid, senderRole.getUserAddress(),
item.elementTextTrim("reason")));
}
} else {
reply.setError(PacketError.Condition.bad_request);
}
} else {
reply.setError(PacketError.Condition.bad_request);
}
}
catch (UserNotFoundException e) {
@@ -237,15 +237,18 @@ private void handleItemsElement(List<Element> itemsList, MUCRole senderRole, IQ
for (final Element item : itemsList) {
try {
String affiliation = item.attributeValue("affiliation");
JID jid;
if (hasJID) {
jid = new JID(item.attributeValue("jid"));
jids.put(new JID(item.attributeValue("jid")), affiliation);
} else {
// Get the bare JID based on the requested nick
nick = item.attributeValue("nick");
jid = room.getOccupant(nick).getUserAddress();
for (MUCRole role : room.getOccupantsByNickname(nick)) {
JID jid = role.getUserAddress();
if (!jids.containsKey(jid)) {
jids.put(jid, affiliation);
}
}
}
jids.put(jid, affiliation);
}
catch (UserNotFoundException e) {
// Do nothing
@@ -276,4 +276,48 @@ private void calculateExtendedInformation() {
ElementUtil.setProperty(extendedInformation, "x.item:affiliation", affiliation.toString());
ElementUtil.setProperty(extendedInformation, "x.item:role", role.toString());
}


@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((nick == null) ? 0 : nick.hashCode());
result = prime * result + ((rJID == null) ? 0 : rJID.hashCode());
result = prime * result + ((room == null) ? 0 : room.hashCode());
result = prime * result + ((user == null) ? 0 : user.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LocalMUCRole other = (LocalMUCRole) obj;
if (nick == null) {
if (other.nick != null)
return false;
} else if (!nick.equals(other.nick))
return false;
if (rJID == null) {
if (other.rJID != null)
return false;
} else if (!rJID.equals(other.rJID))
return false;
if (room == null) {
if (other.room != null)
return false;
} else if (!room.equals(other.room))
return false;
if (user == null) {
if (other.user != null)
return false;
} else if (!user.equals(other.user))
return false;
return true;
}
}
@@ -23,6 +23,7 @@
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -31,7 +32,6 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -109,17 +109,17 @@
/**
* The occupants of the room accessible by the occupants nickname.
*/
private Map<String,MUCRole> occupants = new ConcurrentHashMap<String, MUCRole>();
private Map<String, List<MUCRole>> occupantsByNickname = new ConcurrentHashMap<String, List<MUCRole>>();

/**
* The occupants of the room accessible by the occupants bare JID.
*/
private ConcurrentMap<JID, List<MUCRole>> occupantsByBareJID = new ConcurrentHashMap<JID, List<MUCRole>>();
private Map<JID, List<MUCRole>> occupantsByBareJID = new ConcurrentHashMap<JID, List<MUCRole>>();

/**
* The occupants of the room accessible by the occupants full JID.
*/
private ConcurrentMap<JID, MUCRole> occupantsByFullJID = new ConcurrentHashMap<JID, MUCRole>();
private Map<JID, MUCRole> occupantsByFullJID = new ConcurrentHashMap<JID, MUCRole>();

/**
* The name of the room.
@@ -182,7 +182,7 @@
/**
* List of chatroom's members. The list contains only bare jid, mapped to a nickname.
*/
private ConcurrentMap<JID, String> members = new ConcurrentHashMap<JID,String>();
private Map<JID, String> members = new ConcurrentHashMap<JID,String>();

/**
* List of chatroom's outcast. The list contains only bare jid of not allowed users.
@@ -437,13 +437,27 @@ public MUCRole getRole() {
return role;
}

/**
* @deprecated Prefer {@link #getOccupantsByNickname(String)} (user can be connected more than once)
*/
public MUCRole getOccupant(String nickname) throws UserNotFoundException {
if (nickname == null) {
throw new UserNotFoundException();
}
MUCRole role = occupants.get(nickname.toLowerCase());
if (role != null) {
return role;
List<MUCRole> roles = getOccupantsByNickname(nickname);
if (roles != null && roles.size() > 0) {
return roles.get(0);
}
throw new UserNotFoundException();
}

public List<MUCRole> getOccupantsByNickname(String nickname) throws UserNotFoundException {
if (nickname == null) {
throw new UserNotFoundException();
}
List<MUCRole> roles = occupantsByNickname.get(nickname.toLowerCase());
if (roles != null && roles.size() > 0) {
return roles;
}
throw new UserNotFoundException();
}
@@ -465,15 +479,15 @@ public MUCRole getOccupantByFullJID(JID jid) {
}

public Collection<MUCRole> getOccupants() {
return Collections.unmodifiableCollection(occupants.values());
return Collections.unmodifiableCollection(occupantsByFullJID.values());
}

public int getOccupantsCount() {
return occupants.size();
return occupantsByFullJID.size();
}

public boolean hasOccupant(String nickname) {
return occupants.containsKey(nickname.toLowerCase());
return occupantsByNickname.containsKey(nickname.toLowerCase());
}

public String getReservedNickname(JID jid) {
@@ -518,7 +532,7 @@ public LocalMUCRole joinRoom(String nickname, String password, HistoryRequest hi
lock.writeLock().lock();
try {
// If the room has a limit of max user then check if the limit has been reached
if (isDestroyed || (getMaxUsers() > 0 && getOccupantsCount() >= getMaxUsers())) {
if (!canJoinRoom(user)) {
throw new ServiceUnavailableException();
}
final JID bareJID = user.getAddress().asBareJID();
@@ -530,29 +544,10 @@ public LocalMUCRole joinRoom(String nickname, String password, HistoryRequest hi
}
}
// Check if the nickname is already used in the room
if (occupants.containsKey(nickname.toLowerCase())) {
if (occupants.get(nickname.toLowerCase()).getUserAddress().toBareJID().equals(bareJID.toBareJID())) {
// Nickname exists in room, and belongs to this user, pretend to kick the previous instance.
// The previous instance will see that they are disconnected, and the new instance will
// "take over" the previous role. Participants in the room shouldn't notice anything
// has occurred.
String reason = "Your account signed into this chatroom with the same nickname from another location.";
Presence updatedPresence = new Presence(Presence.Type.unavailable);
updatedPresence.setFrom(occupants.get(nickname.toLowerCase()).getRoleAddress());
updatedPresence.setTo(occupants.get(nickname.toLowerCase()).getUserAddress());
Element frag = updatedPresence.addChildElement(
"x", "http://jabber.org/protocol/muc#user");

// Set the person who performed the kick ("you" effectively)
frag.addElement("item").addElement("actor").setText(user.getAddress().toString());
// Add the reason why the user was kicked
frag.element("item").addElement("reason").setText(reason);
// Add the status code 307 that indicates that the user was kicked
frag.addElement("status").addAttribute("code", "307");

router.route(updatedPresence);
}
else {
if (occupantsByNickname.containsKey(nickname.toLowerCase())) {
List<MUCRole> occupants = occupantsByNickname.get(nickname.toLowerCase());
MUCRole occupant = occupants.size() > 0 ? occupants.get(0) : null;
if (occupant != null && !occupant.getUserAddress().toBareJID().equals(bareJID.toBareJID())) {
// Nickname is already used, and not by the same JID
throw new UserAlreadyExistsException();
}
@@ -566,14 +561,14 @@ public LocalMUCRole joinRoom(String nickname, String password, HistoryRequest hi
}
// If another user attempts to join the room with a nickname reserved by the first user
// raise a ConflictException
if (members.containsValue(nickname)) {
if (!nickname.equals(members.get(bareJID))) {
if (members.containsValue(nickname.toLowerCase())) {
if (!nickname.toLowerCase().equals(members.get(bareJID))) {
throw new ConflictException();
}
}
if (isLoginRestrictedToNickname()) {
String reservedNickname = members.get(bareJID);
if (reservedNickname != null && !nickname.equals(reservedNickname)) {
if (reservedNickname != null && !nickname.toLowerCase().equals(reservedNickname)) {
throw new NotAcceptableException();
}
}
@@ -619,7 +614,12 @@ else if (outcasts.contains(bareJID)) {
// Create a new role for this user in this room
joinRole = new LocalMUCRole(mucService, this, nickname, role, affiliation, user, presence, router);
// Add the new user as an occupant of this room
occupants.put(nickname.toLowerCase(), joinRole);
List<MUCRole> occupants = occupantsByNickname.get(nickname.toLowerCase());
if (occupants == null) {
occupants = new ArrayList<MUCRole>();
occupantsByNickname.put(nickname.toLowerCase(), occupants);
}
occupants.add(joinRole);
// Update the tables of occupants based on the bare and full JID
List<MUCRole> list = occupantsByBareJID.get(bareJID);
if (list == null) {
@@ -643,42 +643,20 @@ else if (outcasts.contains(bareJID)) {
try {
// Send the presence of this new occupant to existing occupants
Presence joinPresence = joinRole.getPresence().createCopy();
if (isRoomNew) {
Element frag = joinPresence.getChildElement("x", "http://jabber.org/protocol/muc#user");
frag.addElement("status").addAttribute("code", "201");
}
broadcastPresence(joinPresence);
broadcastPresence(joinPresence, true);
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
// If the room has just been created send the "room locked until configuration is
// confirmed" message
if (isRoomNew) {
Message message = new Message();
message.setType(Message.Type.groupchat);
message.setBody(LocaleUtils.getLocalizedString("muc.new"));
message.setFrom(role.getRoleAddress());
joinRole.send(message);
}
else if (isLocked()) {
// Warn the owner that the room is locked but it's not new
Message message = new Message();
message.setType(Message.Type.groupchat);
message.setBody(LocaleUtils.getLocalizedString("muc.locked"));
message.setFrom(role.getRoleAddress());
joinRole.send(message);
}
else if (canAnyoneDiscoverJID()) {
// Warn the new occupant that the room is non-anonymous (i.e. his JID will be
// public)
Message message = new Message();
message.setType(Message.Type.groupchat);
message.setBody(LocaleUtils.getLocalizedString("muc.warnnonanonymous"));
message.setFrom(role.getRoleAddress());
Element frag = message.addChildElement("x", "http://jabber.org/protocol/muc#user");
frag.addElement("status").addAttribute("code", "100");
joinRole.send(message);
if (isLocked()) {
// http://xmpp.org/extensions/xep-0045.html#enter-locked
Presence presenceItemNotFound = new Presence(Presence.Type.error);
presenceItemNotFound.setError(PacketError.Condition.item_not_found);
presenceItemNotFound.setFrom(role.getRoleAddress());
joinRole.send(presenceItemNotFound);

}
if (historyRequest == null) {
Iterator<Message> history = roomHistory.getMessageHistory();
@@ -696,13 +674,34 @@ else if (canAnyoneDiscoverJID()) {
return joinRole;
}

/**
* Can a user join this room
*
* @param user the user attempting to join this room
* @return boolean
*/
private boolean canJoinRoom(LocalMUCUser user){
boolean isOwner = owners.contains(user.getAddress().toBareJID());
boolean isAdmin = admins.contains(user.getAddress().toBareJID());
return (!isDestroyed && (!hasOccupancyLimit() || isAdmin || isOwner || (getOccupantsCount() < getMaxUsers())));
}

/**
* Does this room have an occupancy limit?
*
* @return boolean
*/
private boolean hasOccupancyLimit(){
return getMaxUsers() != 0;
}

/**
* Sends presence of existing occupants to new occupant.
*
* @param joinRole the role of the new occupant in the room.
*/
private void sendInitialPresences(LocalMUCRole joinRole) {
for (MUCRole occupant : occupants.values()) {
for (MUCRole occupant : occupantsByFullJID.values()) {
if (occupant == joinRole) {
continue;
}
@@ -729,17 +728,28 @@ private void sendInitialPresences(LocalMUCRole joinRole) {
}

public void occupantAdded(OccupantAddedEvent event) {
// Do not add new occupant with one with same nickname already exists
if (occupants.containsKey(event.getNickname().toLowerCase())) {
// TODO Handle conflict of nicknames
return;
}
// Create a proxy for the occupant that joined the room from another cluster node
RemoteMUCRole joinRole = new RemoteMUCRole(mucService, event);
JID bareJID = event.getUserAddress().asBareJID();
String nickname = event.getNickname();
List<MUCRole> occupants = occupantsByNickname.get(nickname.toLowerCase());
// Do not add new occupant with one with same nickname already exists
if (occupants == null) {
occupants = new ArrayList<MUCRole>();
occupantsByNickname.put(nickname.toLowerCase(), occupants);
} else {
// sanity check; make sure the nickname is owned by the same JID
if (occupants.size() > 0) {
String existingJID = occupants.get(0).getUserAddress().toBareJID();
if (!bareJID.equals(existingJID)) {
Log.warn(MessageFormat.format("Conflict detected; {0} requested nickname '{1}'; already being used by {2}", bareJID, nickname, existingJID));
return;
}
}
}
// Add the new user as an occupant of this room
occupants.put(event.getNickname().toLowerCase(), joinRole);
occupants.add(joinRole);
// Update the tables of occupants based on the bare and full JID
JID bareJID = event.getUserAddress().asBareJID();
List<MUCRole> list = occupantsByBareJID.get(bareJID);
if (list == null) {
list = new ArrayList<MUCRole>();
@@ -756,7 +766,7 @@ public void occupantAdded(OccupantAddedEvent event) {
}
// Check if we need to send presences of the new occupant to occupants hosted by this JVM
if (event.isSendPresence()) {
for (MUCRole occupant : occupants.values()) {
for (MUCRole occupant : occupantsByFullJID.values()) {
if (occupant.isLocal()) {
occupant.send(event.getPresence().createCopy());
}
@@ -795,8 +805,10 @@ public void leaveRoom(MUCRole leaveRole) {
leaveRole.send(presence);
}
else {
// Inform the rest of the room occupants that the user has left the room
broadcastPresence(presence);
if (getOccupantsByNickname(leaveRole.getNickname()).size() <= 1) {
// Inform the rest of the room occupants that the user has left the room
broadcastPresence(presence, false);
}
}
}
catch (Exception e) {
@@ -828,15 +840,15 @@ public void leaveRoom(OccupantLeftEvent event) {

// Remove the room from the service only if there are no more occupants and the room is
// not persistent
if (occupants.isEmpty() && !isPersistent()) {
if (occupantsByFullJID.isEmpty() && !isPersistent()) {
endTime = System.currentTimeMillis();
if (event.isOriginator()) {
mucService.removeChatRoom(name);
// Fire event that the room has been destroyed
MUCEventDispatcher.roomDestroyed(getRole().getRoleAddress());
}
}
if (occupants.isEmpty()) {
if (occupantsByFullJID.isEmpty()) {
// Update the date when the last occupant left the room
setEmptyDate(new Date());
}
@@ -854,14 +866,21 @@ public void leaveRoom(OccupantLeftEvent event) {
* @param originator true if this JVM is the one that originated the event.
*/
private void removeOccupantRole(MUCRole leaveRole, boolean originator) {
occupants.remove(leaveRole.getNickname().toLowerCase());

JID userAddress = leaveRole.getUserAddress();
// Notify the user that he/she is no longer in the room
leaveRole.destroy();
// Update the tables of occupants based on the bare and full JID
JID bareJID = userAddress.asBareJID();
List<MUCRole> list = occupantsByBareJID.get(bareJID);

String nickname = leaveRole.getNickname();
List<MUCRole> occupants = occupantsByNickname.get(nickname.toLowerCase());
if (occupants != null) {
occupants.remove(leaveRole);
if (occupants.isEmpty()) {
occupantsByNickname.remove(nickname.toLowerCase());
}
}
List<MUCRole> list = occupantsByBareJID.get(bareJID);
if (list != null) {
list.remove(leaveRole);
if (list.isEmpty()) {
@@ -878,14 +897,12 @@ private void removeOccupantRole(MUCRole leaveRole, boolean originator) {
public void destroyRoom(DestroyRoomRequest destroyRequest) {
JID alternateJID = destroyRequest.getAlternateJID();
String reason = destroyRequest.getReason();
MUCRole leaveRole;
Collection<MUCRole> removedRoles = new ArrayList<MUCRole>();
lock.writeLock().lock();
try {
boolean hasRemoteOccupants = false;
// Remove each occupant
for (String nickname: occupants.keySet()) {
leaveRole = occupants.remove(nickname);
for (MUCRole leaveRole : occupantsByFullJID.values()) {

if (leaveRole != null) {
// Add the removed occupant to the list of removed occupants. We are keeping a
@@ -980,15 +997,18 @@ public void sendPublicMessage(Message message, MUCRole senderRole) throws Forbid
// Send the message to all occupants
message.setFrom(senderRole.getRoleAddress());
send(message);
// Fire event that message was receibed by the room
// Fire event that message was received by the room
MUCEventDispatcher.messageReceived(getRole().getRoleAddress(), senderRole.getUserAddress(),
senderRole.getNickname(), message);
}

public void sendPrivatePacket(Packet packet, MUCRole senderRole) throws NotFoundException {
String resource = packet.getTo().getResource();
MUCRole occupant = occupants.get(resource.toLowerCase());
if (occupant != null) {
List<MUCRole> occupants = occupantsByNickname.get(resource.toLowerCase());
if (occupants == null || occupants.size() == 0) {
throw new NotFoundException();
}
for (MUCRole occupant : occupants) {
packet.setFrom(senderRole.getRoleAddress());
occupant.send(packet);
if(packet instanceof Message) {
@@ -997,17 +1017,14 @@ public void sendPrivatePacket(Packet packet, MUCRole senderRole) throws NotFound
message);
}
}
else {
throw new NotFoundException();
}
}

public void send(Packet packet) {
if (packet instanceof Message) {
broadcast((Message)packet);
}
else if (packet instanceof Presence) {
broadcastPresence((Presence)packet);
broadcastPresence((Presence)packet, false);
}
else if (packet instanceof IQ) {
IQ reply = IQ.createResultIQ((IQ) packet);
@@ -1044,16 +1061,18 @@ private boolean shouldBroadcastPresence(Presence presence){
* room is semi-anon and the target occupant is not a moderator.
*
* @param presence the presence to broadcast.
* @param isJoinPresence If the presence is sent in the context of joining the room.
*/
private void broadcastPresence(Presence presence) {
private void broadcastPresence(Presence presence, boolean isJoinPresence) {
if (presence == null) {
return;
}
if (!shouldBroadcastPresence(presence)) {
// Just send the presence to the sender of the presence
try {
MUCRole occupant = getOccupant(presence.getFrom().getResource());
occupant.send(presence);
for (MUCRole occupant : getOccupantsByNickname(presence.getFrom().getResource())) {
occupant.send(presence);
}
}
catch (UserNotFoundException e) {
// Do nothing
@@ -1062,11 +1081,11 @@ private void broadcastPresence(Presence presence) {
}

// Broadcast presence to occupants hosted by other cluster nodes
BroadcastPresenceRequest request = new BroadcastPresenceRequest(this, presence);
BroadcastPresenceRequest request = new BroadcastPresenceRequest(this, presence, isJoinPresence);
CacheFactory.doClusterTask(request);

// Broadcast presence to occupants connected to this JVM
request = new BroadcastPresenceRequest(this, presence);
request = new BroadcastPresenceRequest(this, presence, isJoinPresence);
request.setOriginator(true);
request.run();
}
@@ -1080,7 +1099,7 @@ public void broadcast(BroadcastPresenceRequest presenceRequest) {
if (!canAnyoneDiscoverJID()) {
jid = frag.element("item").attributeValue("jid");
}
for (MUCRole occupant : occupants.values()) {
for (MUCRole occupant : occupantsByFullJID.values()) {
if (!occupant.isLocal()) {
continue;
}
@@ -1094,17 +1113,40 @@ public void broadcast(BroadcastPresenceRequest presenceRequest) {
frag.element("item").addAttribute("jid", null);
}
}
occupant.send(presence);
// Some status codes should only be included in the "self-presence", which is only sent to the user, but not to other occupants.
if (occupant.getPresence().getFrom().equals(presence.getTo())) {
Presence selfPresence = presence.createCopy();
Element fragSelfPresence = selfPresence.getChildElement("x", "http://jabber.org/protocol/muc#user");
fragSelfPresence.addElement("status", "110");

// Only in the context of entering the room status code 100, 201 and 210 should be sent.
// http://xmpp.org/registrar/mucstatus.html
if (presenceRequest.isJoinPresence()) {
boolean isRoomNew = isLocked() && creationDate.getTime() == lockedTime;
if (canAnyoneDiscoverJID()) {
// // XEP-0045: Example 26.
// If the user is entering a room that is non-anonymous (i.e., which informs all occupants of each occupant's full JID as shown above), the service MUST warn the user by including a status code of "100" in the initial presence that the room sends to the new occupant
fragSelfPresence.addElement("status").addAttribute("code", "100");
}
if (isRoomNew) {
fragSelfPresence.addElement("status").addAttribute("code", "201");
}
}

occupant.send(selfPresence);
} else {
occupant.send(presence);
}
}
}

private void broadcast(Message message) {
// Broadcast message to occupants hosted by other cluster nodes
BroadcastMessageRequest request = new BroadcastMessageRequest(this, message, occupants.size());
BroadcastMessageRequest request = new BroadcastMessageRequest(this, message, occupantsByFullJID.size());
CacheFactory.doClusterTask(request);

// Broadcast message to occupants connected to this JVM
request = new BroadcastMessageRequest(this, message, occupants.size());
request = new BroadcastMessageRequest(this, message, occupantsByFullJID.size());
request.setOriginator(true);
request.run();
}
@@ -1114,7 +1156,7 @@ public void broadcast(BroadcastMessageRequest messageRequest) {
// Add message to the room history
roomHistory.addMessage(message);
// Send message to occupants connected to this JVM
for (MUCRole occupant : occupants.values()) {
for (MUCRole occupant : occupantsByFullJID.values()) {
// Do not send broadcast messages to deaf occupants or occupants hosted in
// other cluster nodes
if (occupant.isLocal() && !occupant.isVoiceOnly()) {
@@ -1124,8 +1166,11 @@ public void broadcast(BroadcastMessageRequest messageRequest) {
if (messageRequest.isOriginator() && isLogEnabled()) {
MUCRole senderRole = null;
JID senderAddress;
// convert the MUC nickname/role JID back into a real user JID
if (message.getFrom() != null && message.getFrom().getResource() != null) {
senderRole = occupants.get(message.getFrom().getResource().toLowerCase());
// get the first MUCRole for the sender
List<MUCRole> occupants = occupantsByNickname.get(message.getFrom().getResource().toLowerCase());
senderRole = occupants == null ? null : occupants.get(0);
}
if (senderRole == null) {
// The room itself is sending the message
@@ -1250,7 +1295,7 @@ public long getChatLength() {
if (role.isLocal()) {
role.setAffiliation(newAffiliation);
role.setRole(newRole);
// Notify the othe cluster nodes to update the occupant
// Notify the other cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, role));
// Prepare a new presence to be sent to all the room occupants
presences.add(role.getPresence().createCopy());
@@ -1290,7 +1335,7 @@ private Presence changeOccupantRole(JID jid, MUCRole.Role newRole) throws NotAll
if (role.isLocal()) {
// Update the presence with the new role
role.setRole(newRole);
// Notify the othe cluster nodes to update the occupant
// Notify the other cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, role));
// Prepare a new presence to be sent to all the room occupants
return role.getPresence().createCopy();
@@ -1448,7 +1493,7 @@ private boolean removeAdmin(JID bareJID) {
}
}
// Check if the desired nickname is already reserved for another member
if (nickname != null && nickname.trim().length() > 0 && members.containsValue(nickname)) {
if (nickname != null && nickname.trim().length() > 0 && members.containsValue(nickname.toLowerCase())) {
if (!nickname.equals(members.get(bareJID))) {
throw new ConflictException();
}
@@ -1459,7 +1504,7 @@ private boolean removeAdmin(JID bareJID) {
}
// Associate the reserved nickname with the bareJID. If nickname is null then associate an
// empty string
members.put(bareJID, (nickname == null ? "" : nickname));
members.put(bareJID, (nickname == null ? "" : nickname.toLowerCase()));
// Remove the user from other affiliation lists
if (removeOwner(bareJID)) {
oldAffiliation = MUCRole.Affiliation.owner;
@@ -1672,7 +1717,7 @@ public void presenceUpdated(MUCRole occupantRole, Presence newPresence) {
request.run();

// Broadcast new presence of occupant
broadcastPresence(occupantRole.getPresence().createCopy());
broadcastPresence(occupantRole.getPresence().createCopy(), false);
}

/**
@@ -1681,54 +1726,59 @@ public void presenceUpdated(MUCRole occupantRole, Presence newPresence) {
* @param updatePresence request to update an occupant's presence.
*/
public void presenceUpdated(UpdatePresence updatePresence) {
MUCRole occupantRole = occupants.get(updatePresence.getNickname().toLowerCase());
if (occupantRole != null) {
occupantRole.setPresence(updatePresence.getPresence());
}
else {
List <MUCRole> occupants = occupantsByNickname.get(updatePresence.getNickname().toLowerCase());
if (occupants == null || occupants.size() == 0) {
Log.debug("LocalMUCRoom: Failed to update presence of room occupant. Occupant nickname: " + updatePresence.getNickname());
}
} else {
for (MUCRole occupant : occupants) {
occupant.setPresence(updatePresence.getPresence());
}
}
}

public void occupantUpdated(UpdateOccupant update) {
MUCRole occupantRole = occupants.get(update.getNickname().toLowerCase());
if (occupantRole != null) {
if (!occupantRole.isLocal()) {
occupantRole.setPresence(update.getPresence());
try {
occupantRole.setRole(update.getRole());
occupantRole.setAffiliation(update.getAffiliation());
} catch (NotAllowedException e) {
// Ignore. Should never happen with remote roles
List <MUCRole> occupants = occupantsByNickname.get(update.getNickname().toLowerCase());
if (occupants == null || occupants.size() == 0) {
Log.debug("LocalMUCRoom: Failed to update information of room occupant. Occupant nickname: " + update.getNickname());
} else {
for (MUCRole occupant : occupants) {
if (!occupant.isLocal()) {
occupant.setPresence(update.getPresence());
try {
occupant.setRole(update.getRole());
occupant.setAffiliation(update.getAffiliation());
} catch (NotAllowedException e) {
// Ignore. Should never happen with remote roles
}
}
else {
Log.error(MessageFormat.format("Ignoring update of local occupant with info from a remote occupant. "
+ "Occupant nickname: {0} new role: {1} new affiliation: {2}",
update.getNickname(), update.getRole(), update.getAffiliation()));
}
}
else {
Log.error("Tried to update local occupant with info of local occupant?. Occupant nickname: " +
update.getNickname() + " new role: " + update.getRole() + " new affiliation: " +
update.getAffiliation());
}
}
else {
Log.debug("LocalMUCRoom: Failed to update information of room occupant. Occupant nickname: " + update.getNickname());
}
}
}

public Presence updateOccupant(UpdateOccupantRequest updateRequest) throws NotAllowedException {
MUCRole occupantRole = occupants.get(updateRequest.getNickname().toLowerCase());
if (occupantRole != null) {
if (updateRequest.isAffiliationChanged()) {
occupantRole.setAffiliation(updateRequest.getAffiliation());
}
occupantRole.setRole(updateRequest.getRole());
// Notify the othe cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, occupantRole));
return occupantRole.getPresence();
}
else {
Log.debug("LocalMUCRoom: Failed to update information of local room occupant. Occupant nickname: " +
updateRequest.getNickname());
}
return null;
Presence result = null;
List <MUCRole> occupants = occupantsByNickname.get(updateRequest.getNickname().toLowerCase());
if (occupants == null || occupants.size() == 0) {
Log.debug("Failed to update information of local room occupant; nickname: " + updateRequest.getNickname());
} else {
for (MUCRole occupant : occupants) {
if (updateRequest.isAffiliationChanged()) {
occupant.setAffiliation(updateRequest.getAffiliation());
}
occupant.setRole(updateRequest.getRole());
// Notify the the cluster nodes to update the occupant
CacheFactory.doClusterTask(new UpdateOccupant(this, occupant));
if (result == null) {
result = occupant.getPresence();
}
}
}
return result;
}

public void memberAdded(AddMember addMember) {
@@ -1737,7 +1787,7 @@ public void memberAdded(AddMember addMember) {
removeAdmin(bareJID);
removeOutcast(bareJID);
// Associate the reserved nickname with the bareJID
members.put(addMember.getBareJID(), addMember.getNickname());
members.put(addMember.getBareJID(), addMember.getNickname().toLowerCase());
}

public void affiliationAdded(AddAffiliation affiliation) {
@@ -1782,25 +1832,27 @@ public void nicknameChanged(MUCRole occupantRole, Presence newPresence, String o
request.run();

// Broadcast new presence of occupant
broadcastPresence(occupantRole.getPresence().createCopy());
broadcastPresence(occupantRole.getPresence().createCopy(), false);
}

public void nicknameChanged(ChangeNickname changeNickname) {
MUCRole occupantRole = occupants.get(changeNickname.getOldNick().toLowerCase());
if (occupantRole != null) {
// Update the role with the new info
occupantRole.setPresence(changeNickname.getPresence());
occupantRole.changeNickname(changeNickname.getNewNick());
if (changeNickname.isOriginator()) {
// Fire event that user changed his nickname
MUCEventDispatcher.nicknameChanged(getRole().getRoleAddress(), occupantRole.getUserAddress(),
changeNickname.getOldNick(), changeNickname.getNewNick());
}
// Associate the existing MUCRole with the new nickname
occupants.put(changeNickname.getNewNick().toLowerCase(), occupantRole);
// Remove the old nickname
occupants.remove(changeNickname.getOldNick().toLowerCase());
}
List<MUCRole> occupants = occupantsByNickname.get(changeNickname.getOldNick().toLowerCase());
if (occupants != null && occupants.size() > 0) {
for (MUCRole occupant : occupants) {
// Update the role with the new info
occupant.setPresence(changeNickname.getPresence());
occupant.changeNickname(changeNickname.getNewNick());
}
if (changeNickname.isOriginator()) {
// Fire event that user changed his nickname
MUCEventDispatcher.nicknameChanged(getRole().getRoleAddress(), occupants.get(0).getUserAddress(),
changeNickname.getOldNick(), changeNickname.getNewNick());
}
// Associate the existing MUCRole with the new nickname
occupantsByNickname.put(changeNickname.getNewNick().toLowerCase(), occupants);
// Remove the old nickname
occupantsByNickname.remove(changeNickname.getOldNick().toLowerCase());
}
}

public void changeSubject(Message packet, MUCRole role) throws ForbiddenException {
@@ -1942,7 +1994,7 @@ public MUCRoomHistory getRoomHistory() {

public Collection<MUCRole> getModerators() {
List<MUCRole> moderators = new ArrayList<MUCRole>();
for (MUCRole role : occupants.values()) {
for (MUCRole role : occupantsByFullJID.values()) {
if (MUCRole.Role.moderator == role.getRole()) {
moderators.add(role);
}
@@ -1952,7 +2004,7 @@ public MUCRoomHistory getRoomHistory() {

public Collection<MUCRole> getParticipants() {
List<MUCRole> participants = new ArrayList<MUCRole>();
for (MUCRole role : occupants.values()) {
for (MUCRole role : occupantsByFullJID.values()) {
if (MUCRole.Role.participant == role.getRole()) {
participants.add(role);
}
@@ -2020,7 +2072,7 @@ public Presence kickOccupant(JID jid, JID actorJID, String reason)
kickPresence(updatedPresence, actorJID);

//Inform the other occupants that user has been kicked
broadcastPresence(updatedPresence);
broadcastPresence(updatedPresence, false);
}
return updatedPresence;
}
@@ -2035,10 +2087,9 @@ public Presence kickOccupant(JID jid, JID actorJID, String reason)
* was not provided.
*/
private void kickPresence(Presence kickPresence, JID actorJID) {
MUCRole kickedRole;
// Get the role to kick
kickedRole = occupants.get(kickPresence.getFrom().getResource().toLowerCase());
if (kickedRole != null) {
// Get the role(s) to kick
List<MUCRole> occupants = occupantsByNickname.get(kickPresence.getFrom().getResource().toLowerCase());
for (MUCRole kickedRole : occupants) {
kickPresence = kickPresence.createCopy();
// Add the actor's JID that kicked this user from the room
if (actorJID != null && actorJID.toString().length() > 0) {
@@ -2108,7 +2159,7 @@ public boolean isMembersOnly() {
if (membersOnly && !this.membersOnly) {
// If the room was not members-only and now it is, kick occupants that aren't member
// of the room
for (MUCRole occupant : occupants.values()) {
for (MUCRole occupant : occupantsByFullJID.values()) {
if (occupant.getAffiliation().compareTo(MUCRole.Affiliation.member) > 0) {
try {
presences.add(kickOccupant(occupant.getRoleAddress(), null,
@@ -2241,14 +2292,6 @@ public void lock(MUCRole senderRole) throws ForbiddenException {
return;
}
setLocked(true);
if (senderRole.getUserAddress() != null) {
// Send to the occupant that locked the room a message saying so
Message message = new Message();
message.setType(Message.Type.groupchat);
message.setBody(LocaleUtils.getLocalizedString("muc.locked"));
message.setFrom(getRole().getRoleAddress());
senderRole.send(message);
}
}

public void unlock(MUCRole senderRole) throws ForbiddenException {
@@ -2260,14 +2303,6 @@ public void unlock(MUCRole senderRole) throws ForbiddenException {
return;
}
setLocked(false);
if (senderRole.getUserAddress() != null) {
// Send to the occupant that unlocked the room a message saying so
Message message = new Message();
message.setType(Message.Type.groupchat);
message.setBody(LocaleUtils.getLocalizedString("muc.unlocked"));
message.setFrom(getRole().getRoleAddress());
senderRole.send(message);
}
}

private void setLocked(boolean locked) {
@@ -2490,4 +2525,53 @@ public String getUID()
// name is unique for each one particular MUC service.
return name;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((creationDate == null) ? 0 : creationDate.hashCode());
result = prime * result
+ ((description == null) ? 0 : description.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result
+ ((password == null) ? 0 : password.hashCode());
result = prime * result + (int) (roomID ^ (roomID >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LocalMUCRoom other = (LocalMUCRoom) obj;
if (creationDate == null) {
if (other.creationDate != null)
return false;
} else if (!creationDate.equals(other.creationDate))
return false;
if (description == null) {
if (other.description != null)
return false;
} else if (!description.equals(other.description))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (roomID != other.roomID)
return false;
return true;
}
}
@@ -369,7 +369,7 @@ else if (userInfo != null

public void process(IQ packet) {
// Ignore IQs of type ERROR or RESULT sent to a room
if (IQ.Type.error == packet.getType()) {
if (IQ.Type.error == packet.getType() || IQ.Type.result == packet.getType()) {
return;
}
lastPacketTime = System.currentTimeMillis();
@@ -529,7 +529,9 @@ public void process(Presence packet) {
else {
if (packet.isAvailable()) {
// A resource is required in order to join a room
sendErrorPacket(packet, PacketError.Condition.bad_request);
// http://xmpp.org/extensions/xep-0045.html#enter
// If the user does not specify a room nickname (note the bare JID on the 'from' address in the following example), the service MUST return a <jid-malformed/> error
sendErrorPacket(packet, PacketError.Condition.jid_malformed);
}
// TODO: send error message to user (can't send packets to group you haven't
// joined)
@@ -601,4 +603,29 @@ else if (role.getChatRoom().hasOccupant(resource)) {
}
}
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((realjid == null) ? 0 : realjid.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LocalMUCUser other = (LocalMUCUser) obj;
if (realjid == null) {
if (other.realjid != null)
return false;
} else if (!realjid.equals(other.realjid))
return false;
return true;
}
}
@@ -31,6 +31,7 @@
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.muc.*;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,6 +51,9 @@
public class MUCPersistenceManager {

private static final Logger Log = LoggerFactory.getLogger(MUCPersistenceManager.class);

// property name for optional number of days to limit persistent MUC history during reload (OF-764)
private static final String MUC_HISTORY_RELOAD_LIMIT = "xmpp.muc.history.reload.limit";

private static final String GET_RESERVED_NAME =
"SELECT nickname FROM ofMucMember WHERE roomID=? AND jid=?";
@@ -202,14 +206,14 @@ public static void loadFromDB(LocalMUCRoom room) {
room.setLogEnabled(rs.getInt(16) == 1);
room.setSubject(rs.getString(17));
List<String> rolesToBroadcast = new ArrayList<String>();
String roles = Integer.toBinaryString(rs.getInt(18));
String roles = StringUtils.zeroPadString(Integer.toBinaryString(rs.getInt(18)), 3);
if (roles.charAt(0) == '1') {
rolesToBroadcast.add("moderator");
}
if (roles.length() > 1 && roles.charAt(1) == '1') {
if (roles.charAt(1) == '1') {
rolesToBroadcast.add("participant");
}
if (roles.length() > 2 && roles.charAt(2) == '1') {
if (roles.charAt(2) == '1') {
rolesToBroadcast.add("visitor");
}
room.setRolesToBroadcastPresence(rolesToBroadcast);
@@ -219,21 +223,28 @@ public static void loadFromDB(LocalMUCRoom room) {
room.setPersistent(true);
DbConnectionManager.fastcloseStmt(rs, pstmt);

pstmt = con.prepareStatement(LOAD_HISTORY);
// Recreate the history until two days ago
long from = System.currentTimeMillis() - (86400000 * 2);
pstmt.setString(1, StringUtils.dateToMillis(new Date(from)));
pstmt.setLong(2, room.getID());
rs = pstmt.executeQuery();
while (rs.next()) {
String senderJID = rs.getString(1);
String nickname = rs.getString(2);
Date sentDate = new Date(Long.parseLong(rs.getString(3).trim()));
String subject = rs.getString(4);
String body = rs.getString(5);
// Recreate the history only for the rooms that have the conversation logging
// enabled
if (room.isLogEnabled()) {
// Recreate the history only for the rooms that have the conversation logging
// enabled
if (room.isLogEnabled()) {
pstmt = con.prepareStatement(LOAD_HISTORY);
// Reload the history, using "muc.history.reload.limit" (days) if present
long from = 0;
String reloadLimit = JiveGlobals.getProperty(MUC_HISTORY_RELOAD_LIMIT);
if (reloadLimit != null) {
// if the property is defined, but not numeric, default to 2 (days)
int reloadLimitDays = JiveGlobals.getIntProperty(MUC_HISTORY_RELOAD_LIMIT, 2);
Log.warn("MUC history reload limit set to " + reloadLimitDays + " days");
from = System.currentTimeMillis() - (86400000 * reloadLimitDays);
}
pstmt.setString(1, StringUtils.dateToMillis(new Date(from)));
pstmt.setLong(2, room.getID());
rs = pstmt.executeQuery();
while (rs.next()) {
String senderJID = rs.getString(1);
String nickname = rs.getString(2);
Date sentDate = new Date(Long.parseLong(rs.getString(3).trim()));
String subject = rs.getString(4);
String body = rs.getString(5);
room.getRoomHistory().addOldMessage(senderJID, nickname, sentDate, subject,
body);
}
@@ -499,14 +510,14 @@ public static void deleteFromDB(MUCRoom room) {
room.setLogEnabled(resultSet.getInt(17) == 1);
room.setSubject(resultSet.getString(18));
List<String> rolesToBroadcast = new ArrayList<String>();
String roles = Integer.toBinaryString(resultSet.getInt(19));
String roles = StringUtils.zeroPadString(Integer.toBinaryString(resultSet.getInt(19)), 3);
if (roles.charAt(0) == '1') {
rolesToBroadcast.add("moderator");
}
if (roles.length() > 1 && roles.charAt(1) == '1') {
if (roles.charAt(1) == '1') {
rolesToBroadcast.add("participant");
}
if (roles.length() > 2 && roles.charAt(2) == '1') {
if (roles.charAt(2) == '1') {
rolesToBroadcast.add("visitor");
}
room.setRolesToBroadcastPresence(rolesToBroadcast);
@@ -534,27 +545,32 @@ private static void loadHistory(Long serviceID, Map<Long, LocalMUCRoom> rooms) t
connection = DbConnectionManager.getConnection();
statement = connection.prepareStatement(LOAD_ALL_HISTORY);

final long from = System.currentTimeMillis() - (86400000 * 2); // Recreate the history until two days ago
// Reload the history, using "muc.history.reload.limit" (days) if present
long from = 0;
String reloadLimit = JiveGlobals.getProperty(MUC_HISTORY_RELOAD_LIMIT);
if (reloadLimit != null) {
// if the property is defined, but not numeric, default to 2 (days)
int reloadLimitDays = JiveGlobals.getIntProperty(MUC_HISTORY_RELOAD_LIMIT, 2);
Log.warn("MUC history reload limit set to " + reloadLimitDays + " days");
from = System.currentTimeMillis() - (86400000 * reloadLimitDays);
}
statement.setLong(1, serviceID);
statement.setString(2, StringUtils.dateToMillis(new Date(from)));
resultSet = statement.executeQuery();

while (resultSet.next()) {
try {
LocalMUCRoom room = rooms.get(resultSet.getLong(1));
// Skip to the next position if the room does not exist
if (room == null) {
// Skip to the next position if the room does not exist or if history is disabled
if (room == null || !room.isLogEnabled()) {
continue;
}
String senderJID = resultSet.getString(2);
String nickname = resultSet.getString(3);
Date sentDate = new Date(Long.parseLong(resultSet.getString(4).trim()));
String subject = resultSet.getString(5);
String body = resultSet.getString(6);
// Recreate the history only for the rooms that have the conversation logging enabled.
if (room.isLogEnabled()) {
room.getRoomHistory().addOldMessage(senderJID, nickname, sentDate, subject, body);
}
room.getRoomHistory().addOldMessage(senderJID, nickname, sentDate, subject, body);
} catch (SQLException e) {
Log.warn("A database exception prevented the history for one particular MUC room to be loaded from the database.", e);
}
@@ -83,7 +83,7 @@
* the rooms after a period of time and to maintain a log of the conversation in the rooms that
* require to log their conversations. The conversations log is saved to the database using a
* separate process<p>
*
* <p/>
* Temporary rooms are held in memory as long as they have occupants. They will be destroyed after
* the last occupant left the room. On the other hand, persistent rooms are always present in memory
* even after the last occupant left the room. In order to keep memory clean of persistent rooms that
@@ -174,6 +174,12 @@
*/
private boolean allowToDiscoverLockedRooms = true;

/**
* Flag that indicates if the service should provide information about non-public members-only
* rooms when handling service discovery requests.
*/
private boolean allowToDiscoverMembersOnlyRooms = false;

/**
* Returns the permission policy for creating rooms. A true value means that not anyone can
* create a room, only the JIDs listed in <code>allowedToCreate</code> are allowed to create
@@ -365,7 +371,7 @@ public void initialize(JID jid, ComponentManager componentManager) {
}

public void shutdown() {

stop();
}

public String getServiceDomain() {
@@ -838,6 +844,30 @@ public void removeSysadmin(JID userJID) {
MUCPersistenceManager.setProperty(chatServiceName, "sysadmin.jid", fromArray(jids));
}

/**
* Returns the flag that indicates if the service should provide information about non-public
* members-only rooms when handling service discovery requests.
*
* @return true if the service should provide information about non-public members-only rooms.
*/
public boolean isAllowToDiscoverMembersOnlyRooms() {
return allowToDiscoverMembersOnlyRooms;
}

/**
* Sets the flag that indicates if the service should provide information about non-public
* members-only rooms when handling service discovery requests.
*
* @param allowToDiscoverMembersOnlyRooms
* if the service should provide information about
* non-public members-only rooms.
*/
public void setAllowToDiscoverMembersOnlyRooms(boolean allowToDiscoverMembersOnlyRooms) {
this.allowToDiscoverMembersOnlyRooms = allowToDiscoverMembersOnlyRooms;
MUCPersistenceManager.setProperty(chatServiceName, "discover.membersOnly",
Boolean.toString(allowToDiscoverMembersOnlyRooms));
}

/**
* Returns the flag that indicates if the service should provide information about locked rooms
* when handling service discovery requests.
@@ -954,6 +984,8 @@ public void initializeSettings() {
}
allowToDiscoverLockedRooms =
MUCPersistenceManager.getBooleanProperty(chatServiceName, "discover.locked", true);
allowToDiscoverMembersOnlyRooms =
MUCPersistenceManager.getBooleanProperty(chatServiceName, "discover.membersOnly", true);
roomCreationRestricted =
MUCPersistenceManager.getBooleanProperty(chatServiceName, "create.anyone", false);
// Load the list of JIDs that are allowed to create a MUC room
@@ -1052,7 +1084,7 @@ public void start() {
}
}

public void stop() {
private void stop() {
XMPPServer.getInstance().getIQDiscoItemsHandler().removeServerItemsProvider(this);
XMPPServer.getInstance().getIQDiscoInfoHandler().removeServerNodeInfoProvider(this.getServiceDomain());
XMPPServer.getInstance().getServerItemsProviders().remove(this);
@@ -1067,10 +1099,7 @@ public void enableService(boolean enabled, boolean persistent) {
// Do nothing if the service status has not changed
return;
}
XMPPServer server = XMPPServer.getInstance();
if (!enabled) {
// Disable disco information
server.getIQDiscoItemsHandler().removeServerItemsProvider(this);
// Stop the service/module
stop();
}
@@ -1081,8 +1110,6 @@ public void enableService(boolean enabled, boolean persistent) {
if (enabled) {
// Start the service/module
start();
// Enable disco information
server.getIQDiscoItemsHandler().addServerItemsProvider(this);
}
}

@@ -1228,7 +1255,7 @@ public void messageBroadcastedTo(int numOccupants) {
else if (name != null && node == null) {
// Answer the identity of a given room
MUCRoom room = getChatRoom(name);
if (room != null && canDiscoverRoom(room)) {
if (room != null) {
Element identity = DocumentHelper.createElement("identity");
identity.addAttribute("category", "conference");
identity.addAttribute("name", room.getNaturalLanguageName());
@@ -1271,10 +1298,13 @@ else if (name != null && "x-roomuser-item".equals(node)) {
else if (name != null && node == null) {
// Answer the features of a given room
MUCRoom room = getChatRoom(name);
if (room != null && canDiscoverRoom(room)) {
if (room != null) {
features.add("http://jabber.org/protocol/muc");
// Always add public since only public rooms can be discovered
features.add("muc_public");
if (room.isPublicRoom()) {
features.add("muc_public");
} else {
features.add("muc_hidden");
}
if (room.isMembersOnly()) {
features.add("muc_membersonly");
}
@@ -1314,7 +1344,7 @@ public DataForm getExtendedInfo(String name, String node, JID senderJID) {
if (name != null && node == null) {
// Answer the extended info of a given room
MUCRoom room = getChatRoom(name);
if (room != null && canDiscoverRoom(room)) {
if (room != null) {
final DataForm dataForm = new DataForm(Type.result);

final FormField fieldType = dataForm.addField();
@@ -1445,7 +1475,7 @@ else if (name != null && "x-roomuser-item".equals(node)) {
// Answer all the public rooms as items
for (MUCRoom room : rooms.values())
{
if (canDiscoverRoom(room))
if (canDiscoverRoom(room, senderJID))
{
answer.add(new DiscoItem(room.getRole().getRoleAddress(),
room.getNaturalLanguageName(), null, null));
@@ -1455,7 +1485,7 @@ else if (name != null && "x-roomuser-item".equals(node)) {
else if (name != null && node == null) {
// Answer the room occupants as items if that info is publicly available
MUCRoom room = getChatRoom(name);
if (room != null && canDiscoverRoom(room)) {
if (room != null && canDiscoverRoom(room, senderJID)) {
for (MUCRole role : room.getOccupants()) {
// TODO Should we filter occupants that are invisible (presence is not broadcasted)?
answer.add(new DiscoItem(role.getRoleAddress(), null, null, null));
@@ -1465,12 +1495,23 @@ else if (name != null && node == null) {
return answer.iterator();
}

private boolean canDiscoverRoom(MUCRoom room) {
private boolean canDiscoverRoom(MUCRoom room, JID senderJID) {
// Check if locked rooms may be discovered
if (!allowToDiscoverLockedRooms && room.isLocked()) {
return false;
}
return room.isPublicRoom();
if (!room.isPublicRoom()) {
if (!allowToDiscoverMembersOnlyRooms && room.isMembersOnly()) {
return false;
}
MUCRole.Affiliation affiliation = room.getAffiliation(senderJID.asBareJID());
if (affiliation != MUCRole.Affiliation.owner
&& affiliation != MUCRole.Affiliation.admin
&& affiliation != MUCRole.Affiliation.member) {
return false;
}
}
return true;
}

/**
@@ -362,25 +362,48 @@ public void resetInput() {
inputEncoding = oldEncoding;
}

private boolean highSurrogateSeen = false;

/**
* Makes sure that each individual character is a valid XML character.
*
* Note that when MXParser is being modified to handle multibyte chars correctly, this method needs to change (as
* then, there are more codepoints to check).
*
*/
@Override
protected char more() throws IOException, XmlPullParserException {
final char codePoint = super.more(); // note - this does NOT return a codepoint now, but simply a (single byte) character!
final char codePoint = super.more(); // note - this does NOT return a codepoint now, but simply a (double byte) character!
boolean validCodepoint = false;
boolean isLowSurrogate = Character.isLowSurrogate(codePoint);
if ((codePoint == 0x0) || // 0x0 is not allowed, but flash clients insist on sending this as the very first character of a stream. We should stop allowing this codepoint after the first byte has been parsed.
(codePoint == 0x9) ||
(codePoint == 0x9) ||
(codePoint == 0xA) ||
(codePoint == 0xD) ||
((codePoint >= 0x20) && (codePoint <= 0xD7FF)) ||
((codePoint >= 0xE000) && (codePoint <= 0xFFFD)) ||
((codePoint >= 0x10000) && (codePoint <= 0x10FFFF))) {
((codePoint >= 0xE000) && (codePoint <= 0xFFFD))) {
validCodepoint = true;
}
else if (highSurrogateSeen) {
if (isLowSurrogate) {
validCodepoint = true;
} else {
throw new XmlPullParserException("High surrogate followed by non low surrogate '0x" + String.format("%x", (int) codePoint) + "'");
}
}
else if (isLowSurrogate) {
throw new XmlPullParserException("Low surrogate '0x " + String.format("%x", (int) codePoint) + " without preceeding high surrogate");
}
else if (Character.isHighSurrogate(codePoint)) {
highSurrogateSeen = true;
// Return here so that highSurrogateSeen is not reset
return codePoint;
}

throw new XmlPullParserException("Illegal XML character: " + Integer.parseInt(codePoint+"", 16));
// Always reset high surrogate seen
highSurrogateSeen = false;
if (validCodepoint)
return codePoint;

throw new XmlPullParserException("Illegal XML character '0x" + String.format("%x", (int) codePoint) + "'");
}
}
@@ -98,7 +98,7 @@

public enum ElementType {

AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");
ABORT("abort"), AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");

private String name = null;

@@ -121,6 +121,32 @@ public static ElementType valueof(String name) {
}
}

private enum Failure {

ABORTED("aborted"),
ACCOUNT_DISABLED("account-disabled"),
CREDENTIALS_EXPIRED("credentials-expired"),
ENCRYPTION_REQUIRED("encryption-required"),
INCORRECT_ENCODING("incorrect-encoding"),
INVALID_AUTHZID("invalid-authzid"),
INVALID_MECHANISM("invalid-mechanism"),
MALFORMED_REQUEST("malformed-request"),
MECHANISM_TOO_WEAK("mechanism-too-weak"),
NOT_AUTHORIZED("not-authorized"),
TEMPORARY_AUTH_FAILURE("temporary-auth-failure");

private String name = null;

private Failure(String name) {
this.name = name;
}

@Override
public String toString() {
return name;
}
}

public enum Status {
/**
* Entity needs to respond last challenge. Session is still negotiating
@@ -232,8 +258,19 @@ public static Status handle(LocalSession session, Element doc) throws Unsupporte
if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
ElementType type = ElementType.valueof(doc.getName());
switch (type) {
case ABORT:
authenticationFailed(session, Failure.ABORTED);
status = Status.failed;
break;
case AUTH:
mechanism = doc.attributeValue("mechanism");
// http://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism
// The initiating entity did not specify a mechanism
if (mechanism == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
break;
}
// Store the requested SASL mechanism by the client
session.setSessionData("SaslMechanism", mechanism);
//Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
@@ -256,6 +293,12 @@ else if (mechanisms.contains(mechanism)) {
SaslServer ss = Sasl.createSaslServer(mechanism, "xmpp",
JiveGlobals.getProperty("xmpp.fqdn", session.getServerName()), props,
new XMPPCallbackHandler());

if (ss == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
return Status.failed;
}

// evaluateResponse doesn't like null parameter
byte[] token = new byte[0];
if (doc.getText().length() > 0) {
@@ -286,14 +329,14 @@ else if (mechanisms.contains(mechanism)) {
}
catch (SaslException e) {
Log.info("User Login Failed. " + e.getMessage());
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn("Client wants to do a MECH we don't support: '" +
mechanism + "'");
authenticationFailed(session);
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
@@ -337,33 +380,33 @@ else if (mechanisms.contains(mechanism)) {
}
catch (SaslException e) {
Log.debug("SASLAuthentication: SaslException", e);
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.error("SaslServer is null, should be valid object instead.");
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn(
"Client responded to a MECH we don't support: '" + mechanism + "'");
authenticationFailed(session);
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
default:
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
// Ignore
break;
}
}
else {
Log.debug("SASLAuthentication: Unknown namespace sent in auth element: " + doc.asXML());
authenticationFailed(session);
authenticationFailed(session, Failure.MALFORMED_REQUEST);
status = Status.failed;
}
// Check if SASL authentication has finished so we can clean up temp information
@@ -461,7 +504,7 @@ private static Status doAnonymousAuthentication(LocalSession session) {
forbidAccess = true;
}
if (forbidAccess) {
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Just accept the authentication :)
@@ -470,7 +513,7 @@ private static Status doAnonymousAuthentication(LocalSession session) {
}
else {
// anonymous login is disabled so close the connection
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
}
@@ -521,13 +564,13 @@ else if (session instanceof LocalClientSession) {
Log.debug("SASLAuthentication: EXTERNAL authentication via SSL certs for c2s connection");

// This may be null, we will deal with that later
String username = new String(StringUtils.decodeBase64(doc.getTextTrim()), CHARSET);
String username = new String(StringUtils.decodeBase64(doc.getTextTrim()), CHARSET);
String principal = "";
ArrayList<String> principals = new ArrayList<String>();
Connection connection = session.getConnection();
if (connection.getPeerCertificates().length < 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but no certificates found.");
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}

@@ -578,7 +621,7 @@ else if (session instanceof LocalClientSession) {
} else {
Log.debug("SASLAuthentication: unknown session type. Cannot perform EXTERNAL authentication");
}
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}

@@ -603,7 +646,7 @@ private static Status doSharedSecretAuthentication(LocalSession session, Element
return Status.authenticated;
}
// Otherwise, authentication failed.
authenticationFailed(session);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}

@@ -628,7 +671,7 @@ private static void authenticationSuccessful(LocalSession session, String userna
if (username != null && LockOutManager.getInstance().isAccountDisabled(username)) {
// Interception! This person is locked out, fail instead!
LockOutManager.getInstance().recordFailedLogin(username);
authenticationFailed(session);
authenticationFailed(session, Failure.ACCOUNT_DISABLED);
return;
}
StringBuilder reply = new StringBuilder(80);
@@ -653,10 +696,11 @@ else if (session instanceof IncomingServerSession) {
}
}

private static void authenticationFailed(LocalSession session) {
private static void authenticationFailed(LocalSession session, Failure failure) {
StringBuilder reply = new StringBuilder(80);
reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
reply.append("<not-authorized/></failure>");
reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><");
reply.append(failure.toString());
reply.append("/></failure>");
session.deliverRawText(reply.toString());
// Give a number of retries before closing the connection
Integer retries = (Integer) session.getSessionData("authRetries");
@@ -712,7 +756,7 @@ public static void removeSupportedMechanism(String mechanism) {
if (mech.equals("CRAM-MD5") || mech.equals("DIGEST-MD5")) {
// Check if the user provider in use supports passwords retrieval. Accessing
// to the users passwords will be required by the CallbackHandler
if (!AuthFactory.getAuthProvider().supportsPasswordRetrieval()) {
if (!AuthFactory.supportsPasswordRetrieval()) {
it.remove();
}
}
@@ -182,7 +182,7 @@ else if ("presence".equals(tag)) {
packet.getType();
}
catch (IllegalArgumentException e) {
Log.warn("Invalid presence type", e);
Log.debug("Invalid presence (type): " + packet);
// The presence packet contains an invalid presence type so replace it with
// an available presence type
packet.setType(null);
@@ -192,7 +192,7 @@ else if ("presence".equals(tag)) {
packet.getShow();
}
catch (IllegalArgumentException e) {
Log.warn("Invalid presence show", e);
Log.debug("Invalid presence (show): " + packet);
// The presence packet contains an invalid presence show so replace it with
// an available presence show
packet.setShow(null);
@@ -19,9 +19,6 @@

package org.jivesoftware.openfire.net;

import java.io.IOException;
import java.io.StringReader;

import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
@@ -38,13 +35,10 @@
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;
import org.xmpp.packet.Roster;
import org.xmpp.packet.StreamError;
import org.xmpp.packet.*;

import java.io.IOException;
import java.io.StringReader;

/**
* A StanzaHandler is the main responsible for handling incoming stanzas. Some stanzas like startTLS
@@ -53,7 +47,7 @@
* @author Gaston Dombiak
*/
public abstract class StanzaHandler {

private static final Logger Log = LoggerFactory.getLogger(StanzaHandler.class);

/**
@@ -177,8 +171,7 @@ else if ("auth".equals(tag)) {
startedSASL = true;
// Process authentication stanza
saslStatus = SASLAuthentication.handle(session, doc);
}
else if (startedSASL && "response".equals(tag)) {
} else if (startedSASL && "response".equals(tag) || "abort".equals(tag)) {
// User is responding to SASL challenge. Process response
saslStatus = SASLAuthentication.handle(session, doc);
}
@@ -602,36 +595,35 @@ protected void createSession(XmlPullParser xpp) throws XmlPullParserException, I
// subdomain. If the value of the 'to' attribute is not valid then return a host-unknown
// error and close the underlying connection.
String host = xpp.getAttributeValue("", "to");
StreamError streamError = null;
if (validateHost() && isHostUnknown(host)) {
StringBuilder sb = new StringBuilder(250);
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
// Append stream header
sb.append("<stream:stream ");
sb.append("from=\"").append(serverName).append("\" ");
sb.append("id=\"").append(StringUtils.randomString(5)).append("\" ");
sb.append("xmlns=\"").append(xpp.getNamespace(null)).append("\" ");
sb.append("xmlns:stream=\"").append(xpp.getNamespace("stream")).append("\" ");
sb.append("version=\"1.0\">");
// Set the host_unknown error
StreamError error = new StreamError(StreamError.Condition.host_unknown);
sb.append(error.toXML());
// Deliver stanza
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
streamError = new StreamError(StreamError.Condition.host_unknown);
// Log a warning so that admins can track this cases from the server side
Log.warn("Closing session due to incorrect hostname in stream header. Host: " + host +
". Connection: " + connection);
}
// Validate the stream namespace
else if (!"http://etherx.jabber.org/streams".equals(xpp.getNamespace())) {
// Include the invalid-namespace in the response
streamError = new StreamError(StreamError.Condition.invalid_namespace);
// Log a warning so that admins can track this cases from the server side
Log.warn("Closing session due to invalid_namespace in stream header. Namespace: " +
xpp.getNamespace() + ". Connection: " + connection);

}
// Create the correct session based on the sent namespace. At this point the server
// may offer the client to secure the connection. If the client decides to secure
// the connection then a <starttls> stanza should be received
else if (!createSession(xpp.getNamespace(null), serverName, xpp, connection)) {
// No session was created because of an invalid namespace prefix so answer a stream
// error and close the underlying connection
// http://xmpp.org/rfcs/rfc6120.html#streams-error-conditions-invalid-namespace
// "or the content namespace declared as the default namespace is not supported (e.g., something other than "jabber:client" or "jabber:server")."
streamError = new StreamError(StreamError.Condition.invalid_namespace);
// Log a warning so that admins can track this cases from the server side
Log.warn("Closing session due to invalid_namespace in stream header. Prefix: " +
xpp.getNamespace(null) + ". Connection: " + connection);
}

if (streamError != null) {
StringBuilder sb = new StringBuilder(250);
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
@@ -641,18 +633,15 @@ else if (!createSession(xpp.getNamespace(null), serverName, xpp, connection)) {
sb.append("from=\"").append(serverName).append("\" ");
sb.append("id=\"").append(StringUtils.randomString(5)).append("\" ");
sb.append("xmlns=\"").append(xpp.getNamespace(null)).append("\" ");
sb.append("xmlns:stream=\"").append(xpp.getNamespace("stream")).append("\" ");
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
sb.append("version=\"1.0\">");
// Include the bad-namespace-prefix in the response
StreamError error = new StreamError(StreamError.Condition.bad_namespace_prefix);
sb.append(error.toXML());
sb.append(streamError.toXML());
// Deliver stanza
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
// Log a warning so that admins can track this cases from the server side
Log.warn("Closing session due to bad_namespace_prefix in stream header. Prefix: " +
xpp.getNamespace(null) + ". Connection: " + connection);
}

}

private boolean isHostUnknown(String host) {
@@ -671,23 +660,23 @@ private boolean isHostUnknown(String host) {
/**
* Obtain the address of the XMPP entity for which this StanzaHandler
* handles stanzas.
*
*
* Note that the value that is returned for this method can
* change over time. For example, if no session has been established yet,
* this method will return </tt>null</tt>, or, if resource binding occurs,
* the returned value might change. Values obtained from this method are
* therefore best <em>not</em> cached.
*
*
* @return The address of the XMPP entity for.
*/
public JID getAddress() {
if (session == null) {
return null;
}

return session.getAddress();
}

/**
* Returns the stream namespace. (E.g. jabber:client, jabber:server, etc.).
*
@@ -19,6 +19,7 @@

package org.jivesoftware.openfire.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
@@ -252,6 +253,10 @@ public void deliver(Packet packet) throws UnauthorizedException {

boolean errorDelivering = false;
try {
// OF-464: if the connection has been dropped, fail over to backupDeliverer (offline)
if (!ioSession.isConnected()) {
throw new IOException("Connection reset/closed by peer");
}
XMLWriter xmlSerializer =
new XMLWriter(new ByteBufferWriter(buffer, encoder.get()), new OutputFormat());
xmlSerializer.write(packet.getElement());
@@ -263,7 +268,7 @@ public void deliver(Packet packet) throws UnauthorizedException {
ioSession.write(buffer);
}
catch (Exception e) {
Log.debug("NIOConnection: Error delivering packet" + "\n" + this.toString(), e);
Log.debug("Error delivering packet:\n" + packet, e);
errorDelivering = true;
}
if (errorDelivering) {
@@ -298,6 +303,10 @@ private void deliverRawText(String text, boolean asynchronous) {
}
buffer.flip();
if (asynchronous) {
// OF-464: handle dropped connections (no backupDeliverer in this case?)
if (!ioSession.isConnected()) {
throw new IOException("Connection reset/closed by peer");
}
ioSession.write(buffer);
}
else {
@@ -310,7 +319,7 @@ private void deliverRawText(String text, boolean asynchronous) {
}
}
catch (Exception e) {
Log.debug("NIOConnection: Error delivering raw text" + "\n" + this.toString(), e);
Log.debug("Error delivering raw text:\n" + text, e);
errorDelivering = true;
}
// Close the connection if delivering text fails and we are already not closing the connection
@@ -30,6 +30,8 @@
import java.util.regex.Pattern;

import org.apache.mina.common.ByteBuffer;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
@@ -159,6 +161,7 @@ protected void foundMsg(String msg) throws XMLNotWellFormedException {
// Add message to the complete message list
if (msg != null) {
if (hasIllegalCharacterReferences(msg)) {
buffer = null;
throw new XMLNotWellFormedException("Illegal character reference found in: " + msg);
}
msgs.add(msg);
@@ -177,17 +180,29 @@ protected void foundMsg(String msg) throws XMLNotWellFormedException {
* Main reading method
*/
public void read(ByteBuffer byteBuffer) throws Exception {
if (buffer == null) {
// exception was thrown before, avoid duplicate exception(s)
// "read" and discard remaining data
byteBuffer.position(byteBuffer.limit());
return;
}
invalidateBuffer();
// Check that the buffer is not bigger than 1 Megabyte. For security reasons
// we will abort parsing when 1 Mega of queued chars was found.
if (buffer.length() > maxBufferSize) {
throw new Exception("Stopped parsing never ending stanza");
// purge the local buffer / free memory
buffer = null;
// processing the exception takes quite long
final ProtocolDecoderException ex = new ProtocolDecoderException("Stopped parsing never ending stanza");
ex.setHexdump("(redacted hex dump of never ending stanza)");
throw ex;
}
CharBuffer charBuffer = CharBuffer.allocate(byteBuffer.capacity());
encoder.reset();
encoder.decode(byteBuffer.buf(), charBuffer, false);
char[] buf = new char[charBuffer.position()];
charBuffer.flip();charBuffer.get(buf);
charBuffer.flip();
charBuffer.get(buf);
int readChar = buf.length;

// Just return if nothing was read
@@ -205,6 +220,7 @@ public void read(ByteBuffer byteBuffer) throws Exception {
if (ch < 0x20 && ch != 0x9 && ch != 0xA && ch != 0xD && ch != 0x0) {
//Unicode characters in the range 0x0000-0x001F other than 9, A, and D are not allowed in XML
//We need to allow the NULL character, however, for Flash XMLSocket clients to work.
buffer = null;
throw new XMLNotWellFormedException("Character is invalid in: " + ch);
}
if (isHighSurrogate) {
@@ -214,6 +230,7 @@ public void read(ByteBuffer byteBuffer) throws Exception {
}
else {
// Trigger error. Found high surrogate not followed by low surrogate
buffer = null;
throw new Exception("Found high surrogate not followed by low surrogate");
}
}
@@ -222,6 +239,7 @@ else if (Character.isHighSurrogate(ch)) {
}
else if (Character.isLowSurrogate(ch)) {
// Trigger error. Found low surrogate char without a preceding high surrogate
buffer = null;
throw new Exception("Found low surrogate char without a preceding high surrogate");
}
if (status == XMLLightweightParser.TAIL) {
@@ -2016,7 +2016,7 @@ protected void sendEventNotification(JID subscriberJID, Message notification,
headers = notification.addChildElement("headers", "http://jabber.org/protocol/shim");
for (String subID : subIDs) {
Element header = headers.addElement("header");
header.addAttribute("name", "pubsub#subid");
header.addAttribute("name", "SubID");
header.setText(subID);
}
}
@@ -1739,12 +1739,16 @@ private void probePresences(final PubSubService service) {

public void shutdown(PubSubService service) {
PubSubPersistenceManager.shutdown();
// Stop executing ad-hoc commands
service.getManager().stop();

// clear all nodes for this service, to remove circular references back to the service instance.
service.getNodes().clear(); // FIXME: this is an ugly hack. getNodes() is documented to return an unmodifiable collection (but does not).

if (service != null) {

if (service.getManager() != null) {
// Stop executing ad-hoc commands
service.getManager().stop();
}

// clear all nodes for this service, to remove circular references back to the service instance.
service.getNodes().clear(); // FIXME: this is an ugly hack. getNodes() is documented to return an unmodifiable collection (but does not).
}
}

/*******************************************************************************
@@ -335,6 +335,12 @@ public void removeUserAllowedToCreate(String userJID) {
@Override
public void initialize(XMPPServer server) {
super.initialize(server);

JiveGlobals.migrateProperty("xmpp.pubsub.enabled");
JiveGlobals.migrateProperty("xmpp.pubsub.service");
JiveGlobals.migrateProperty("xmpp.pubsub.root.nodeID");
JiveGlobals.migrateProperty("xmpp.pubsub.root.creator");
JiveGlobals.migrateProperty("xmpp.pubsub.multiple-subscriptions");

// Listen to property events so that the template is always up to date
PropertyEventDispatcher.addListener(this);
@@ -72,6 +72,14 @@
"ORDER BY creationDate DESC LIMIT ?) AS noDelete " +
"ON ofPubsubItem.id = noDelete.id WHERE noDelete.id IS NULL AND " +
"ofPubsubItem.serviceID = ? AND nodeID = ?";

private static final String PURGE_FOR_SIZE_POSTGRESQL =
"DELETE from ofPubsubItem where id in " +
"(select ofPubsubItem.id FROM ofPubsubItem LEFT JOIN " +
"(SELECT id FROM ofPubsubItem WHERE serviceID=? AND nodeID=? " +
"ORDER BY creationDate DESC LIMIT ?) AS noDelete " +
"ON ofPubsubItem.id = noDelete.id WHERE noDelete.id IS NULL " +
"AND ofPubsubItem.serviceID = ? AND nodeID = ?)";

private static final String PURGE_FOR_SIZE_HSQLDB = "DELETE FROM ofPubsubItem WHERE serviceID=? AND nodeID=? AND id NOT IN "
+ "(SELECT id FROM ofPubsubItem WHERE serviceID=? AND nodeID=? ORDER BY creationDate DESC LIMIT ?)";
@@ -1917,6 +1925,8 @@ private static String getPurgeStatement(DatabaseType type)
{
switch (type)
{
case postgresql:
return PURGE_FOR_SIZE_POSTGRESQL;
case hsqldb:
return PURGE_FOR_SIZE_HSQLDB;

@@ -75,7 +75,9 @@
private static final String LOAD_ROSTER =
"SELECT jid, rosterID, sub, ask, recv, nick FROM ofRoster WHERE username=?";
private static final String LOAD_ROSTER_ITEM_GROUPS =
"SELECT rosterID,groupName FROM ofRosterGroups";
"SELECT ofRosterGroups.rosterID,groupName FROM ofRosterGroups " +
"INNER JOIN ofRoster ON ofRosterGroups.rosterID = ofRoster.rosterID " +
"WHERE username=? ORDER BY ofRosterGroups.rosterID, rank";

/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#createItem(java.lang.String, org.jivesoftware.openfire.roster.RosterItem)
@@ -267,14 +269,8 @@ public int getItemCount(String username) {

// Load the groups for the loaded contact
if (!itemList.isEmpty()) {
StringBuilder sb = new StringBuilder(100);
sb.append(LOAD_ROSTER_ITEM_GROUPS).append(" WHERE rosterID IN (");
for (RosterItem item : itemList) {
sb.append(item.getID()).append(",");
}
sb.setLength(sb.length()-1);
sb.append(") ORDER BY rosterID, rank");
pstmt = con.prepareStatement(sb.toString());
pstmt = con.prepareStatement(LOAD_ROSTER_ITEM_GROUPS);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
while (rs.next()) {
itemsByID.get(rs.getLong(1)).getGroups().add(rs.getString(2));
@@ -159,6 +159,8 @@ public Roster() {
if (group.isUser(jid)) {
item.addSharedGroup(group);
itemGroups.add(group);
item.setNickname(UserNameManager.getUserName(jid));
broadcast(item, true);
} else {
item.addInvisibleSharedGroup(group);
}
@@ -146,4 +146,19 @@
* @return the new number of conflicts detected on this session.
*/
public int incrementConflictCount();

/**
* Indicates, whether message carbons are enabled.
*
* @return True, if message carbons are enabled.
*/
boolean isMessageCarbonsEnabled();

/**
* Enables or disables <a href="http://xmpp.org/extensions/xep-0280.html">XEP-0280: Message Carbons</a> for this session.
*
* @param enabled True, if message carbons are enabled.
* @see <a href="hhttp://xmpp.org/extensions/xep-0280.html">XEP-0280: Message Carbons</a>
*/
void setMessageCarbonsEnabled(boolean enabled);
}
@@ -74,6 +74,8 @@
private static Map<String,String> allowedIPs = new HashMap<String,String>();
private static Map<String,String> allowedAnonymIPs = new HashMap<String,String>();

private boolean messageCarbonsEnabled;

/**
* The authentication token for this session.
*/
@@ -813,6 +815,16 @@ public int incrementConflictCount() {
return conflictCount;
}

@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}

@Override
public void setMessageCarbonsEnabled(boolean enabled) {
messageCarbonsEnabled = true;
}

/**
* Returns true if the specified packet must not be blocked based on the active or default
* privacy list rules. The active list will be tried first. If none was found then the
@@ -24,6 +24,8 @@
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -270,18 +272,24 @@ private static LocalOutgoingServerSession createOutgoingSession(String domain, S
List<DNSUtil.HostAddress> hosts = DNSUtil.resolveXMPPDomain(hostname, port);
for (Iterator<DNSUtil.HostAddress> it = hosts.iterator(); it.hasNext();) {
try {
socket = new Socket();
DNSUtil.HostAddress address = it.next();
realHostname = address.getHost();
realPort = address.getPort();
Log.debug("LocalOutgoingServerSession: OS - Trying to connect to " + hostname + ":" + port +
"(DNS lookup: " + realHostname + ":" + realPort + ")");
// Establish a TCP connection to the Receiving Server
socket.connect(new InetSocketAddress(realHostname, realPort),
RemoteServerManager.getSocketTimeout());
socket = new Socket();
socket.connect(new InetSocketAddress(realHostname, realPort), RemoteServerManager.getSocketTimeout());
Log.debug("LocalOutgoingServerSession: OS - Plain connection to " + hostname + ":" + port + " successful");
break;
}
catch (UnknownHostException uhe) {
Log.warn("Unknown host: " + realHostname);
}
catch (SocketTimeoutException ste) {
Log.error("Connection Timed out trying to connect to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")");
}
catch (Exception e) {
Log.warn("Error trying to connect to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
@@ -295,7 +303,7 @@ private static LocalOutgoingServerSession createOutgoingSession(String domain, S
}
}
}
if (!socket.isConnected()) {
if (socket == null || !socket.isConnected()) {
return null;
}

@@ -374,32 +382,27 @@ else if (ServerDialback.isEnabled() && features.element("dialback") != null) {
Log.debug("LocalOutgoingServerSession: OS - Error, <starttls> was not received");
}
}
// Something went wrong so close the connection and try server dialback over
// a plain connection
if (connection != null) {
connection.close();
}
}
catch (SSLHandshakeException e) {
Log.debug("LocalOutgoingServerSession: Handshake error while creating secured outgoing session to remote " +
"server: " + hostname + "(DNS lookup: " + realHostname + ":" + realPort +
")", e);
// Close the connection
if (connection != null) {
connection.close();
}
Log.debug("LocalOutgoingServerSession: SSL Handshake error while creating secured outgoing session to remote " +
"server: " + hostname + "(DNS lookup: " + realHostname + ":" + realPort + "): " + e.getMessage());
}
catch (XmlPullParserException e) {
Log.warn("Error creating secured outgoing session to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
// Close the connection
if (connection != null) {
connection.close();
}
"(DNS lookup: " + realHostname + ":" + realPort + "): ", e);
}
catch (UnknownHostException uhe) {
Log.warn("Unknown host: " + realHostname);
}
catch (SocketTimeoutException ste) {
Log.error("Connection Timed out trying to connect to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")");
}
catch (Exception e) {
Log.error("Error creating secured outgoing session to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
Log.error("Error creating secured outgoing session to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
}
finally {
// Close the connection
if (connection != null) {
connection.close();
@@ -446,7 +449,7 @@ private static LocalOutgoingServerSession secureAndAuthenticate(String hostname,
String id = xpp.getAttributeValue("", "id");
// Get new stream features
features = reader.parseDocument().getRootElement();
if (features != null && (features.element("mechanisms") != null || features.element("dialback") != null)) {
if (features != null) {
// Check if we can use stream compression
String policyName = JiveGlobals.getProperty("xmpp.server.compression.policy", Connection.CompressionPolicy.disabled.toString());
Connection.CompressionPolicy compressionPolicy = Connection.CompressionPolicy.valueOf(policyName);
@@ -486,7 +489,7 @@ private static LocalOutgoingServerSession secureAndAuthenticate(String hostname,
}
// Get new stream features
features = reader.parseDocument().getRootElement();
if (features == null || features.element("mechanisms") == null) {
if (features == null) {
log.debug("Error, EXTERNAL SASL was not offered.");
return null;
}
@@ -24,14 +24,15 @@
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.openfire.spi.RoutingTableImpl;
import org.jivesoftware.util.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.*;

/**
* The session represents a connection between the server and a client (c2s) or
@@ -288,6 +289,25 @@ public void process(Packet packet) {
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
} else {
// http://xmpp.org/extensions/xep-0016.html#protocol-error
if (packet instanceof Message) {
// For message stanzas, the server SHOULD return an error, which SHOULD be <service-unavailable/>.
Message message = (Message) packet;
Message result = message.createCopy();
result.setTo(message.getFrom());
result.setError(PacketError.Condition.service_unavailable);
XMPPServer.getInstance().getRoutingTable().routePacket(message.getFrom(), result, true);
} else if (packet instanceof IQ) {
// For IQ stanzas of type "get" or "set", the server MUST return an error, which SHOULD be <service-unavailable/>.
// IQ stanzas of other types MUST be silently dropped by the server.
IQ iq = (IQ) packet;
if (iq.getType() == IQ.Type.get || iq.getType() == IQ.Type.set) {
IQ result = IQ.createResultIQ(iq);
result.setError(PacketError.Condition.service_unavailable);
XMPPServer.getInstance().getRoutingTable().routePacket(iq.getFrom(), result, true);
}
}
}
}

@@ -25,22 +25,14 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.locks.Lock;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.PacketDeliverer;
import org.jivesoftware.openfire.PresenceManager;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.container.BasicModule;
@@ -71,7 +63,7 @@
*
* @author Iain Shigeoka
*/
public class PresenceManagerImpl extends BasicModule implements PresenceManager, UserEventListener {
public class PresenceManagerImpl extends BasicModule implements PresenceManager, UserEventListener, XMPPServerListener {

private static final Logger Log = LoggerFactory.getLogger(PresenceManagerImpl.class);

@@ -273,31 +265,32 @@ public void userUnavailable(Presence presence) {
}
lastActivityCache.put(username, offlinePresenceDate.getTime());

// delete existing offline presence (if any)
deleteOfflinePresenceFromDB(username);

// Insert data into the database.
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_OFFLINE_PRESENCE);
pstmt.setString(1, username);
if (offlinePresence != null) {
DbConnectionManager.setLargeTextField(pstmt, 2, offlinePresence);
}
else {
pstmt.setNull(2, Types.VARCHAR);
}
pstmt.setString(3, StringUtils.dateToMillis(offlinePresenceDate));
pstmt.execute();
}
catch (SQLException sqle) {
Log.error("Error storing offline presence of user: " + username, sqle);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
writeToDatabase(username, offlinePresence, offlinePresenceDate);
}
}

private void writeToDatabase(String username, String offlinePresence, Date offlinePresenceDate) {
// delete existing offline presence (if any)
deleteOfflinePresenceFromDB(username);

// Insert data into the database.
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_OFFLINE_PRESENCE);
pstmt.setString(1, username);
if (offlinePresence != null) {
DbConnectionManager.setLargeTextField(pstmt, 2, offlinePresence);
} else {
pstmt.setNull(2, Types.VARCHAR);
}
pstmt.setString(3, StringUtils.dateToMillis(offlinePresenceDate));
pstmt.execute();
} catch (SQLException sqle) {
Log.error("Error storing offline presence of user: " + username, sqle);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}

@@ -582,4 +575,21 @@ private void loadOfflinePresence(String username) {
lock.unlock();
}
}

@Override
public void serverStarted() {
}

@Override
public void serverStopping() {
for (ClientSession session : XMPPServer.getInstance().getSessionManager().getSessions()) {
if (!session.isAnonymousUser()) {
try {
writeToDatabase(session.getUsername(), null, new Date());
} catch (UserNotFoundException e) {
Log.error(e.getMessage(), e);
}
}
}
}
}
@@ -20,13 +20,17 @@

package org.jivesoftware.openfire.spi;

import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.carbons.Received;
import org.jivesoftware.openfire.cluster.ClusterEventListener;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.cluster.NodeID;
import org.jivesoftware.openfire.component.ExternalComponentManager;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.forward.Forwarded;
import org.jivesoftware.openfire.handler.PresenceUpdateHandler;
import org.jivesoftware.openfire.server.OutgoingSessionPromise;
import org.jivesoftware.openfire.session.*;
@@ -233,7 +237,7 @@ public void routePacket(JID jid, Packet packet, boolean fromServer) throws Packe
// Packet sent to our domain.
routed = routeToLocalDomain(jid, packet, fromServer);
}
else if (jid.getDomain().contains(serverName)) {
else if (jid.getDomain().endsWith(serverName) && hasComponentRoute(jid)) {
// Packet sent to component hosted in this server
routed = routeToComponent(jid, packet, routed);
}
@@ -277,11 +281,16 @@ else if (packet instanceof Presence) {
private boolean routeToLocalDomain(JID jid, Packet packet,
boolean fromServer) {
boolean routed = false;
Element privateElement = packet.getElement().element(QName.get("private", "urn:xmpp:carbons:2"));
boolean isPrivate = privateElement != null;
// The receiving server and SHOULD remove the <private/> element before delivering to the recipient.
packet.getElement().remove(privateElement);

if (jid.getResource() == null) {
// Packet sent to a bare JID of a user
if (packet instanceof Message) {
// Find best route of local user
routed = routeToBareJID(jid, (Message) packet);
routed = routeToBareJID(jid, (Message) packet, isPrivate);
}
else {
throw new PacketException("Cannot route packet of type IQ or Presence to bare JID: " + packet.toXML());
@@ -298,11 +307,40 @@ private boolean routeToLocalDomain(JID jid, Packet packet,
!presenceUpdateHandler.hasDirectPresence(packet.getTo(), packet.getFrom())) {
Log.debug("Unable to route packet. Packet should only be sent to available sessions and the route is not available. {} ", packet.toXML());
routed = false;
}
else {
if (localRoutingTable.isLocalRoute(jid)) {
// This is a route to a local user hosted in this node
try {
} else {
if (localRoutingTable.isLocalRoute(jid)) {
if (packet instanceof Message) {
Message message = (Message) packet;
if (message.getType() == Message.Type.chat && !isPrivate) {
List<JID> routes = getRoutes(jid.asBareJID(), null);
for (JID route : routes) {
// The receiving server MUST NOT send a forwarded copy to the full JID the original <message/> stanza was addressed to, as that recipient receives the original <message/> stanza.
if (!route.equals(jid)) {
ClientSession clientSession = getClientRoute(route);
if (clientSession.isMessageCarbonsEnabled()) {
Message carbon = new Message();
// The wrapping message SHOULD maintain the same 'type' attribute value;
carbon.setType(message.getType());
// the 'from' attribute MUST be the Carbons-enabled user's bare JID
carbon.setFrom(route.asBareJID());
// and the 'to' attribute MUST be the full JID of the resource receiving the copy
carbon.setTo(route);
// The content of the wrapping message MUST contain a <received/> element qualified by the namespace "urn:xmpp:carbons:2", which itself contains a <forwarded/> element qualified by the namespace "urn:xmpp:forward:0" that contains the original <message/>.
carbon.addExtension(new Received(new Forwarded(message)));

try {
localRoutingTable.getRoute(route.toString()).process(carbon);
} catch (UnauthorizedException e) {
Log.error("Unable to route packet " + packet.toXML(), e);
}
}
}
}
}
}

// This is a route to a local user hosted in this node
try {
localRoutingTable.getRoute(jid.toString()).process(packet);
routed = true;
} catch (UnauthorizedException e) {
@@ -333,9 +371,6 @@ private boolean routeToLocalDomain(JID jid, Packet packet,
* the recipient of the packet to route.
* @param packet
* the packet to route.
* @param fromServer
* true if the packet was created by the server. This packets
* should always be delivered
* @throws PacketException
* thrown if the packet is malformed (results in the sender's
* session being shutdown).
@@ -398,9 +433,6 @@ private boolean routeToComponent(JID jid, Packet packet,
* the recipient of the packet to route.
* @param packet
* the packet to route.
* @param fromServer
* true if the packet was created by the server. This packets
* should always be delivered
* @throws PacketException
* thrown if the packet is malformed (results in the sender's
* session being shutdown).
@@ -483,7 +515,7 @@ else if (packet instanceof Message || packet instanceof Presence) {
* @param packet the message to send.
* @return true if at least one target session was found
*/
private boolean routeToBareJID(JID recipientJID, Message packet) {
private boolean routeToBareJID(JID recipientJID, Message packet, boolean isPrivate) {
List<ClientSession> sessions = new ArrayList<ClientSession>();
// Get existing AVAILABLE sessions of this user or AVAILABLE to the sender of the packet
for (JID address : getRoutes(recipientJID, packet.getFrom())) {
@@ -492,7 +524,7 @@ private boolean routeToBareJID(JID recipientJID, Message packet) {
sessions.add(session);
}
}
sessions = getHighestPrioritySessions(sessions);

if (sessions.isEmpty()) {
// No session is available so store offline
Log.debug("Unable to route packet. No session is available so store offline. {} ", packet.toXML());
@@ -503,6 +535,15 @@ else if (sessions.size() == 1) {
sessions.get(0).process(packet);
}
else {

// Check for message carbons enabled sessions and sent message to them.
for (ClientSession session : sessions) {
// Deliver to each session.
if (shouldSentToResource(session, packet, isPrivate)) {
session.process(packet);
}
}

// Many sessions have the highest priority (be smart now) :)
if (!JiveGlobals.getBooleanProperty("route.all-resources", false)) {
// Sort sessions by show value (e.g. away, xa)
@@ -555,19 +596,32 @@ public int compare(ClientSession o1, ClientSession o2) {
return o2.getLastActiveDate().compareTo(o1.getLastActiveDate());
}
});
// Deliver stanza to session with highest priority, highest show value and most recent activity
targets.get(0).process(packet);

// Make sure, we don't send the packet again, if it has already been sent by message carbons.
ClientSession session = targets.get(0);
if (!shouldSentToResource(session, packet, isPrivate)) {
// Deliver stanza to session with highest priority, highest show value and most recent activity
session.process(packet);
}
}
else {
// Deliver stanza to all connected resources with highest priority
sessions = getHighestPrioritySessions(sessions);
for (ClientSession session : sessions) {
session.process(packet);
// Make sure, we don't send the packet again, if it has already been sent by message carbons.
if (!shouldSentToResource(session, packet, isPrivate)) {
session.process(packet);
}
}
}
}
return true;
}

private boolean shouldSentToResource(ClientSession session, Message message, boolean isPrivate) {
return !isPrivate && session.isMessageCarbonsEnabled() && message.getType() == Message.Type.chat;
}

/**
* Returns the sessions that had the highest presence priority that is non-negative.
*
@@ -22,6 +22,7 @@

import org.jivesoftware.openfire.ConnectionManager;
import org.jivesoftware.openfire.ServerPort;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.XMPPServerInfo;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Version;
@@ -54,12 +55,11 @@
* it hasn't been started).
* @param connectionManager the object that keeps track of the active ports.
*/
public XMPPServerInfoImpl(String xmppDomain, String hostname, Version version, Date startDate, ConnectionManager connectionManager) {
public XMPPServerInfoImpl(String xmppDomain, String hostname, Version version, Date startDate) {
this.xmppDomain = xmppDomain;
this.hostname = hostname;
this.ver = version;
this.startDate = startDate;
this.connectionManager = connectionManager;
}

public Version getVersion() {
@@ -103,10 +103,10 @@ public Date getLastStarted() {

public Collection<ServerPort> getServerPorts() {
if (connectionManager == null) {
return Collections.emptyList();
}
else {
return connectionManager.getPorts();
connectionManager = XMPPServer.getInstance().getConnectionManager();
}
return (Collection<ServerPort>) (connectionManager == null ?
Collections.emptyList() :
connectionManager.getPorts());
}
}
@@ -146,7 +146,7 @@ public void run() {
catch (Exception e) {
Log.error("Error checking for updates", e);
}
// Keep track of the last time we checked for updates.
// Keep track of the last time we checked for updates.
long now = System.currentTimeMillis();
JiveGlobals.setProperty("update.lastCheck", String.valueOf(now));
// As an extra precaution, make sure that that the value
@@ -198,6 +198,9 @@ public void initialize(XMPPServer server) {
super.initialize(server);
router = server.getMessageRouter();
serverName = server.getServerInfo().getXMPPDomain();

JiveGlobals.migrateProperty("update.service-enabled");
JiveGlobals.migrateProperty("update.notify-admins");
}

/**
@@ -13,436 +13,263 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*
* HybridUserProvider.java
*
* Created on 16. April 2007, 21:48
* by Marc Seeger
* code works fine as far as my 10 User Test-Server goes
* It basically checks different userproviders which are being set in the configuration xml file
* I use it in combination with hybridauth providers to be able to get the usual users from ldap but still have some Bots in MySQL
*
* Changed on 14. Nov. 2007, 10:48
* by Chris Neasbitt
* -changed getUsers(int startIndex, int numResults) method to return a subset of the total users from all providers
* -changed the getUsers() method to use a vector internally since addAll is an optional method of the collection
* interface we cannot assume that all classes that support the collection interface also support the addAll method
* -changed the getUserCount() method to iterate through an array of providers while calling a private helper method
* getUserCount(UserProvider provider) on each of them.
*/

package org.jivesoftware.openfire.user;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Delegate UserProvider operations among up to three configurable provider implementation classes.
*
* @author Marc Seeger
* @author Chris Neasbitt
* @author Tom Evans
*/

public class HybridUserProvider implements UserProvider {

private static final Logger Log = LoggerFactory.getLogger(HybridUserProvider.class);

private UserProvider primaryProvider = null;
private UserProvider secondaryProvider = null;
private UserProvider tertiaryProvider = null;
private UserProvider[] userproviders = {primaryProvider, secondaryProvider, tertiaryProvider};
private List<UserProvider> userproviders = null;

private Set<String> primaryOverrides = new HashSet<String>();
private Set<String> secondaryOverrides = new HashSet<String>();
private Set<String> tertiaryOverrides = new HashSet<String>();
public HybridUserProvider() {

// Migrate user provider properties
JiveGlobals.migrateProperty("hybridUserProvider.primaryProvider.className");
JiveGlobals.migrateProperty("hybridUserProvider.secondaryProvider.className");
JiveGlobals.migrateProperty("hybridUserProvider.tertiaryProvider.className");

public HybridUserProvider() {
// Load primary, secondary, and tertiary user providers.
String primaryClass = JiveGlobals.getXMLProperty("hybridUserProvider.primaryProvider.className");
userproviders = new ArrayList<UserProvider>();

// Load primary, secondary, and tertiary user providers.
String primaryClass = JiveGlobals.getProperty("hybridUserProvider.primaryProvider.className");
if (primaryClass == null) {
Log.error("A primary UserProvider must be specified in the openfire.xml.");
Log.error("A primary UserProvider must be specified via openfire.xml or the system properties");
return;
}
try {
Class c = ClassUtils.forName(primaryClass);
primaryProvider = (UserProvider) c.newInstance();
UserProvider primaryProvider = (UserProvider) c.newInstance();
userproviders.add(primaryProvider);
Log.debug("Primary user provider: " + primaryClass);
} catch (Exception e) {
Log.error("Unable to load primary user provider: " + primaryClass +
". Users in this provider will be disabled.", e);
return;
}
String secondaryClass = JiveGlobals.getXMLProperty("hybridUserProvider.secondaryProvider.className");
String secondaryClass = JiveGlobals.getProperty("hybridUserProvider.secondaryProvider.className");
if (secondaryClass != null) {
try {
Class c = ClassUtils.forName(secondaryClass);
secondaryProvider = (UserProvider) c.newInstance();
UserProvider secondaryProvider = (UserProvider) c.newInstance();
userproviders.add(secondaryProvider);
Log.debug("Secondary user provider: " + secondaryClass);
} catch (Exception e) {
Log.error("Unable to load secondary user provider: " + secondaryClass, e);
}
}
String tertiaryClass = JiveGlobals.getXMLProperty("hybridUserProvider.tertiaryProvider.className");
String tertiaryClass = JiveGlobals.getProperty("hybridUserProvider.tertiaryProvider.className");
if (tertiaryClass != null) {
try {
Class c = ClassUtils.forName(tertiaryClass);
tertiaryProvider = (UserProvider) c.newInstance();
UserProvider tertiaryProvider = (UserProvider) c.newInstance();
userproviders.add(tertiaryProvider);
Log.debug("Tertiary user provider: " + tertiaryClass);
} catch (Exception e) {
Log.error("Unable to load tertiary user provider: " + tertiaryClass, e);
}
}

// Now, load any overrides.
String overrideList = JiveGlobals.getXMLProperty(
"hybridUserProvider.primaryProvider.overrideList", "");
for (String user : overrideList.split(",")) {
primaryOverrides.add(user.trim().toLowerCase());
}

if (secondaryProvider != null) {
overrideList = JiveGlobals.getXMLProperty(
"hybridUserProvider.secondaryProvider.overrideList", "");
for (String user : overrideList.split(",")) {
secondaryOverrides.add(user.trim().toLowerCase());
}
}

if (tertiaryProvider != null) {
overrideList = JiveGlobals.getXMLProperty(
"hybridUserProvider.tertiaryProvider.overrideList", "");
for (String user : overrideList.split(",")) {
tertiaryOverrides.add(user.trim().toLowerCase());
}
}

}


public User createUser(String username, String password, String name, String email) throws UserAlreadyExistsException {
//initialize our returnvalue
User returnvalue = null;

//try to use the providers to create a user and change the return value to that user
if (!primaryProvider.isReadOnly()) {
try {
returnvalue = primaryProvider.createUser(username, password, name, email);
}

finally {
}

} else if (secondaryProvider != null) {
if (!secondaryProvider.isReadOnly()) {
try {
returnvalue = secondaryProvider.createUser(username, password, name, email);
}

finally {
}
User returnvalue = null;

}
} else if (tertiaryProvider != null) {
if (!tertiaryProvider.isReadOnly()) {
try {
returnvalue = tertiaryProvider.createUser(username, password, name, email);
}

finally {
}
}
// create the user (first writable provider wins)
for (UserProvider provider : userproviders) {
if (provider.isReadOnly()) {
continue;
}
returnvalue = provider.createUser(username, password, name, email);
if (returnvalue != null) {
break;
}
}

//return our created user
if (returnvalue != null) {
return returnvalue;
} else {
throw new UnsupportedOperationException();
if (returnvalue == null) {
throw new UnsupportedOperationException();
}
return returnvalue;
}


public void deleteUser(String username) {
if (!primaryProvider.isReadOnly()) {
try {
primaryProvider.deleteUser(username);
return;
}

finally {
}

} else if (secondaryProvider != null) {
if (!secondaryProvider.isReadOnly()) {
try {
secondaryProvider.deleteUser(username);
return;
}
boolean isDeleted = false;

finally {
}

}
} else if (tertiaryProvider != null) {
if (!tertiaryProvider.isReadOnly()) {
try {
tertiaryProvider.deleteUser(username);
return;
}

finally {
}

} else {
// Reject the operation since all of the providers seem to be read-only
throw new UnsupportedOperationException();
}
for (UserProvider provider : userproviders) {
if (provider.isReadOnly()) {
continue;
}
provider.deleteUser(username);
isDeleted = true;
}

}
// all providers are read-only
if (!isDeleted) {
throw new UnsupportedOperationException();
}
}


public Collection<User> findUsers(Set<String> fields, String query) throws UnsupportedOperationException {


Collection<User> returnvalue = null;
try {
returnvalue = primaryProvider.findUsers(fields, query);
}

finally {
}

if (secondaryProvider != null) {
try {

returnvalue = secondaryProvider.findUsers(fields, query);
}

finally {
}
}
if (tertiaryProvider != null) {
try {
returnvalue = tertiaryProvider.findUsers(fields, query);
}

finally {
}
}

//return our collection of users
if (returnvalue != null) {
return returnvalue;
} else {
throw new UnsupportedOperationException();
}
List<User> userList = new ArrayList<User>();
boolean isUnsupported = false;

for (UserProvider provider : userproviders) {

// validate search fields for each provider
Set<String> validFields = provider.getSearchFields();
for (String field : fields) {
if (!validFields.contains(field)) {
continue;
}
}

try {
userList.addAll(provider.findUsers(fields, query));
} catch (UnsupportedOperationException uoe) {
Log.warn("UserProvider.findUsers is not supported by this UserProvider: " + provider.getClass().getName());
isUnsupported = true;
}
}

if (isUnsupported && userList.size() == 0) {
throw new UnsupportedOperationException();
}
return userList;
}


public Collection<User> findUsers(Set<String> fields, String query, int startIndex, int numResults) throws UnsupportedOperationException {
Collection<User> returnvalue = null;
try {
returnvalue = primaryProvider.findUsers(fields, query, startIndex, numResults);
}

finally {
}

if (secondaryProvider != null) {
try {

returnvalue = secondaryProvider.findUsers(fields, query, startIndex, numResults);
}

finally {
}
}
if (tertiaryProvider != null) {
try {
returnvalue = tertiaryProvider.findUsers(fields, query, startIndex, numResults);
}

finally {
}
}

//return our Collection of Users
if (returnvalue != null) {
return returnvalue;
} else {
throw new UnsupportedOperationException();
}
List<User> userList = new ArrayList<User>();
boolean isUnsupported = false;
int totalMatchedUserCount = 0;

for (UserProvider provider : userproviders) {

// validate search fields for each provider
Set<String> validFields = provider.getSearchFields();
for (String field : fields) {
if (!validFields.contains(field)) {
continue;
}
}

try {
Collection<User> providerResults = provider.findUsers(fields, query);
totalMatchedUserCount += providerResults.size();
if (startIndex >= totalMatchedUserCount) {
continue;
}
int providerStartIndex = Math.max(0, startIndex - totalMatchedUserCount);
int providerResultMax = numResults - userList.size();
List<User> providerList = providerResults instanceof List<?> ?
(List<User>) providerResults : new ArrayList<User>(providerResults);
userList.addAll(providerList.subList(providerStartIndex, providerResultMax));
if (userList.size() >= numResults) {
break;
}
} catch (UnsupportedOperationException uoe) {
Log.warn("UserProvider.findUsers is not supported by this UserProvider: " + provider.getClass().getName());
isUnsupported = true;
}
}

if (isUnsupported && userList.size() == 0) {
throw new UnsupportedOperationException();
}
return userList;
}


public Set<String> getSearchFields() throws UnsupportedOperationException {
Set<String> returnvalue = null;
try {
returnvalue = primaryProvider.getSearchFields();
}

finally {
}

if (secondaryProvider != null) {
try {

returnvalue = secondaryProvider.getSearchFields();
}
Set<String> returnvalue = new HashSet<String>();

finally {
}
}
if (tertiaryProvider != null) {
try {
returnvalue = tertiaryProvider.getSearchFields();
}

finally {
}
for (UserProvider provider : userproviders) {
returnvalue.addAll(provider.getSearchFields());
}

//return our Set of Strings
if (returnvalue != null) {
return returnvalue;
} else {
// no search fields were returned
if (returnvalue.size() == 0) {
throw new UnsupportedOperationException();
}
return returnvalue;
}


public int getUserCount() {
int count = 0;
for (UserProvider provider : userproviders) {
count = count + this.getUserCount(provider);
count += provider.getUserCount();
}
return count;
}

private int getUserCount(UserProvider provider) {
int returnvalue = 0;
if (provider != null) {
try {

returnvalue = returnvalue + provider.getUserCount();
}

finally {
}
}
return returnvalue;
}


public Collection<String> getUsernames() {
Collection<String> returnvalue = null;
try {
returnvalue = primaryProvider.getUsernames();
}

finally {
}

if (secondaryProvider != null) {
try {
returnvalue.addAll(secondaryProvider.getUsernames());
}
List<String> returnvalue = new ArrayList<String>();

finally {
}
}
if (tertiaryProvider != null) {
try {
returnvalue.addAll(tertiaryProvider.getUsernames());
}

finally {
}
}

//return our Set of Strings
if (returnvalue != null) {
return returnvalue;
} else {
throw new UnsupportedOperationException();
for (UserProvider provider : userproviders){
returnvalue.addAll(provider.getUsernames());
}
return returnvalue;
}


public Collection<User> getUsers() {
Vector<User> returnvalue = null;
try {
returnvalue = new Vector<User>(primaryProvider.getUsers());
}

finally {
}

if (secondaryProvider != null) {
try {
returnvalue.addAll(secondaryProvider.getUsers());
}

finally {
}
}
if (tertiaryProvider != null) {
try {
returnvalue.addAll(tertiaryProvider.getUsers());
}
List<User> returnvalue = new ArrayList<User>();

finally {
}
for (UserProvider provider : userproviders){
returnvalue.addAll(provider.getUsers());
}

//return our Set of Strings
if (returnvalue != null) {
return returnvalue;
} else {
throw new UnsupportedOperationException();
}
return returnvalue;
}

/*
*Changed by Chris Neasbitt to more accurately represent the intent of the method
*
*This method now removes a sub set of the combined users from all providers. This
*is done in places as to avoid copying collections of users in memory.
*/

public Collection<User> getUsers(int startIndex, int numResults) {
Vector<User> returnresult = new Vector<User>();
int numResultsLeft = numResults;
int currentStartIndex = startIndex;
for (UserProvider provider : userproviders) {
if (numResultsLeft == 0) {
break;
}

int pusercount = this.getUserCount(provider);

if (pusercount == 0 || currentStartIndex >= pusercount) {

currentStartIndex = currentStartIndex - pusercount;
continue;

} else {

Collection<User> subresult = provider.getUsers(currentStartIndex, numResultsLeft);
currentStartIndex = currentStartIndex - subresult.size();
numResultsLeft = numResultsLeft - subresult.size();
returnresult.addAll(subresult);
}
}

return returnresult;
List<User> userList = new ArrayList<User>();
int totalUserCount = 0;

for (UserProvider provider : userproviders) {
int providerStartIndex = Math.max((startIndex - totalUserCount), 0);
totalUserCount += provider.getUserCount();
if (startIndex >= totalUserCount) {
continue;
}
int providerResultMax = numResults - userList.size();
userList.addAll(provider.getUsers(providerStartIndex, providerResultMax));
if (userList.size() >= numResults) {
break;
}
}
return userList;
}

public boolean isReadOnly() {
@@ -458,163 +285,128 @@ public boolean isEmailRequired() {
}

public User loadUser(String username) throws UserNotFoundException {
try {
return primaryProvider.loadUser(username);

}

catch (Exception e) {
}

if (secondaryProvider != null) {
try {

return secondaryProvider.loadUser(username);
}

catch (Exception e) {
}
}


if (tertiaryProvider != null) {
try {
return tertiaryProvider.loadUser(username);
}

catch (Exception e) {
}
}

//if we get this far, no provider seems to successfully have loaded the user
for (UserProvider provider : userproviders) {
try {
return provider.loadUser(username);
}
catch (UserNotFoundException unfe) {
if (Log.isDebugEnabled()) {
Log.debug("User " + username + " not found by UserProvider " + provider.getClass().getName());
}
}
}
//if we get this far, no provider was able to load the user
throw new UserNotFoundException();
}

public void setCreationDate(String username, Date creationDate) throws UserNotFoundException {
if (primaryProvider != null)
try {
primaryProvider.setCreationDate(username, creationDate);
return;
} catch (Exception e) {
}

if (secondaryProvider != null) {
try {
secondaryProvider.setCreationDate(username, creationDate);
return;
}

catch (Exception e) {
}
}
if (tertiaryProvider != null) {
try {
tertiaryProvider.setCreationDate(username, creationDate);
return;
}

catch (Exception e) {
throw new UserNotFoundException();
}
}


boolean isUnsupported = false;

for (UserProvider provider : userproviders) {
try {
provider.setCreationDate(username, creationDate);
return;
}
catch (UnsupportedOperationException uoe) {
Log.warn("UserProvider.setCreationDate is not supported by this UserProvider: " + provider.getClass().getName());
isUnsupported = true;
}
catch (UserNotFoundException unfe) {
if (Log.isDebugEnabled()) {
Log.debug("User " + username + " not found by UserProvider " + provider.getClass().getName());
}
}
}
if (isUnsupported) {
throw new UnsupportedOperationException();
}
else {
throw new UserNotFoundException();
}
}

public void setEmail(String username, String email) throws UserNotFoundException {
if (primaryProvider != null)
try {
primaryProvider.setEmail(username, email);
return;
} catch (Exception e) {
}

if (secondaryProvider != null) {
try {
secondaryProvider.setEmail(username, email);
return;
}

catch (Exception e) {
}
}
if (tertiaryProvider != null) {
try {
tertiaryProvider.setEmail(username, email);
return;
}

catch (Exception e) {
throw new UserNotFoundException();
}
}

boolean isUnsupported = false;

for (UserProvider provider : userproviders) {
try {
provider.setEmail(username, email);
return;
}
catch (UnsupportedOperationException uoe) {
Log.warn("UserProvider.setEmail is not supported by this UserProvider: " + provider.getClass().getName());
isUnsupported = true;
}
catch (UserNotFoundException unfe) {
if (Log.isDebugEnabled()) {
Log.debug("User " + username + " not found by UserProvider " + provider.getClass().getName());
}
}
}
if (isUnsupported) {
throw new UnsupportedOperationException();
}
else {
throw new UserNotFoundException();
}
}


public void setModificationDate(String username, Date modificationDate) throws UserNotFoundException {

//without it eclipse goes apeshit
if (primaryProvider != null)
//apeshit I say!


try {
primaryProvider.setModificationDate(username, modificationDate);
return;
} catch (Exception e) {
}

if (secondaryProvider != null) {
try {
secondaryProvider.setModificationDate(username, modificationDate);
return;
}

catch (Exception e) {
}
}
if (tertiaryProvider != null) {
try {
tertiaryProvider.setModificationDate(username, modificationDate);
return;
}

catch (Exception e) {
throw new UserNotFoundException();
}
}


boolean isUnsupported = false;

for (UserProvider provider : userproviders) {
try {
provider.setModificationDate(username, modificationDate);
return;
}
catch (UnsupportedOperationException uoe) {
Log.warn("UserProvider.setModificationDate is not supported by this UserProvider: " + provider.getClass().getName());
isUnsupported = true;
}
catch (UserNotFoundException unfe) {
if (Log.isDebugEnabled()) {
Log.debug("User " + username + " not found by UserProvider " + provider.getClass().getName());
}
}
}
if (isUnsupported) {
throw new UnsupportedOperationException();
}
else {
throw new UserNotFoundException();
}
}

public void setName(String username, String name) throws UserNotFoundException {
if (primaryProvider != null)
try {
primaryProvider.setName(username, name);
return;
} catch (Exception e) {
}

if (secondaryProvider != null) {
try {
secondaryProvider.setName(username, name);
return;
}

catch (Exception e) {
}
}
if (tertiaryProvider != null) {
try {
tertiaryProvider.setName(username, name);
return;
}

catch (Exception e) {
throw new UserNotFoundException();
}
}
boolean isUnsupported = false;

for (UserProvider provider : userproviders) {
try {
provider.setName(username, name);
return;
}
catch (UnsupportedOperationException uoe) {
Log.warn("UserProvider.setName is not supported by this UserProvider: " + provider.getClass().getName());
isUnsupported = true;
}
catch (UserNotFoundException unfe) {
if (Log.isDebugEnabled()) {
Log.debug("User " + username + " not found by UserProvider " + provider.getClass().getName());
}
}
}
if (isUnsupported) {
throw new UnsupportedOperationException();
}
else {
throw new UserNotFoundException();
}
}
}

@@ -40,6 +40,8 @@
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.ConnectionException;
import org.jivesoftware.openfire.auth.InternalUnauthenticatedException;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.util.StringUtils;
@@ -181,17 +183,21 @@ public void setPassword(String password) throws UnsupportedOperationException {
}

try {
AuthFactory.getAuthProvider().setPassword(username, password);
AuthFactory.setPassword(username, password);

// Fire event.
Map<String,Object> params = new HashMap<String,Object>();
params.put("type", "passwordModified");
UserEventDispatcher.dispatchEvent(this, UserEventDispatcher.EventType.user_modified,
params);
}
catch (UserNotFoundException unfe) {
Log.error(unfe.getMessage(), unfe);
}
catch (UserNotFoundException e) {
Log.error(e.getMessage(), e);
} catch (ConnectionException e) {
Log.error(e.getMessage(), e);
} catch (InternalUnauthenticatedException e) {
Log.error(e.getMessage(), e);
}
}

public String getName() {
@@ -0,0 +1,191 @@
package org.jivesoftware.util;

import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.Security;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Utility class providing symmetric AES encryption/decryption. To strengthen
* the encrypted result, use the {@link #setKey} method to provide a custom
* key prior to invoking the {@link #encrypt} or {@link #decrypt} methods.
*
* @author Tom Evans
*/
public class AesEncryptor implements Encryptor {

private static final Logger log = LoggerFactory.getLogger(AesEncryptor.class);
private static final String ALGORITHM = "AES/CBC/PKCS7Padding";

private static final byte[] INIT_PARM =
{
(byte)0xcd, (byte)0x91, (byte)0xa7, (byte)0xc5,
(byte)0x27, (byte)0x8b, (byte)0x39, (byte)0xe0,
(byte)0xfa, (byte)0x72, (byte)0xd0, (byte)0x29,
(byte)0x83, (byte)0x65, (byte)0x9d, (byte)0x74
};

private static final byte[] DEFAULT_KEY =
{
(byte)0xf2, (byte)0x46, (byte)0x5d, (byte)0x2a,
(byte)0xd1, (byte)0x73, (byte)0x0b, (byte)0x18,
(byte)0xcb, (byte)0x86, (byte)0x95, (byte)0xa3,
(byte)0xb1, (byte)0xe5, (byte)0x89, (byte)0x27
};

private static boolean isInitialized = false;

private byte[] cipherKey = null;

/** Default constructor */
public AesEncryptor() { initialize(); }

/** Custom key constructor */
public AesEncryptor(String key) {
initialize();
setKey(key);
}

/* (non-Javadoc)
* @see org.jivesoftware.util.Encryptor#encrypt(java.lang.String)
*/
@Override
public String encrypt(String value)
{
if (value == null) { return null; }
byte [] bytes = null;
try { bytes = value.getBytes("UTF-8"); }
catch (UnsupportedEncodingException uee) { bytes = value.getBytes(); }
return Base64.encodeBytes( cipher(bytes, getKey(), Cipher.ENCRYPT_MODE) );
}

/* (non-Javadoc)
* @see org.jivesoftware.util.Encryptor#decrypt(java.lang.String)
*/
@Override
public String decrypt(String value)
{
if (value == null) { return null; }
byte [] bytes = cipher(Base64.decode(value), getKey(), Cipher.DECRYPT_MODE);
if (bytes == null) { return null; }
String result = null;
try { result = new String(bytes,"UTF-8"); }
catch (UnsupportedEncodingException uee) { result = new String(bytes); }
return result;
}

/**
* Symmetric encrypt/decrypt routine.
*
* @param attribute The value to be converted
* @param key The encryption key
* @param mode The cipher mode (encrypt or decrypt)
* @return The converted attribute, or null if conversion fails
*/
private byte [] cipher(byte [] attribute, byte [] key, int mode)
{
byte [] result = null;
try
{
// Create AES encryption key
Key aesKey = new SecretKeySpec(key, "AES");

// Create AES Cipher
Cipher aesCipher = Cipher.getInstance(ALGORITHM);

// Initialize AES Cipher and convert
aesCipher.init(mode, aesKey, new IvParameterSpec(INIT_PARM));
result = aesCipher.doFinal(attribute);
}
catch (Exception e)
{
log.error("AES cipher failed", e);
}
return result;
}

/**
* Return the encryption key. This will return the user-defined
* key (if available) or a default encryption key.
*
* @return The encryption key
*/
private byte [] getKey()
{
return cipherKey == null ? DEFAULT_KEY : cipherKey;
}

/**
* Set the encryption key. This will apply the user-defined key,
* truncated or filled (via the default key) as needed to meet
* the key length specifications.
*
* @param key The encryption key
*/
private void setKey(byte [] key)
{
cipherKey = editKey(key);
}

/* (non-Javadoc)
* @see org.jivesoftware.util.Encryptor#setKey(java.lang.String)
*/
@Override
public void setKey(String key)
{
if (key == null) {
cipherKey = null;
return;
}
byte [] bytes = null;
try { bytes = key.getBytes("UTF-8"); }
catch (UnsupportedEncodingException uee) { bytes = key.getBytes(); }
setKey(editKey(bytes));
}

/**
* Validates an optional user-defined encryption key. Only the
* first sixteen bytes of the input array will be used for the key.
* It will be filled (if necessary) to a minimum length of sixteen.
*
* @param key The user-defined encryption key
* @return A valid encryption key, or null
*/
private byte [] editKey(byte [] key)
{
if (key == null) { return null; }
byte [] result = new byte [DEFAULT_KEY.length];
for (int x=0; x<DEFAULT_KEY.length; x++)
{
result[x] = x < key.length ? key[x] : DEFAULT_KEY[x];
}
return result;
}

/** Installs the required security provider(s) */
private synchronized void initialize()
{
if (!isInitialized)
{
try
{
Security.addProvider(new BouncyCastleProvider());
isInitialized = true;
}
catch (Throwable t)
{
log.warn("JCE provider failure; unable to load BC", t);
}
}
}

/* */

}
@@ -26,12 +26,20 @@
* @author Markus Hahn <markus_hahn@gmx.net>
* @author Gaston Dombiak
*/
public class Blowfish {
public class Blowfish implements Encryptor {

private static final Logger Log = LoggerFactory.getLogger(Blowfish.class);

private BlowfishCBC m_bfish;
private static Random m_rndGen = new Random();
private static final String DEFAULT_KEY = "Blowfish-CBC";

/**
* Creates a new Blowfish object using the default key
*/
public Blowfish() {
setKey(DEFAULT_KEY);
}

/**
* Creates a new Blowfish object using the specified key (oversized
@@ -40,19 +48,7 @@
* @param password the password (treated as a real unicode array)
*/
public Blowfish(String password) {
// hash down the password to a 160bit key
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA1");
digest.update(password.getBytes());
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}

// setup the encryptor (use a dummy IV)
m_bfish = new BlowfishCBC(digest.digest(), 0);
digest.reset();
setKey(password);
}

/**
@@ -64,6 +60,7 @@ public Blowfish(String password) {
* @return encrypted string in binhex format
*/
public String encryptString(String sPlainText) {
if (sPlainText == null) { return null; }
// get the IV
long lCBCIV;
synchronized (m_rndGen)
@@ -1484,5 +1481,36 @@ private static String byteArrayToUNCString(byte[] data,

return sbuf.toString();
}

// Encryptor interface

@Override
public String encrypt(String value) {
return this.encryptString(value);
}

@Override
public String decrypt(String value) {
return this.decryptString(value);
}

@Override
public void setKey(String key) {

String password = key == null ? DEFAULT_KEY : key;
// hash down the password to a 160bit key
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA1");
digest.update(password.getBytes());
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}

// setup the encryptor (use a dummy IV)
m_bfish = new BlowfishCBC(digest.digest(), 0);
digest.reset();
}
}

@@ -71,8 +71,13 @@
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -576,14 +581,22 @@ public static boolean installCert(KeyStore keyStore, KeyStore trustStore, String
Log.warn("Certificate already exists for alias: " + alias);
return false;
}
// Retrieve the private key of the stored certificate
PasswordFinder passwordFinder = new PasswordFinder() {
public char[] getPassword() {
return passPhrase != null ? passPhrase.toCharArray() : new char[] {};
}
};
PEMReader pemReader = new PEMReader(new InputStreamReader(pkInputStream), passwordFinder);
KeyPair kp = (KeyPair) pemReader.readObject();

PEMParser pemParser = new PEMParser(new InputStreamReader(pkInputStream));
Object object = pemParser.readObject();
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(passPhrase.toCharArray());
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");

KeyPair kp;

if (object instanceof PEMEncryptedKeyPair) {
Log.debug("Encrypted key - we will use provided password");
kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv));
} else {
Log.debug("Unencrypted key - no password needed");
kp = converter.getKeyPair((PEMKeyPair) object);
}

PrivateKey privKey = kp.getPrivate();

// Load certificates found in the PEM input stream
@@ -0,0 +1,30 @@
package org.jivesoftware.util;

public interface Encryptor {

/**
* Encrypt a clear text String.
*
* @param value The clear text attribute
* @return The encrypted attribute, or null
*/
public abstract String encrypt(String value);

/**
* Decrypt an encrypted String.
*
* @param value The encrypted attribute in Base64 encoding
* @return The clear text attribute, or null
*/
public abstract String decrypt(String value);

/**
* Set the encryption key. This will apply the user-defined key,
* truncated or filled (via the default key) as needed to meet
* the key length specifications.
*
* @param key The encryption key
*/
public abstract void setKey(String key);

}
@@ -30,10 +30,12 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.TimerTask;

import org.jivesoftware.database.DbConnectionManager;
import org.slf4j.Logger;
@@ -51,13 +53,23 @@
* setting the home directory and path to the configuration file.<p>
*
* XML property names must be in the form <code>prop.name</code> - parts of the name must
* be seperated by ".". The value can be any valid String, including strings with line breaks.
* be separated by ".". The value can be any valid String, including strings with line breaks.
*/
public class JiveGlobals {

private static final Logger Log = LoggerFactory.getLogger(JiveGlobals.class);

private static String JIVE_CONFIG_FILENAME = "conf" + File.separator + "openfire.xml";

private static final String JIVE_SECURITY_FILENAME = "conf" + File.separator + "security.xml";
private static final String ENCRYPTED_PROPERTY_NAME_PREFIX = "encrypt.";
private static final String ENCRYPTED_PROPERTY_NAMES = ENCRYPTED_PROPERTY_NAME_PREFIX + "property.name";
private static final String ENCRYPTION_ALGORITHM = ENCRYPTED_PROPERTY_NAME_PREFIX + "algorithm";
private static final String ENCRYPTION_KEY_CURRENT = ENCRYPTED_PROPERTY_NAME_PREFIX + "key.current";
private static final String ENCRYPTION_KEY_NEW = ENCRYPTED_PROPERTY_NAME_PREFIX + "key.new";
private static final String ENCRYPTION_KEY_OLD = ENCRYPTED_PROPERTY_NAME_PREFIX + "key.old";
private static final String ENCRYPTION_ALGORITHM_AES = "AES";
private static final String ENCRYPTION_ALGORITHM_BLOWFISH = "Blowfish";

/**
* Location of the jiveHome directory. All configuration files should be
@@ -67,27 +79,31 @@

public static boolean failedLoading = false;

private static XMLProperties xmlProperties = null;
private static XMLProperties openfireProperties = null;
private static XMLProperties securityProperties = null;
private static JiveProperties properties = null;

private static Locale locale = null;
private static TimeZone timeZone = null;
private static DateFormat dateFormat = null;
private static DateFormat dateTimeFormat = null;
private static DateFormat timeFormat = null;

private static Encryptor propertyEncryptor = null;
private static String currentKey = null;

/**
* Returns the global Locale used by Jive. A locale specifies language
* and country codes, and is used for internationalization. The default
* locale is system dependant - Locale.getDefault().
* locale is system dependent - Locale.getDefault().
*
* @return the global locale used by Jive.
*/
public static Locale getLocale() {
if (locale == null) {
if (xmlProperties != null) {
if (openfireProperties != null) {
String [] localeArray;
String localeProperty = xmlProperties.getProperty("locale");
String localeProperty = openfireProperties.getProperty("locale");
if (localeProperty != null) {
localeArray = localeProperty.split("_");
}
@@ -251,8 +267,8 @@ public static String formatDateTime(Date date) {
* @return the location of the home dir.
*/
public static String getHomeDirectory() {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}
return home;
}
@@ -297,16 +313,16 @@ else if (!mh.canRead() || !mh.canWrite()) {
* @return the property value specified by name.
*/
public static String getXMLProperty(String name) {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}

// home not loaded?
if (xmlProperties == null) {
if (openfireProperties == null) {
return null;
}

return xmlProperties.getProperty(name);
return openfireProperties.getProperty(name);
}

/**
@@ -329,16 +345,16 @@ public static String getXMLProperty(String name) {
* @return the property value specified by name.
*/
public static String getXMLProperty(String name, String defaultValue) {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}

// home not loaded?
if (xmlProperties == null) {
return null;
if (openfireProperties == null) {
return defaultValue;
}

String value = xmlProperties.getProperty(name);
String value = openfireProperties.getProperty(name);
if (value == null) {
value = defaultValue;
}
@@ -426,13 +442,13 @@ public static boolean getXMLProperty(String name, boolean defaultValue) {
* @param value the value of the property being set.
*/
public static void setXMLProperty(String name, String value) {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}

// jiveHome not loaded?
if (xmlProperties != null) {
xmlProperties.setProperty(name, value);
if (openfireProperties != null) {
openfireProperties.setProperty(name, value);
}
}

@@ -453,12 +469,12 @@ public static void setXMLProperty(String name, String value) {
* @param propertyMap a map of properties, keyed on property name.
*/
public static void setXMLProperties(Map<String, String> propertyMap) {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}

if (xmlProperties != null) {
xmlProperties.setProperties(propertyMap);
if (openfireProperties != null) {
openfireProperties.setProperties(propertyMap);
}
}

@@ -485,16 +501,16 @@ public static void setXMLProperties(Map<String, String> propertyMap) {
* @return all child property values for the given parent.
*/
public static List getXMLProperties(String parent) {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}

// jiveHome not loaded?
if (xmlProperties == null) {
if (openfireProperties == null) {
return Collections.EMPTY_LIST;
}

String[] propNames = xmlProperties.getChildrenProperties(parent);
String[] propNames = openfireProperties.getChildrenProperties(parent);
List<String> values = new ArrayList<String>();
for (String propName : propNames) {
String value = getXMLProperty(parent + "." + propName);
@@ -513,10 +529,10 @@ public static List getXMLProperties(String parent) {
* @param name the name of the property to delete.
*/
public static void deleteXMLProperty(String name) {
if (xmlProperties == null) {
loadSetupProperties();
if (openfireProperties == null) {
loadOpenfireProperties();
}
xmlProperties.deleteProperty(name);
openfireProperties.deleteProperty(name);
}

/**
@@ -762,22 +778,105 @@ public static void migrateProperty(String name) {
if (isSetupMode()) {
return;
}
if (getXMLProperty(name) != null) {
if (getProperty(name) == null) {
Log.debug("JiveGlobals: Migrating XML property '"+name+"' into database.");
setProperty(name, getXMLProperty(name));
deleteXMLProperty(name);
}
else if (getProperty(name).equals(getXMLProperty(name))) {
Log.debug("JiveGlobals: Deleting duplicate XML property '"+name+"' that is already in database.");
deleteXMLProperty(name);
}
else if (!getProperty(name).equals(getXMLProperty(name))) {
Log.warn("Property '"+name+"' as specified in openfire.xml differs from what is stored in the database. Please make property changes in the database instead of openfire.xml.");
}
}
openfireProperties.migrateProperty(name);
}

/**
* Flags certain properties as being sensitive, based on
* property naming conventions. Values for matching property
* names are hidden from the Openfire console.
*
* @param name The name of the property
* @returns True if the property is considered sensitive, otherwise false
*/
public static boolean isPropertySensitive(String name) {

return name != null && (
name.toLowerCase().indexOf("passwd") > -1 ||
name.toLowerCase().indexOf("password") > -1 ||
name.toLowerCase().indexOf("cookiekey") > -1);
}


/**
* Determines whether a property is configured for encryption.
*
* @param name The name of the property
* @returns True if the property is stored using encryption, otherwise false
*/
public static boolean isPropertyEncrypted(String name) {
if (securityProperties == null) {
loadSecurityProperties();
}
return name != null &&
!name.startsWith(ENCRYPTED_PROPERTY_NAME_PREFIX) &&
securityProperties.getProperties(ENCRYPTED_PROPERTY_NAMES, true).contains(name);
}

/**
* Set the encryption status for the given property.
*
* @param name The name of the property
* @param encrypt True to encrypt the property, false to decrypt
* @returns True if the property's encryption status changed, otherwise false
*/
public static boolean setPropertyEncrypted(String name, boolean encrypt) {
if (securityProperties == null) {
loadSecurityProperties();
}
boolean propertyWasChanged;
if (isPropertyEncrypted(name)) {
propertyWasChanged = securityProperties.removeFromList(ENCRYPTED_PROPERTY_NAMES, name);
} else {
propertyWasChanged = securityProperties.addToList(ENCRYPTED_PROPERTY_NAMES, name);
}
if (propertyWasChanged) {
resetProperty(name);
}
return propertyWasChanged;
}

/**
* Fetches the current value of the property encryption key.
*
* @returns The property encryption key
*/
public static Encryptor getPropertyEncryptor() {
if (securityProperties == null) {
loadSecurityProperties();
}
if (propertyEncryptor == null) {
String algorithm = securityProperties.getProperty(ENCRYPTION_ALGORITHM);
if (ENCRYPTION_ALGORITHM_AES.equalsIgnoreCase(algorithm)) {
propertyEncryptor = new AesEncryptor(currentKey);
} else {
propertyEncryptor = new Blowfish(currentKey);
}
}
return propertyEncryptor;
}

/**
* This method is called early during the setup process to
* set the algorithm for encrypting property values
*/
public static void setupPropertyEncryptionAlgorithm(String alg) {
if (ENCRYPTION_ALGORITHM_AES.equalsIgnoreCase(alg)) {
securityProperties.setProperty(ENCRYPTION_ALGORITHM, ENCRYPTION_ALGORITHM_AES);
} else {
securityProperties.setProperty(ENCRYPTION_ALGORITHM, ENCRYPTION_ALGORITHM_BLOWFISH);
}
}

/**
* This method is called early during the setup process to
* set a custom key for encrypting property values
*/
public static void setupPropertyEncryptionKey(String key) {
currentKey = key;
securityProperties.setProperty(ENCRYPTION_KEY_CURRENT, new AesEncryptor().encrypt(currentKey));
}

/**
* Allows the name of the local config file name to be changed. The
* default is "openfire.xml".
@@ -789,7 +888,7 @@ public static void setConfigName(String configName) {
}

/**
* Returns the name of the local config file name.
* Returns the name of the local config file.
*
* @return the name of the config file.
*/
@@ -799,9 +898,9 @@ static String getConfigName() {

/**
* Returns true if in setup mode. A false value means that setup has been completed
* or that a connection to the database was possible to properies stored in the
* datbase can be retrieved now. The latter means that once the database settings
* during the setup was done a connection to the datbase should be available thus
* or that a connection to the database was possible to properties stored in the
* database can be retrieved now. The latter means that once the database settings
* during the setup was done a connection to the database should be available thus
* properties stored from a previous setup will be available.
*
* @return true if in setup mode.
@@ -832,11 +931,11 @@ private static boolean isSetupMode() {
}

/**
* Loads properties if necessary. Property loading must be done lazily so
* Loads Openfire properties if necessary. Property loading must be done lazily so
* that we give outside classes a chance to set <tt>home</tt>.
*/
private synchronized static void loadSetupProperties() {
if (xmlProperties == null) {
private synchronized static void loadOpenfireProperties() {
if (openfireProperties == null) {
// If home is null then log that the application will not work correctly
if (home == null && !failedLoading) {
failedLoading = true;
@@ -845,10 +944,10 @@ private synchronized static void loadSetupProperties() {
msg.append("which will prevent the application from working correctly.\n\n");
System.err.println(msg.toString());
}
// Create a manager with the full path to the xml config file.
// Create a manager with the full path to the Openfire config file.
else {
try {
xmlProperties = new XMLProperties(home + File.separator + getConfigName());
openfireProperties = new XMLProperties(home + File.separator + getConfigName());
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
@@ -857,4 +956,132 @@ private synchronized static void loadSetupProperties() {
}
}
}

/**
* Lazy-loads the security configuration properties.
*/
private synchronized static void loadSecurityProperties() {

if (securityProperties == null) {
// If home is null then log that the application will not work correctly
if (home == null && !failedLoading) {
failedLoading = true;
StringBuilder msg = new StringBuilder();
msg.append("Critical Error! The home directory has not been configured, \n");
msg.append("which will prevent the application from working correctly.\n\n");
System.err.println(msg.toString());
try {
securityProperties = new XMLProperties();
} catch (IOException ioe) {
Log.error("Failed to setup default secuirty properties", ioe);
}

}
// Create a manager with the full path to the security XML file.
else {
try {
securityProperties = new XMLProperties(home + File.separator + JIVE_SECURITY_FILENAME);
setupPropertyEncryption();
TaskEngine.getInstance().schedule(new TimerTask() {
public void run() {
// Migrate all secure XML properties into the database automatically
for (String propertyName : securityProperties.getAllPropertyNames()) {
if (!propertyName.startsWith(ENCRYPTED_PROPERTY_NAME_PREFIX)) {
setPropertyEncrypted(propertyName, true);
securityProperties.migrateProperty(propertyName);
}
}
}
}, 1000);
}
catch (IOException ioe) {
Log.error(ioe.getMessage(), ioe);
failedLoading = true;
}
}
}
}

/**
* Setup the property encryption key, rewriting encrypted values as appropriate
*/
private static void setupPropertyEncryption() {

// get/set the current encryption key
Encryptor keyEncryptor = new AesEncryptor();
String encryptedKey = securityProperties.getProperty(ENCRYPTION_KEY_CURRENT);
if (encryptedKey == null || encryptedKey.isEmpty()) {
currentKey = null;
} else {
currentKey = keyEncryptor.decrypt(encryptedKey);
}

// check to see if a new key has been defined
String newKey = securityProperties.getProperty(ENCRYPTION_KEY_NEW, false);
if (newKey != null) {

Log.info("Detected new encryption key; updating encrypted properties");

// if a new key has been provided, check to see if the old key matches
// the current key, otherwise log an error and ignore the new key
String oldKey = securityProperties.getProperty(ENCRYPTION_KEY_OLD);
if (oldKey == null) {
if (currentKey != null) {
Log.warn("Old encryption key was not provided; ignoring new encryption key");
return;
}
} else {
if (!oldKey.equals(currentKey)) {
Log.warn("Old encryption key does not match current encryption key; ignoring new encryption key");
return;
}
}

// load DB properties using the current key
if (properties == null) {
properties = JiveProperties.getInstance();
}

// load XML properties using the current key
Map<String, String> openfireProps = new HashMap<String, String>();
for (String xmlProp : openfireProperties.getAllPropertyNames()) {
if (isPropertyEncrypted(xmlProp)) {
openfireProps.put(xmlProp, openfireProperties.getProperty(xmlProp));
}
}

// rewrite existing encrypted properties using new encryption key
currentKey = newKey == null || newKey.isEmpty() ? null : newKey;
propertyEncryptor = null;
for (String propertyName : securityProperties.getProperties(ENCRYPTED_PROPERTY_NAMES, true)) {
Log.info("Updating encrypted value for " + propertyName);
if (openfireProps.containsKey(propertyName)) {
openfireProperties.setProperty(propertyName, openfireProps.get(propertyName));
} else if (!resetProperty(propertyName)) {
Log.warn("Failed to reset encrypted property value for " + propertyName);
};
}
securityProperties.deleteProperty(ENCRYPTION_KEY_NEW);
securityProperties.deleteProperty(ENCRYPTION_KEY_OLD);
}

// (re)write the encryption key to the security XML file
securityProperties.setProperty(ENCRYPTION_KEY_CURRENT, keyEncryptor.encrypt(currentKey));
}

/**
* Read and re-write a given property to reset its encryption status
* @param propertyName
*/
private static boolean resetProperty(String propertyName) {
if (properties != null) {
String propertyValue = properties.get(propertyName);
if (propertyValue != null) {
properties.remove(propertyName);
properties.put(propertyName, propertyValue);
return true;
}
}
return false;
}
}
@@ -51,12 +51,7 @@
private static final String UPDATE_PROPERTY = "UPDATE ofProperty SET propValue=? WHERE name=?";
private static final String DELETE_PROPERTY = "DELETE FROM ofProperty WHERE name LIKE ?";

private static class JivePropertyHolder {
private static final JiveProperties instance = new JiveProperties();
static {
instance.init();
}
}
private static JiveProperties instance = null;

private Map<String, String> properties;

@@ -65,16 +60,19 @@
*
* @return an instance of JiveProperties.
*/
public static JiveProperties getInstance() {
return JivePropertyHolder.instance;
}

private JiveProperties() {
public synchronized static JiveProperties getInstance() {
if (instance == null) {
JiveProperties props = new JiveProperties();
props.init();
instance = props;
}
return instance;
}
private JiveProperties() { }

/**
* For internal use only. This method allows for the reloading of all properties from the
* values in the datatabase. This is required since it's quite possible during the setup
* values in the database. This is required since it's quite possible during the setup
* process that a database connection will not be available till after this class is
* initialized. Thus, if there are existing properties in the database we will want to reload
* this class after the setup process has been completed.
@@ -192,7 +190,7 @@ public String remove(Object key) {
PropertyEventDispatcher.dispatchEvent((String)key, PropertyEventDispatcher.EventType.property_deleted, params);

// Send update to other cluster members.
CacheFactory.doClusterTask(PropertyClusterEventTask.createDeteleTask((String) key));
CacheFactory.doClusterTask(PropertyClusterEventTask.createDeleteTask((String) key));

return value;
}
@@ -284,13 +282,14 @@ public boolean getBooleanProperty(String name, boolean defaultValue) {
}

private void insertProperty(String name, String value) {
Encryptor encryptor = getEncryptor();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_PROPERTY);
pstmt.setString(1, name);
pstmt.setString(2, value);
pstmt.setString(2, JiveGlobals.isPropertyEncrypted(name) ? encryptor.encrypt(value) : value);
pstmt.executeUpdate();
}
catch (SQLException e) {
@@ -302,12 +301,13 @@ private void insertProperty(String name, String value) {
}

private void updateProperty(String name, String value) {
Encryptor encryptor = getEncryptor();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_PROPERTY);
pstmt.setString(1, value);
pstmt.setString(1, JiveGlobals.isPropertyEncrypted(name) ? encryptor.encrypt(value) : value);
pstmt.setString(2, name);
pstmt.executeUpdate();
}
@@ -337,6 +337,7 @@ private void deleteProperty(String name) {
}

private void loadProperties() {
Encryptor encryptor = getEncryptor();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
@@ -347,7 +348,17 @@ private void loadProperties() {
while (rs.next()) {
String name = rs.getString(1);
String value = rs.getString(2);
properties.put(name, value);
if (JiveGlobals.isPropertyEncrypted(name)) {
try {
value = encryptor.decrypt(value);
} catch (Exception ex) {
Log.error("Failed to load encrypted property value for " + name, ex);
value = null;
}
}
if (value != null) {
properties.put(name, value);
}
}
}
catch (Exception e) {
@@ -357,4 +368,8 @@ private void loadProperties() {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}

private Encryptor getEncryptor() {
return JiveGlobals.getPropertyEncryptor();
}
}
@@ -46,7 +46,7 @@ public static PropertyClusterEventTask createPutTask(String key, String value) {
return task;
}

public static PropertyClusterEventTask createDeteleTask(String key) {
public static PropertyClusterEventTask createDeleteTask(String key) {
PropertyClusterEventTask task = new PropertyClusterEventTask();
task.event = Type.deleted;
task.key = key;
@@ -146,8 +146,8 @@ public User getUser() {
try {
pageUser = getUserManager().getUser(getAuthToken().getUsername());
}
catch (Exception ignored) {
// Ignore.
catch (Exception ex) {
Log.debug("Unexpected exception (which is ignored) while trying to obtain user.", ex);
}
return pageUser;
}
@@ -33,8 +33,10 @@
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -43,6 +45,7 @@
import java.util.StringTokenizer;

import org.apache.commons.lang.StringEscapeUtils;
import org.dom4j.Attribute;
import org.dom4j.CDATA;
import org.dom4j.Document;
import org.dom4j.Element;
@@ -64,7 +67,7 @@
* </pre>
* <p/>
* The XML file is passed in to the constructor and must be readable and
* writtable. Setting property values will automatically persist those value
* writable. Setting property values will automatically persist those value
* to disk. The file encoding used is UTF-8.
*
* @author Derek DeMoro
@@ -73,6 +76,7 @@
public class XMLProperties {

private static final Logger Log = LoggerFactory.getLogger(XMLProperties.class);
private static final String ENCRYPTED_ATTRIBUTE = "encrypted";

private File file;
private Document document;
@@ -83,6 +87,15 @@
*/
private Map<String, String> propertyCache = new HashMap<String, String>();

/**
* Creates a new empty XMLPropertiesTest object.
*
* @throws IOException if an error occurs loading the properties.
*/
public XMLProperties() throws IOException {
buildDoc(new StringReader("<root />"));
}

/**
* Creates a new XMLPropertiesTest object.
*
@@ -152,13 +165,24 @@ public XMLProperties(File file) throws IOException {
* @return the value of the specified property.
*/
public synchronized String getProperty(String name) {
return getProperty(name, true);
}

/**
* Returns the value of the specified property.
*
* @param name the name of the property to get.
* @param ignoreEmpty Ignore empty property values (return null)
* @return the value of the specified property.
*/
public synchronized String getProperty(String name, boolean ignoreEmpty) {
String value = propertyCache.get(name);
if (value != null) {
return value;
}

String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy.
// Search for this property by traversing down the XML hierarchy.
Element element = document.getRootElement();
for (String aPropName : propName) {
element = element.element(aPropName);
@@ -171,10 +195,21 @@ public synchronized String getProperty(String name) {
// At this point, we found a matching property, so return its value.
// Empty strings are returned as null.
value = element.getTextTrim();
if ("".equals(value)) {
if (ignoreEmpty && "".equals(value)) {
return null;
}
else {
// check to see if the property is marked as encrypted
if (JiveGlobals.isPropertyEncrypted(name)) {
Attribute encrypted = element.attribute(ENCRYPTED_ATTRIBUTE);
if (encrypted != null) {
value = JiveGlobals.getPropertyEncryptor().decrypt(value);
} else {
// rewrite property as an encrypted value
Log.info("Rewriting XML property " + name + " as an encrypted value");
setProperty(name, value);
}
}
// Add to cache so that getting property next time is fast.
propertyCache.put(name, value);
return value;
@@ -202,32 +237,75 @@ public synchronized String getProperty(String name) {
* @param name the name of the property to retrieve
* @return all child property values for the given node name.
*/
public String[] getProperties(String name) {
public List<String> getProperties(String name, boolean asList) {
List<String> result = new ArrayList<String>();
String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy,
// Search for this property by traversing down the XML hierarchy,
// stopping one short.
Element element = document.getRootElement();
for (int i = 0; i < propName.length - 1; i++) {
element = element.element(propName[i]);
if (element == null) {
// This node doesn't match this part of the property name which
// indicates this property doesn't exist so return empty array.
return new String[]{};
return result;
}
}
// We found matching property, return names of children.
Iterator iter = element.elementIterator(propName[propName.length - 1]);
List<String> props = new ArrayList<String>();
Iterator<Element> iter = element.elementIterator(propName[propName.length - 1]);
Element prop;
String value;
boolean updateEncryption = false;
while (iter.hasNext()) {
prop = iter.next();
// Empty strings are skipped.
value = ((Element)iter.next()).getTextTrim();
value = prop.getTextTrim();
if (!"".equals(value)) {
props.add(value);
// check to see if the property is marked as encrypted
if (JiveGlobals.isPropertyEncrypted(name)) {
Attribute encrypted = prop.attribute(ENCRYPTED_ATTRIBUTE);
if (encrypted != null) {
value = JiveGlobals.getPropertyEncryptor().decrypt(value);
} else {
// rewrite property as an encrypted value
prop.addAttribute(ENCRYPTED_ATTRIBUTE, "true");
updateEncryption = true;
}
}
result.add(value);
}
}
String[] childrenNames = new String[props.size()];
return props.toArray(childrenNames);
if (updateEncryption) {
Log.info("Rewriting values for XML property " + name + " using encryption");
saveProperties();
}
return result;
}

/**
* Return all values who's path matches the given property
* name as a String array, or an empty array if the if there
* are no children. This allows you to retrieve several values
* with the same property name. For example, consider the
* XML file entry:
* <pre>
* &lt;foo&gt;
* &lt;bar&gt;
* &lt;prop&gt;some value&lt;/prop&gt;
* &lt;prop&gt;other value&lt;/prop&gt;
* &lt;prop&gt;last value&lt;/prop&gt;
* &lt;/bar&gt;
* &lt;/foo&gt;
* </pre>
* If you call getProperties("foo.bar.prop") will return a string array containing
* {"some value", "other value", "last value"}.
*
* @deprecated Retained for backward compatibility. Prefer getProperties(String, boolean)
* @param name the name of the property to retrieve
* @return all child property values for the given node name.
*/
public String[] getProperties(String name) {
return (String[]) getProperties(name, false).toArray();
}

/**
@@ -253,7 +331,7 @@ public synchronized String getProperty(String name) {
*/
public Iterator getChildProperties(String name) {
String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy,
// Search for this property by traversing down the XML hierarchy,
// stopping one short.
Element element = document.getRootElement();
for (int i = 0; i < propName.length - 1; i++) {
@@ -265,17 +343,25 @@ public Iterator getChildProperties(String name) {
}
}
// We found matching property, return values of the children.
Iterator iter = element.elementIterator(propName[propName.length - 1]);
Iterator<Element> iter = element.elementIterator(propName[propName.length - 1]);
ArrayList<String> props = new ArrayList<String>();
Element prop;
String value;
while (iter.hasNext()) {
props.add(((Element)iter.next()).getText());
prop = iter.next();
value = prop.getText();
// check to see if the property is marked as encrypted
if (JiveGlobals.isPropertyEncrypted(name) && Boolean.parseBoolean(prop.attribute(ENCRYPTED_ATTRIBUTE).getText())) {
value = JiveGlobals.getPropertyEncryptor().decrypt(value);
}
props.add(value);
}
return props.iterator();
}

/**
* Returns the value of the attribute of the given property name or <tt>null</tt>
* if it doesn't exist. Note, this
* if it doesn't exist.
*
* @param name the property name to lookup - ie, "foo.bar"
* @param attribute the name of the attribute, ie "id"
@@ -287,7 +373,7 @@ public String getAttribute(String name, String attribute) {
return null;
}
String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy.
// Search for this property by traversing down the XML hierarchy.
Element element = document.getRootElement();
for (String child : propName) {
element = element.element(child);
@@ -304,6 +390,39 @@ public String getAttribute(String name, String attribute) {
return null;
}

/**
* Removes the given attribute from the XML document.
*
* @param name the property name to lookup - ie, "foo.bar"
* @param attribute the name of the attribute, ie "id"
* @return the value of the attribute of the given property or <tt>null</tt> if
* it did not exist.
*/
public String removeAttribute(String name, String attribute) {
if (name == null || attribute == null) {
return null;
}
String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML hierarchy.
Element element = document.getRootElement();
for (String child : propName) {
element = element.element(child);
if (element == null) {
// This node doesn't match this part of the property name which
// indicates this property doesn't exist so return empty array.
break;
}
}
String result = null;
if (element != null) {
// Get the attribute value and then remove the attribute
Attribute attr = element.attribute(attribute);
result = attr.getValue();
element.remove(attr);
}
return result;
}

/**
* Sets a property to an array of values. Multiple values matching the same property
* is mapped to an XML file as multiple elements containing each value.
@@ -324,11 +443,11 @@ public String getAttribute(String name, String attribute) {
*/
public void setProperties(String name, List<String> values) {
String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy,
// Search for this property by traversing down the XML hierarchy,
// stopping one short.
Element element = document.getRootElement();
for (int i = 0; i < propName.length - 1; i++) {
// If we don't find this part of the property in the XML heirarchy
// If we don't find this part of the property in the XML hierarchy
// we add it as a new node
if (element.element(propName[i]) == null) {
element.addElement(propName[i]);
@@ -338,9 +457,9 @@ public void setProperties(String name, List<String> values) {
String childName = propName[propName.length - 1];
// We found matching property, clear all children.
List<Element> toRemove = new ArrayList<Element>();
Iterator iter = element.elementIterator(childName);
Iterator<Element> iter = element.elementIterator(childName);
while (iter.hasNext()) {
toRemove.add((Element) iter.next());
toRemove.add(iter.next());
}
for (iter = toRemove.iterator(); iter.hasNext();) {
element.remove((Element)iter.next());
@@ -349,9 +468,9 @@ public void setProperties(String name, List<String> values) {
for (String value : values) {
Element childElement = element.addElement(childName);
if (value.startsWith("<![CDATA[")) {
Iterator it = childElement.nodeIterator();
Iterator<Node> it = childElement.nodeIterator();
while (it.hasNext()) {
Node node = (Node) it.next();
Node node = it.next();
if (node instanceof CDATA) {
childElement.remove(node);
break;
@@ -360,7 +479,13 @@ public void setProperties(String name, List<String> values) {
childElement.addCDATA(value.substring(9, value.length()-3));
}
else {
childElement.setText(StringEscapeUtils.escapeXml(value));
String propValue = StringEscapeUtils.escapeXml(value);
// check to see if the property is marked as encrypted
if (JiveGlobals.isPropertyEncrypted(name)) {
propValue = JiveGlobals.getPropertyEncryptor().encrypt(propValue);
childElement.addAttribute(ENCRYPTED_ATTRIBUTE, "true");
}
childElement.setText(propValue);
}
}
saveProperties();
@@ -371,6 +496,72 @@ public void setProperties(String name, List<String> values) {
PropertyEventDispatcher.dispatchEvent(name,
PropertyEventDispatcher.EventType.xml_property_set, params);
}

/**
* Adds the given value to the list of values represented by the property name.
* The property is created if it did not already exist.
*
* @param propertyName The name of the property list to change
* @param value The value to be added to the list
* @return True if the value was added to the list; false if the value was already present
*/
public boolean addToList(String propertyName, String value) {

List<String> properties = getProperties(propertyName, true);
boolean propertyWasAdded = properties.add(value);
if (propertyWasAdded) {
setProperties(propertyName, properties);
}
return propertyWasAdded;
}

/**
* Removes the given value from the list of values represented by the property name.
* The property is deleted if it no longer contains any values.
*
* @param propertyName The name of the property list to change
* @param value The value to be removed from the list
* @return True if the value was removed from the list; false if the value was not found
*/
public boolean removeFromList(String propertyName, String value) {

List<String> properties = getProperties(propertyName, true);
boolean propertyWasRemoved = properties.remove(value);
if (propertyWasRemoved) {
setProperties(propertyName, properties);
}
return propertyWasRemoved;
}

/**
* Returns a list of names for all properties found in the XML file.
*
* @return Names for all properties in the file
*/
public List<String> getAllPropertyNames() {
List<String> result = new ArrayList<String>();
for (String propertyName : getChildPropertyNamesFor(document.getRootElement(), "")) {
if (getProperty(propertyName) != null) {
result.add(propertyName);
}
}
return result;
}

private List<String> getChildPropertyNamesFor(Element parent, String parentName) {
List<String> result = new ArrayList<String>();
for (Element child : (Collection<Element>) parent.elements()) {
String childName = new StringBuilder(parentName)
.append(parentName.isEmpty() ? "" : ".")
.append(child.getName())
.toString();
if (!result.contains(childName)) {
result.add(childName);
result.addAll(getChildPropertyNamesFor(child, childName));
}
}
return result;
}

/**
* Return all children property names of a parent property as a String array,
@@ -384,7 +575,7 @@ public void setProperties(String name, List<String> values) {
*/
public String[] getChildrenProperties(String parent) {
String[] propName = parsePropertyName(parent);
// Search for this property by traversing down the XML heirarchy.
// Search for this property by traversing down the XML hierarchy.
Element element = document.getRootElement();
for (String aPropName : propName) {
element = element.element(aPropName);
@@ -426,10 +617,10 @@ public synchronized void setProperty(String name, String value) {
propertyCache.put(name, value);

String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy.
// Search for this property by traversing down the XML hierarchy.
Element element = document.getRootElement();
for (String aPropName : propName) {
// If we don't find this part of the property in the XML heirarchy
// If we don't find this part of the property in the XML hierarchy
// we add it as a new node
if (element.element(aPropName) == null) {
element.addElement(aPropName);
@@ -449,7 +640,13 @@ public synchronized void setProperty(String name, String value) {
element.addCDATA(value.substring(9, value.length()-3));
}
else {
element.setText(value);
String propValue = StringEscapeUtils.escapeXml(value);
// check to see if the property is marked as encrypted
if (JiveGlobals.isPropertyEncrypted(name)) {
propValue = JiveGlobals.getPropertyEncryptor().encrypt(propValue);
element.addAttribute(ENCRYPTED_ATTRIBUTE, "true");
}
element.setText(propValue);
}
// Write the XML properties to disk
saveProperties();
@@ -471,7 +668,7 @@ public synchronized void deleteProperty(String name) {
propertyCache.remove(name);

String[] propName = parsePropertyName(name);
// Search for this property by traversing down the XML heirarchy.
// Search for this property by traversing down the XML hierarchy.
Element element = document.getRootElement();
for (int i = 0; i < propName.length - 1; i++) {
element = element.element(propName[i]);
@@ -482,6 +679,9 @@ public synchronized void deleteProperty(String name) {
}
// Found the correct element to remove, so remove it...
element.remove(element.element(propName[propName.length - 1]));
if (element.elements().size() == 0) {
element.getParent().remove(element);
}
// .. then write to disk.
saveProperties();

@@ -490,6 +690,30 @@ public synchronized void deleteProperty(String name) {
PropertyEventDispatcher.dispatchEvent(name, PropertyEventDispatcher.EventType.xml_property_deleted, params);
}

/**
* Convenience routine to migrate an XML property into the database
* storage method. Will check for the XML property being null before
* migrating.
*
* @param name the name of the property to migrate.
*/
public void migrateProperty(String name) {
if (getProperty(name) != null) {
if (JiveGlobals.getProperty(name) == null) {
Log.debug("JiveGlobals: Migrating XML property '"+name+"' into database.");
JiveGlobals.setProperty(name, getProperty(name));
deleteProperty(name);
}
else if (JiveGlobals.getProperty(name).equals(getProperty(name))) {
Log.debug("JiveGlobals: Deleting duplicate XML property '"+name+"' that is already in database.");
deleteProperty(name);
}
else if (!JiveGlobals.getProperty(name).equals(getProperty(name))) {
Log.warn("XML Property '"+name+"' differs from what is stored in the database. Please make property changes in the database instead of the configuration file.");
}
}
}

/**
* Builds the document XML model up based the given reader of XML data.
* @param in the input stream used to build the xml document
@@ -545,7 +769,7 @@ private synchronized void saveProperties() {
}
}

// No errors occured, so delete the main file.
// No errors occurred, so delete the main file.
if (!error) {
// Delete the old file so we can replace it.
if (!file.delete()) {
@@ -19,6 +19,7 @@
*/
package org.jivesoftware.util.cache;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -70,7 +71,7 @@

private static String localCacheFactoryClass;
private static String clusteredCacheFactoryClass;
private static CacheFactoryStrategy cacheFactoryStrategy;
private static CacheFactoryStrategy cacheFactoryStrategy = new DefaultLocalCacheStrategy();
private static CacheFactoryStrategy localCacheFactoryStrategy;
private static CacheFactoryStrategy clusteredCacheFactoryStrategy;
private static Thread statsThread;
@@ -418,7 +419,11 @@ public static synchronized Lock getLock(Object key, Cache cache) {

@SuppressWarnings("unchecked")
private static <T extends Cache> T wrapCache(T cache, String name) {
cache = (T) new CacheWrapper(cache);
if ("Routing Components Cache".equals(name)) {
cache = (T) new ComponentCacheWrapper(cache);
} else {
cache = (T) new CacheWrapper(cache);
}
cache.setName(name);

caches.put(name, cache);
@@ -440,6 +445,8 @@ public static boolean isClusteringAvailable() {
clusteredCacheFactoryStrategy = (CacheFactoryStrategy) Class.forName(
clusteredCacheFactoryClass, true,
getClusteredCacheStrategyClassLoader()).newInstance();
} catch (NoClassDefFoundError e) {
log.warn("Clustered cache factory strategy " + clusteredCacheFactoryClass + " not found");
} catch (Exception e) {
log.warn("Clustered cache factory strategy " + clusteredCacheFactoryClass + " not found");
}
@@ -626,10 +633,18 @@ private static ClassLoader getClusteredCacheStrategyClassLoader() {
}
PluginClassLoader pluginLoader = pluginManager.getPluginClassloader(plugin);
if (pluginLoader != null) {
if (log.isDebugEnabled()) {
StringBuffer pluginLoaderDetails = new StringBuffer("Clustering plugin class loader: ");
pluginLoaderDetails.append(pluginLoader.getClass().getName());
for (URL url : pluginLoader.getURLs()) {
pluginLoaderDetails.append("\n\t").append(url.toExternalForm());
}
log.debug(pluginLoaderDetails.toString());
}
return pluginLoader;
}
else {
log.debug("CacheFactory - Unable to find a Plugin that provides clustering support.");
log.warn("CacheFactory - Unable to find a Plugin that provides clustering support.");
return Thread.currentThread().getContextClassLoader();
}
}
@@ -0,0 +1,39 @@
/**
* $RCSfile$
* $Revision: 3144 $
* $Date: 2005-12-01 14:20:11 -0300 (Thu, 01 Dec 2005) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.util.cache;


/**
* This specialized wrapper is used for the Components cache, which
* should not be purged.
*
* See {@link http://issues.igniterealtime.org/browse/OF-114} for more info.
*
*/
public class ComponentCacheWrapper<K, V> extends CacheWrapper<K, V> {

public ComponentCacheWrapper(Cache<K, V> cache) {
super(cache);
}

public void clear() {
// no-op; we don't want to clear the components cache
}
}
@@ -47,6 +47,8 @@

private long initialized = -1;

private boolean messageCarbonsEnabled;

public RemoteClientSession(byte[] nodeID, JID address) {
super(nodeID, address);
}
@@ -153,6 +155,16 @@ public int incrementConflictCount() {
return (Integer) doSynchronousClusterTask(task);
}

@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}

@Override
public void setMessageCarbonsEnabled(boolean enabled) {
messageCarbonsEnabled = true;
}

RemoteSessionTask getRemoteSessionTask(RemoteSessionTask.Operation operation) {
return new ClientSessionTask(address, operation);
}
@@ -44,6 +44,20 @@ <h1>
Hazelcast Clustering Plugin Changelog
</h1>

<p><b>1.2.1</b> -- April 10, 2014</p>
<p>Hazelcast update:</p>
<ul>
<li>Updated Hazelcast to release 3.1.7 (<a href="http://www.hazelcast.org/docs/3.1/manual/html-single/#WhatsNew31">what's new</a>).</li>
<li>Changed configuration file and README to adjust for Hazelcast schema changes.</li>
</ul>

<p><b>1.2.0</b> -- February 10, 2014</p>
<p>Miscellaneous enhancements:</p>
<ul>
<li>Fix cluster initialization logic (<a href="http://issues.igniterealtime.org/browse/OF-699">OF-699</a>)</li>
<li>Updated Hazelcast to release 3.1.5.</li>
</ul>

<p><b>1.1.0</b> -- Sep 13, 2013</p>
<ul>
<li>Requires Openfire 3.9.0.</li>
@@ -54,7 +68,7 @@ <h1>
<ul>
<li>Added support for cluster time (<a href="http://issues.igniterealtime.org/browse/OF-666">OF-666</a>)</li>
<li>Added <code>hazelcast-cloud.jar</code> to support AWS deployments (<a href="http://community.igniterealtime.org/blogs/ignite/2012/09/23/introducing-hazelcast-a-new-way-to-cluster-openfire#comment-8027">more info</a>).</li>
<li>Updated Hazelcast to release 2.5.1 (<a href="http://www.hazelcast.com/docs/2.5/manual/single_html/#ReleaseNotes">bug fixes</a>).</li>
<li>Updated Hazelcast to release 2.5.1 (<a href="http://www.hazelcast.org/docs/2.5/manual/single_html/#ReleaseNotes">bug fixes</a>).</li>
</ul>

<p><b>1.0.5</b> -- March 26, 2013</p>
@@ -81,7 +95,7 @@ <h1>
</ul>

<p><b>1.0.2</b> -- January 9, 2013</p>
<p>This release addresses a number of issues and other feedback received via the
<p>This release addresses a number of issues and other feedback received via the
<a href="http://community.igniterealtime.org/message/224947#224947">Hazelcast announcement</a>
posted in the Openfire community forum:</p>
<ul>
@@ -15,7 +15,7 @@
~ limitations under the License.
-->

<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-2.3.xsd"
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.1.xsd"
xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<group>
@@ -137,7 +137,7 @@
Any integer between 0 and Integer.MAX_VALUE. 0 means
Integer.MAX_VALUE. Default is 0.
-->
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<!--
When max. size is reached, specified percentage of
the map will be evicted. Any integer between 0 and 100.
@@ -250,13 +250,13 @@

<map name="POP3 Authentication">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">10000</max-size>
<max-size policy="per_partition">10000</max-size>
<time-to-live-seconds>3600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="LDAP Authentication">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">10000</max-size>
<max-size policy="per_partition">10000</max-size>
<time-to-live-seconds>7200</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
@@ -267,7 +267,7 @@
</map>
<map name="File Transfer Cache">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">10000</max-size>
<max-size policy="per_partition">10000</max-size>
<time-to-live-seconds>600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
@@ -278,7 +278,7 @@
</map>
<map name="Javascript Cache">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">10000</max-size>
<max-size policy="per_partition">10000</max-size>
<time-to-live-seconds>864000</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
@@ -304,7 +304,7 @@
</map>
<map name="Last Activity Cache">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">10000</max-size>
<max-size policy="per_partition">10000</max-size>
<time-to-live-seconds>21600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
@@ -315,37 +315,37 @@
</map>
<map name="Multicast Service">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">10000</max-size>
<max-size policy="per_partition">10000</max-size>
<time-to-live-seconds>86400</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Offline Message Size">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>43200</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Offline Presence Cache">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>21600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Privacy Lists">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>21600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Remote Users Existence">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Remote Server Configurations">
<backup-count>1</backup-count>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>1800</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
@@ -355,28 +355,28 @@
<map name="Group Metadata Cache">
<backup-count>1</backup-count>
<read-backup-data>true</read-backup-data>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<max-idle-seconds>3600</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Group">
<backup-count>1</backup-count>
<read-backup-data>true</read-backup-data>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<max-idle-seconds>3600</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="Roster">
<backup-count>1</backup-count>
<read-backup-data>true</read-backup-data>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<max-idle-seconds>3600</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
<map name="User">
<backup-count>1</backup-count>
<read-backup-data>true</read-backup-data>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<max-idle-seconds>3600</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
</map>
@@ -386,7 +386,7 @@
<map name="VCard">
<backup-count>1</backup-count>
<read-backup-data>true</read-backup-data>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>21600</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
<near-cache>
@@ -399,7 +399,7 @@
<map name="Published Items">
<backup-count>1</backup-count>
<read-backup-data>true</read-backup-data>
<max-size policy="cluster_wide_map_size">100000</max-size>
<max-size policy="per_partition">100000</max-size>
<time-to-live-seconds>900</time-to-live-seconds>
<eviction-policy>LRU</eviction-policy>
<near-cache>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@
<name>${plugin.name}</name>
<description>${plugin.description}</description>
<author>Tom Evans</author>
<version>1.1.0</version>
<date>09/13/2013</date>
<version>1.2.1</version>
<date>04/10/2014</date>
<minServerVersion>3.9.0</minServerVersion>
</plugin>
@@ -61,45 +61,45 @@ <h2>Overview</h2>
distribute the connection load among several servers, while also providing
failover in the event that one of your servers fails. This plugin is a
drop-in replacement for the original Openfire clustering plugin, using the
open source <a href="http://www.hazelcast.com">Hazelcast</a> data distribution
open source <a href="http://www.hazelcast.org">Hazelcast</a> data distribution
framework in lieu of an expensive proprietary third-party product.
</p>
<p>
The current Hazelcast release is version 2.5.1.
The current Hazelcast release is version 3.1.7.
</p>
<h2>Installation</h2>
<p>
To create an Openfire cluster, you should have at least two Openfire servers,
and each server must have the Hazelcast plugin installed. To install Hazelcast,
simply drop the hazelcast.jar into $OPENFIRE_HOME/plugins along with any other
plugins you may have installed. You may also use the Plugins page from the
admin console to install the plugin. Note that all servers in a given cluster
must be configured to share a single external database (not the Embedded DB).
and each server must have the Hazelcast plugin installed. To install Hazelcast,
simply drop the hazelcast.jar into $OPENFIRE_HOME/plugins along with any other
plugins you may have installed. You may also use the Plugins page from the
admin console to install the plugin. Note that all servers in a given cluster
must be configured to share a single external database (not the Embedded DB).
</p>
<p>
By default during the Openfire startup/initialization process, the servers
will discover each other by exchanging UDP (multicast) packets via a configurable
IP address and port. However, be advised that many other initialization options
are available and may be used if your network does not support multicast
IP address and port. However, be advised that many other initialization options
are available and may be used if your network does not support multicast
communication (see <a href="#config">Configuration</a> below).
</p>
<p>After the Hazelcast plugin has been deployed to each of the servers, use the
radio button controls located on the Clustering page in the admin console to
activate/enable the cluster. You only need to enable clustering once; the change
will be propagated to the other servers automatically. After refreshing the
Clustering page you will be able to see all the servers that have successfully
<p>After the Hazelcast plugin has been deployed to each of the servers, use the
radio button controls located on the Clustering page in the admin console to
activate/enable the cluster. You only need to enable clustering once; the change
will be propagated to the other servers automatically. After refreshing the
Clustering page you will be able to see all the servers that have successfully
joined the cluster.
</p>
<p>
Note that Hazelcast and the earlier clustering plugins (clustering.jar and enterprise.jar)
are mutually exclusive. You will need to remove any existing older clustering plugin(s)
Note that Hazelcast and the earlier clustering plugins (clustering.jar and enterprise.jar)
are mutually exclusive. You will need to remove any existing older clustering plugin(s)
before installing Hazelcast into your Openfire server(s).
</p>
<p>
With your cluster up and running, you will now want some form of load balancer to
distribute the connection load among the members of your Openfire cluster. There
are several commercial and open source alternatives for this. For example,
if you are using the HTTP/BOSH Openfire connector to connect to Openfire,
With your cluster up and running, you will now want some form of load balancer to
distribute the connection load among the members of your Openfire cluster. There
are several commercial and open source alternatives for this. For example,
if you are using the HTTP/BOSH Openfire connector to connect to Openfire,
the Apache web server (httpd) plus the corresponding proxy balancer module
(<a href="http://httpd.apache.org/docs/current/mod/mod_proxy_balancer.html">mod_proxy_balancer</a>)
could provide a workable solution. Some other popular options include the
@@ -109,9 +109,9 @@ <h2>Installation</h2>
<p>
A simple round-robin DNS configuration can help distribute XMPP connections across multiple
Openfire servers in a cluster. While popular as a lightweight and low-cost way to provide
basic scalability, note that this approach is not considered adequate for true load balancing
nor does it provide high availability (HA) from a client perspective. If you are evaluating
these options, you can <a href="http://en.wikipedia.org/wiki/Round-robin_DNS">read more here</a>.
basic scalability, note that this approach is not considered adequate for true load balancing
nor does it provide high availability (HA) from a client perspective. If you are evaluating
these options, you can <a href="http://en.wikipedia.org/wiki/Round-robin_DNS">read more here</a>.
</p>
<h2>Upgrading the Hazelcast Plugin</h2>
<p>
@@ -134,7 +134,7 @@ <h3>Option 1: Offline</h3>
</ol>
<li>Repeat these steps for the remaining servers in the cluster.</li>
</ol>
<h3>Option 2: Online</h3>
<h3>Option 2: Online</h3>
<p><b>NOTE:</b> Using this approach you should be able to continue servicing
XMPP connections during the upgrade.
</p>
@@ -144,9 +144,9 @@ <h3>Option 2: Online</h3>
<li>Upload the new Hazelcast plugin and confirm it is installed (refresh the page if necessary)</li>
<li>Use the "Offline" steps above to upgrade and restart the remaining servers.</li>
</ol>
<h3>Option 3: Split-Brain</h3>
<h3>Option 3: Split-Brain</h3>
<p><b>NOTE:</b> Use this approach if you only have access to the Openfire console.
Note however that users may not be able to communicate with each other during the upgrade
Note however that users may not be able to communicate with each other during the upgrade
(if they are connected to different servers).
</p>
<ol>
@@ -174,19 +174,19 @@ <h2>Configuration</h2>
<li><i>hazelcast.config.xml.filename</i> (hazelcast-cache-config.xml): Name
of the Hazelcast configuration file. By overriding this value you can easily
install a custom cache configuration file in the Hazelcast plugin /classes/
directory, in the directory named via the <i>hazelcast.config.xml.directory</i>
directory, in the directory named via the <i>hazelcast.config.xml.directory</i>
property (described below), or in the classpath of your own custom plugin.</li>
<li><i>hazelcast.config.xml.directory</i> ({OPENFIRE_HOME}/conf): Directory
that will be added to the plugin's classpath. This allows a custom Hazelcast
that will be added to the plugin's classpath. This allows a custom Hazelcast
configuration file to be located outside the Openfire home directory.</li>
<li><i>hazelcast.config.jmx.enabled</i> (false): Enables JMX support for
the Hazelcast cluster if JMX has been enabled via the Openfire admin console.
Refer to the <a href="http://www.hazelcast.com/docs/2.5/manual/multi_html/ch06.html">
the Hazelcast cluster if JMX has been enabled via the Openfire admin console.
Refer to the <a href="http://www.hazelcast.com/docs/3.1/manual/multi_html/ch07.html">
Hazelcast JMX docs</a> for additional information.</li>
</ol>
</p>
<p>
The Hazelcast plugin uses the <a href="http://www.hazelcast.com/docs/2.5/manual/single_html/#Config">
The Hazelcast plugin uses the <a href="http://www.hazelcast.org/docs/3.1/manual/single_html/#Config">
XML configuration builder</a> to initialize the cluster from the XML file described above.
By default the cluster members will attempt to discover each other via multicast at the
following location:
@@ -204,14 +204,14 @@ <h2>Configuration</h2>
&lt;join&gt;
&lt;multicast enabled="false"/&gt;
&lt;tcp-ip enabled="true"&gt;
&lt;hostname&gt;of-node-a.example.com:5701&lt;/hostname&gt;
&lt;hostname&gt;of-node-b.example.com:5701&lt;/hostname&gt;
&lt;member&gt;of-node-a.example.com:5701&lt;/member&gt;
&lt;member&gt;of-node-b.example.com:5701&lt;/member&gt;
&lt;/tcp-ip&gt;
&lt;aws enabled="false"/&gt;
&lt;/join&gt;
...
</pre>
Please refer to the <a href="http://www.hazelcast.com/docs/2.5/manual/single_html/">
Please refer to the <a href="http://www.hazelcast.org/docs/3.1/manual/single_html/">
Hazelcast reference manual</a> for more information.
</p>
</body>
@@ -47,6 +47,8 @@

private long initialized = -1;

private boolean messageCarbonsEnabled;

public RemoteClientSession(byte[] nodeID, JID address) {
super(nodeID, address);
}
@@ -150,7 +152,18 @@ public void setPresence(Presence presence) {

public int incrementConflictCount() {
ClusterTask task = getRemoteSessionTask(RemoteSessionTask.Operation.incrementConflictCount);
return (Integer) doSynchronousClusterTask(task);
Object result = doSynchronousClusterTask(task);
return result == null ? 0 : (Integer) result;
}

@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}

@Override
public void setMessageCarbonsEnabled(boolean enabled) {
this.messageCarbonsEnabled = enabled;
}

RemoteSessionTask getRemoteSessionTask(RemoteSessionTask.Operation operation) {
@@ -20,19 +20,26 @@

package com.jivesoftware.util.cache;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.Externalizable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jivesoftware.util.cache.ExternalizableUtilStrategy;

import com.hazelcast.nio.SerializationHelper;
import com.hazelcast.core.HazelcastInstance;

/**
* Serialization strategy that uses Hazelcast as its underlying mechanism.
@@ -51,7 +58,7 @@
* @throws java.io.IOException if an error occurs.
*/
public void writeStringMap(DataOutput out, Map<String, String> stringMap) throws IOException {
SerializationHelper.writeObject(out, stringMap);
writeObject(out, stringMap);
}

/**
@@ -63,7 +70,7 @@ public void writeStringMap(DataOutput out, Map<String, String> stringMap) throws
* @throws IOException if an error occurs.
*/
public Map<String, String> readStringMap(DataInput in) throws IOException {
return (Map<String, String>) SerializationHelper.readObject(in);
return (Map<String, String>) readObject(in);
}

/**
@@ -75,7 +82,7 @@ public void writeStringMap(DataOutput out, Map<String, String> stringMap) throws
* @throws java.io.IOException if an error occurs.
*/
public void writeStringsMap(DataOutput out, Map<String, Set<String>> map) throws IOException {
SerializationHelper.writeObject(out, map);
writeObject(out, map);
}

/**
@@ -87,7 +94,7 @@ public void writeStringsMap(DataOutput out, Map<String, Set<String>> map) throws
* @throws IOException if an error occurs.
*/
public int readStringsMap(DataInput in, Map<String, Set<String>> map) throws IOException {
Map<String, Set<String>> result = (Map<String, Set<String>>) SerializationHelper.readObject(in);
Map<String, Set<String>> result = (Map<String, Set<String>>) readObject(in);
if (result == null) return 0;
map.putAll(result);
return result.size();
@@ -102,7 +109,7 @@ public int readStringsMap(DataInput in, Map<String, Set<String>> map) throws IOE
* @throws IOException if an error occurs.
*/
public void writeLongIntMap(DataOutput out, Map<Long, Integer> map) throws IOException {
SerializationHelper.writeObject(out, map);
writeObject(out, map);
}

/**
@@ -114,7 +121,7 @@ public void writeLongIntMap(DataOutput out, Map<Long, Integer> map) throws IOExc
* @throws IOException if an error occurs.
*/
public Map<Long, Integer> readLongIntMap(DataInput in) throws IOException {
return (Map<Long, Integer>) SerializationHelper.readObject(in);
return (Map<Long, Integer>) readObject(in);
}

/**
@@ -126,7 +133,7 @@ public void writeLongIntMap(DataOutput out, Map<Long, Integer> map) throws IOExc
* @throws IOException if an error occurs.
*/
public void writeStringList(DataOutput out, List<String> stringList) throws IOException {
SerializationHelper.writeObject(out, stringList);
writeObject(out, stringList);
}

/**
@@ -138,7 +145,7 @@ public void writeStringList(DataOutput out, List<String> stringList) throws IOEx
* @throws IOException if an error occurs.
*/
public List<String> readStringList(DataInput in) throws IOException {
return (List<String>) SerializationHelper.readObject(in);
return (List<String>) readObject(in);
}

/**
@@ -150,7 +157,7 @@ public void writeStringList(DataOutput out, List<String> stringList) throws IOEx
* @throws IOException if an error occurs.
*/
public void writeLongArray(DataOutput out, long [] array) throws IOException {
SerializationHelper.writeObject(out, array);
writeObject(out, array);
}

/**
@@ -162,112 +169,301 @@ public void writeLongArray(DataOutput out, long [] array) throws IOException {
* @throws IOException if an error occurs.
*/
public long [] readLongArray(DataInput in) throws IOException {
return (long[]) SerializationHelper.readObject(in);
return (long []) readObject(in);
}

public void writeLong(DataOutput out, long value) throws IOException {
SerializationHelper.writeObject(out, value);
writeObject(out, value);
}

public long readLong(DataInput in) throws IOException {
return (Long) SerializationHelper.readObject(in);
return (Long) readObject(in);
}

public void writeByteArray(DataOutput out, byte[] value) throws IOException {
SerializationHelper.writeObject(out, value);
writeObject(out, value);
}

public byte[] readByteArray(DataInput in) throws IOException {
return (byte[]) SerializationHelper.readObject(in);
return (byte []) readObject(in);
}

public void writeInt(DataOutput out, int value) throws IOException {
SerializationHelper.writeObject(out, value);
writeObject(out, value);
}

public int readInt(DataInput in) throws IOException {
return (Integer) SerializationHelper.readObject(in);
return (Integer) readObject(in);
}

public void writeBoolean(DataOutput out, boolean value) throws IOException {
SerializationHelper.writeObject(out, value);
writeObject(out, value);
}

public boolean readBoolean(DataInput in) throws IOException {
return (Boolean) SerializationHelper.readObject(in);
return (Boolean) readObject(in);
}

public void writeSerializable(DataOutput out, Serializable value) throws IOException {
SerializationHelper.writeObject(out, value);
writeObject(out, value);
}

public Serializable readSerializable(DataInput in) throws IOException {
return (Serializable) SerializationHelper.readObject(in);
return (Serializable) readObject(in);
}

public void writeSafeUTF(DataOutput out, String value) throws IOException {
SerializationHelper.writeObject(out, value);
writeObject(out, value);
}

public String readSafeUTF(DataInput in) throws IOException {
return (String) SerializationHelper.readObject(in);
return (String) readObject(in);
}

public void writeExternalizableCollection(DataOutput out, Collection<? extends Externalizable> value)
throws IOException {
SerializationHelper.writeObject(out, value);
/**
* Writes a collection of Externalizable objects. The collection passed as a parameter
* must be a collection and not a <tt>null</null> value.
*
* @param out the output stream.
* @param value the collection of Externalizable objects. This value must not be null.
* @throws IOException if an error occurs.
*/
public void writeExternalizableCollection(DataOutput out, Collection<? extends Externalizable> value) throws IOException {
writeObject(out, value);
}

public void writeSerializableCollection(DataOutput out, Collection<? extends Serializable> value)
throws IOException {
SerializationHelper.writeObject(out, value);
/**
* Writes a collection of Serializable objects. The collection passed as a parameter
* must be a collection and not a <tt>null</null> value.
*
* @param out the output stream.
* @param value the collection of Serializable objects. This value must not be null.
* @throws IOException if an error occurs.
*/
public void writeSerializableCollection(DataOutput out, Collection<? extends Serializable> value) throws IOException {
writeObject(out, value);
}
public int readExternalizableCollection(DataInput in, Collection<? extends Externalizable> value,
ClassLoader loader) throws IOException {
Collection<Externalizable> result = (Collection<Externalizable>) SerializationHelper.readObject(in);

/**
* Reads a collection of Externalizable objects and adds them to the collection passed as a parameter. The
* collection passed as a parameter must be a collection and not a <tt>null</null> value.
*
* @param in the input stream.
* @param value the collection of Externalizable objects. This value must not be null.
* @param loader class loader to use to build elements inside of the serialized collection.
* @throws IOException if an error occurs.
* @return the number of elements added to the collection.
*/
public int readExternalizableCollection(DataInput in, Collection<? extends Externalizable> value, ClassLoader loader) throws IOException {
Collection<Externalizable> result = (Collection<Externalizable>) readObject(in);
if (result == null) return 0;
((Collection<Externalizable>)value).addAll(result);
return result.size();
}

public int readSerializableCollection(DataInput in, Collection<? extends Serializable> value,
ClassLoader loader) throws IOException {
Collection<Serializable> result = (Collection<Serializable>) SerializationHelper.readObject(in);
/**
* Reads a collection of Serializable objects and adds them to the collection passed as a parameter. The
* collection passed as a parameter must be a collection and not a <tt>null</null> value.
*
* @param in the input stream.
* @param value the collection of Serializable objects. This value must not be null.
* @param loader class loader to use to build elements inside of the serialized collection.
* @throws IOException if an error occurs.
* @return the number of elements added to the collection.
*/
public int readSerializableCollection(DataInput in, Collection<? extends Serializable> value, ClassLoader loader) throws IOException {
Collection<Serializable> result = (Collection<Serializable>) readObject(in);
if (result == null) return 0;
((Collection<Serializable>)value).addAll(result);
return result.size();
}
}

/**
* Writes a Map of String key and value pairs. This method handles the
* case when the Map is <tt>null</tt>.
*
* @param out the output stream.
* @param map the Map of String key and Externalizable value pairs.
* @throws java.io.IOException if an error occurs.
*/
public void writeExternalizableMap(DataOutput out, Map<String, ? extends Externalizable> map) throws IOException {
SerializationHelper.writeObject(out, map);
writeObject(out, map);
}

/**
* Writes a Map of Serializable key and value pairs. This method handles the
* case when the Map is <tt>null</tt>.
*
* @param out the output stream.
* @param map the Map of Serializable key and value pairs.
* @throws java.io.IOException if an error occurs.
*/
public void writeSerializableMap(DataOutput out, Map<? extends Serializable, ? extends Serializable> map) throws IOException {
SerializationHelper.writeObject(out, map);
writeObject(out, map);
}

/**
* Reads a Map of String key and value pairs. This method will return
* <tt>null</tt> if the Map written to the stream was <tt>null</tt>.
*
* @param in the input stream.
* @param map a Map of String key and Externalizable value pairs.
* @param loader class loader to use to build elements inside of the serialized collection.
* @throws IOException if an error occurs.
* @return the number of elements added to the collection.
*/
public int readExternalizableMap(DataInput in, Map<String, ? extends Externalizable> map, ClassLoader loader) throws IOException {
Map<String, Externalizable> result = (Map<String, Externalizable>) SerializationHelper.readObject(in);
Map<String, Externalizable> result = (Map<String, Externalizable>) readObject(in);
if (result == null) return 0;
((Map<String, Externalizable>)map).putAll(result);
return result.size();
}

/**
* Reads a Map of Serializable key and value pairs. This method will return
* <tt>null</tt> if the Map written to the stream was <tt>null</tt>.
*
* @param in the input stream.
* @param map a Map of Serializable key and value pairs.
* @param loader class loader to use to build elements inside of the serialized collection.
* @throws IOException if an error occurs.
* @return the number of elements added to the collection.
*/
public int readSerializableMap(DataInput in, Map<? extends Serializable, ? extends Serializable> map, ClassLoader loader) throws IOException {
Map<String, Serializable> result = (Map<String, Serializable>) SerializationHelper.readObject(in);
Map<String, Serializable> result = (Map<String, Serializable>) readObject(in);
if (result == null) return 0;
((Map<String, Serializable>)map).putAll(result);
return result.size();
}

public void writeStrings(DataOutput out, Collection<String> collection) throws IOException {
SerializationHelper.writeObject(out, collection);
writeObject(out, collection);
}

public int readStrings(DataInput in, Collection<String> collection) throws IOException {
Collection<String> result = (Collection<String>) SerializationHelper.readObject(in);
Collection<String> result = (Collection<String>) readObject(in);
if (result == null) return 0;
collection.addAll(result);
return result.size();
}

// serialization helpers

public static void writeObject(DataOutput out, Object obj) throws IOException {
if (obj == null) {
out.writeByte(0);
} else if (obj instanceof Long) {
out.writeByte(1);
out.writeLong((Long) obj);
} else if (obj instanceof Integer) {
out.writeByte(2);
out.writeInt((Integer) obj);
} else if (obj instanceof String) {
out.writeByte(3);
out.writeUTF((String) obj);
} else if (obj instanceof Double) {
out.writeByte(4);
out.writeDouble((Double) obj);
} else if (obj instanceof Float) {
out.writeByte(5);
out.writeFloat((Float) obj);
} else if (obj instanceof Boolean) {
out.writeByte(6);
out.writeBoolean((Boolean) obj);
} else if (obj instanceof Date) {
out.writeByte(8);
out.writeLong(((Date) obj).getTime());
} else {
out.writeByte(9);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
byte[] buf = bos.toByteArray();
out.writeInt(buf.length);
out.write(buf);
}
}

public static Object readObject(DataInput in) throws IOException {
byte type = in.readByte();
if (type == 0) {
return null;
} else if (type == 1) {
return in.readLong();
} else if (type == 2) {
return in.readInt();
} else if (type == 3) {
return in.readUTF();
} else if (type == 4) {
return in.readDouble();
} else if (type == 5) {
return in.readFloat();
} else if (type == 6) {
return in.readBoolean();
} else if (type == 8) {
return new Date(in.readLong());
} else if (type == 9) {
int len = in.readInt();
byte[] buf = new byte[len];
in.readFully(buf);
ObjectInputStream oin = newObjectInputStream(new ByteArrayInputStream(buf));
try {
return oin.readObject();
} catch (ClassNotFoundException e) {
throw new IOException(e);
} finally {
oin.close();
}
} else {
throw new IOException("Unknown object type=" + type);
}
}

public static ObjectInputStream newObjectInputStream(final InputStream in) throws IOException {
return new ObjectInputStream(in) {
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc) throws ClassNotFoundException {
return loadClass(desc.getName());
}
};
}

public static Class<?> loadClass(final String className) throws ClassNotFoundException {
return loadClass(null, className);
}

public static Class<?> loadClass(final ClassLoader classLoader, final String className) throws ClassNotFoundException {
if (className == null) {
throw new IllegalArgumentException("ClassName cannot be null!");
}
if (className.length() <= MAX_PRIM_CLASSNAME_LENGTH && Character.isLowerCase(className.charAt(0))) {
for (int i = 0; i < PRIMITIVE_CLASSES_ARRAY.length; i++) {
if (className.equals(PRIMITIVE_CLASSES_ARRAY[i].getName())) {
return PRIMITIVE_CLASSES_ARRAY[i];
}
}
}
ClassLoader theClassLoader = classLoader;
if (className.startsWith("com.hazelcast.") || className.startsWith("[Lcom.hazelcast.")) {
theClassLoader = HazelcastInstance.class.getClassLoader();
}
if (theClassLoader == null) {
theClassLoader = Thread.currentThread().getContextClassLoader();
}
if (theClassLoader != null) {
if (className.startsWith("[")) {
return Class.forName(className, true, theClassLoader);
} else {
return theClassLoader.loadClass(className);
}
}
return Class.forName(className);
}

private static final Class[] PRIMITIVE_CLASSES_ARRAY = {int.class, long.class, boolean.class, byte.class,
float.class, double.class, byte.class, char.class, short.class, void.class};
private static final int MAX_PRIM_CLASSNAME_LENGTH = 7; // boolean.class.getName().length();


}
@@ -54,6 +54,7 @@

import com.hazelcast.core.Cluster;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.EntryEventType;
import com.hazelcast.core.EntryListener;
import com.hazelcast.core.LifecycleEvent;
import com.hazelcast.core.LifecycleEvent.LifecycleState;
@@ -138,18 +139,6 @@ public ClusterListener(Cluster cluster) {

directedPresencesCache = CacheFactory.createCache(PresenceUpdateHandler.PRESENCE_CACHE_NAME);

addEntryListener(C2SCache, new CacheListener(this, C2SCache.getName()));
addEntryListener(anonymousC2SCache, new CacheListener(this, anonymousC2SCache.getName()));
addEntryListener(S2SCache, new CacheListener(this, S2SCache.getName()));
addEntryListener(componentsCache, new ComponentCacheListener());

addEntryListener(sessionInfoCache, new CacheListener(this, sessionInfoCache.getName()));
addEntryListener(componentSessionsCache, new CacheListener(this, componentSessionsCache.getName()));
addEntryListener(multiplexerSessionsCache, new CacheListener(this, multiplexerSessionsCache.getName()));
addEntryListener(incomingServerSessionsCache, new CacheListener(this, incomingServerSessionsCache.getName()));

addEntryListener(directedPresencesCache, new DirectedPresenceListener());

joinCluster();
}

@@ -173,7 +162,7 @@ private void simulateCacheInserts(Cache cache) {
ClusteredCache clusteredCache = (ClusteredCache) wrapped;
for (Map.Entry entry : (Set<Map.Entry>) cache.entrySet()) {
EntryEvent event = new EntryEvent(clusteredCache.map.getName(), cluster.getLocalMember(),
EntryEvent.TYPE_ADDED, entry.getKey(), null, entry.getValue());
EntryEventType.ADDED.getType(), entry.getKey(), null, entry.getValue());
EntryListener.entryAdded(event);
}
}
@@ -468,7 +457,7 @@ public void entryUpdated(EntryEvent event) {
}

public void entryRemoved(EntryEvent event) {
if (event.getValue() == null && ((Collection)event.getOldValue()).isEmpty()) {
if (event == null || (event.getValue() == null && event.getOldValue() == null)) {
// Nothing to remove
return;
}
@@ -555,6 +544,20 @@ private synchronized void joinCluster() {
if (!isDone()) { // already joined
return;
}
// Trigger events
ClusterManager.fireJoinedCluster(false);
addEntryListener(C2SCache, new CacheListener(this, C2SCache.getName()));
addEntryListener(anonymousC2SCache, new CacheListener(this, anonymousC2SCache.getName()));
addEntryListener(S2SCache, new CacheListener(this, S2SCache.getName()));
addEntryListener(componentsCache, new ComponentCacheListener());

addEntryListener(sessionInfoCache, new CacheListener(this, sessionInfoCache.getName()));
addEntryListener(componentSessionsCache, new CacheListener(this, componentSessionsCache.getName()));
addEntryListener(multiplexerSessionsCache, new CacheListener(this, multiplexerSessionsCache.getName()));
addEntryListener(incomingServerSessionsCache, new CacheListener(this, incomingServerSessionsCache.getName()));

addEntryListener(directedPresencesCache, new DirectedPresenceListener());

// Simulate insert events of existing cache content
simulateCacheInserts(C2SCache);
simulateCacheInserts(anonymousC2SCache);
@@ -566,8 +569,7 @@ private synchronized void joinCluster() {
simulateCacheInserts(incomingServerSessionsCache);
simulateCacheInserts(directedPresencesCache);

// Trigger events
ClusterManager.fireJoinedCluster(false);

if (CacheFactory.isSeniorClusterMember()) {
seniorClusterMember = true;
ClusterManager.fireMarkedAsSeniorClusterMember();
@@ -19,6 +19,7 @@
package com.jivesoftware.util.cache;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -39,6 +40,8 @@
public class ClusteredCache implements Cache {

private static Logger logger = LoggerFactory.getLogger(ClusteredCache.class);

private final Map<EntryListener, String> registrations = new HashMap<EntryListener, String>();

/**
* The map is used for distributed operations such as get, put, etc.
@@ -59,11 +62,14 @@ protected ClusteredCache(String name, IMap cache) {
}

public void addEntryListener(EntryListener listener, boolean includeValue) {
map.addEntryListener(listener, includeValue);
registrations.put(listener, map.addEntryListener(listener, includeValue));
}

public void removeEntryListener(EntryListener listener) {
map.removeEntryListener(listener);
String registrationId = registrations.get(listener);
if (registrationId != null) {
map.removeEntryListener(registrationId);
}
}

// Cache Interface
@@ -168,7 +174,12 @@ public boolean lock(Object key, long timeout) {
} else if (timeout == 0) {
result = map.tryLock(key);
} else {
result = map.tryLock(key, timeout, TimeUnit.MILLISECONDS);
try {
result = map.tryLock(key, timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
logger.error("Failed to get cluster lock", e);
result = false;
}
}
return result;
}
@@ -20,6 +20,7 @@
package com.jivesoftware.util.cache;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -29,6 +30,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;
@@ -52,11 +54,9 @@
import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.config.Config;
import com.hazelcast.core.Cluster;
import com.hazelcast.core.DistributedTask;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.Member;
import com.hazelcast.core.MultiTask;
import com.jivesoftware.openfire.session.RemoteSessionLocator;
import com.jivesoftware.util.cluster.ClusterPacketRouter;
import com.jivesoftware.util.cluster.HazelcastClusterNodeInfo;
@@ -69,6 +69,8 @@
*/
public class ClusteredCacheFactory implements CacheFactoryStrategy {

public static final String HAZELCAST_EXECUTOR_SERVICE_NAME =
JiveGlobals.getProperty("hazelcast.executor.service.name", "openfire::cluster::executor");
private static final long MAX_CLUSTER_EXECUTION_TIME =
JiveGlobals.getLongProperty("hazelcast.max.execution.seconds", 30);
private static final long CLUSTER_STARTUP_RETRY_TIME =
@@ -276,8 +278,8 @@ public void doClusterTask(final ClusterTask task) {
if (members.size() > 0) {
// Asynchronously execute the task on the other cluster members
logger.debug("Executing asynchronous MultiTask: " + task.getClass().getName());
hazelcast.getExecutorService().execute(
new MultiTask<Object>(new CallableTask<Object>(task), members));
hazelcast.getExecutorService(HAZELCAST_EXECUTOR_SERVICE_NAME).submitToMembers(
new CallableTask<Object>(task), members);
} else {
logger.warn("No cluster members selected for cluster task " + task.getClass().getName());
}
@@ -295,8 +297,8 @@ public boolean doClusterTask(final ClusterTask task, byte[] nodeID) {
if (member != null) {
// Asynchronously execute the task on the target member
logger.debug("Executing asynchronous DistributedTask: " + task.getClass().getName());
hazelcast.getExecutorService().execute(
new DistributedTask<Object>(new CallableTask<Object>(task), member));
hazelcast.getExecutorService(HAZELCAST_EXECUTOR_SERVICE_NAME).submitToMember(
new CallableTask<Object>(task), member);
return true;
} else {
logger.warn("Requested node " + StringUtils.getString(nodeID) + " not found in cluster");
@@ -310,24 +312,27 @@ public boolean doClusterTask(final ClusterTask task, byte[] nodeID) {
* (seconds) until the task is run on all members.
*/
public Collection<Object> doSynchronousClusterTask(ClusterTask task, boolean includeLocalMember) {
Collection<Object> result = Collections.emptyList();
if (cluster == null) { return result; }
if (cluster == null) { return Collections.emptyList(); }
Set<Member> members = new HashSet<Member>();
Member current = cluster.getLocalMember();
for(Member member : cluster.getMembers()) {
if (includeLocalMember || (!member.getUuid().equals(current.getUuid()))) {
members.add(member);
}
}
Collection<Object> result = new ArrayList<Object>();
if (members.size() > 0) {
// Asynchronously execute the task on the other cluster members
MultiTask<Object> multiTask = new MultiTask<Object>(
new CallableTask<Object>(task), members);
try {
logger.debug("Executing MultiTask: " + task.getClass().getName());
hazelcast.getExecutorService().execute(multiTask);
result = multiTask.get(MAX_CLUSTER_EXECUTION_TIME,TimeUnit.SECONDS);
logger.debug("MultiTask result: " + (result == null ? "null" : result.size()));
Map<Member, Future<Object>> futures = hazelcast.getExecutorService(HAZELCAST_EXECUTOR_SERVICE_NAME)
.submitToMembers(new CallableTask<Object>(task), members);
long nanosLeft = TimeUnit.SECONDS.toNanos(MAX_CLUSTER_EXECUTION_TIME);
for (Future<Object> future : futures.values()) {
long start = System.nanoTime();
result.add(future.get(nanosLeft, TimeUnit.NANOSECONDS));
nanosLeft = (System.nanoTime() - start);
}
} catch (TimeoutException te) {
logger.error("Failed to execute cluster task within " + MAX_CLUSTER_EXECUTION_TIME + " seconds", te);
} catch (Exception e) {
@@ -351,12 +356,11 @@ public Object doSynchronousClusterTask(ClusterTask task, byte[] nodeID) {
// Check that the requested member was found
if (member != null) {
// Asynchronously execute the task on the target member
DistributedTask<Object> distributedTask = new DistributedTask<Object>(
new CallableTask<Object>(task), member);
logger.debug("Executing DistributedTask: " + task.getClass().getName());
hazelcast.getExecutorService().execute(distributedTask);
try {
result = distributedTask.get(MAX_CLUSTER_EXECUTION_TIME, TimeUnit.SECONDS);
Future<Object> future = hazelcast.getExecutorService(HAZELCAST_EXECUTOR_SERVICE_NAME)
.submitToMember(new CallableTask<Object>(task), member);
result = future.get(MAX_CLUSTER_EXECUTION_TIME, TimeUnit.SECONDS);
logger.debug("DistributedTask result: " + (result == null ? "null" : result));
} catch (TimeoutException te) {
logger.error("Failed to execute cluster task within " + MAX_CLUSTER_EXECUTION_TIME + " seconds", te);
@@ -39,11 +39,52 @@
</style>
</head>
<body>

<h1>
Jitsi Video Bridge Plugin Changelog
</h1>

<p><b>1.3.0</b> -- March 14th, 2014</p>

<ul>
<li>OF-749: Upgraded bouncy castle libraries from 1.49 to 1.50</li>
<li>Jisti Videobridge plugin: Added latest jitmeet web conference application</li>
<li>Jisti Videobridge plugin: Implememted SIP registration, incoming and outgoing calls</li>
<li>Jisti Videobridge plugin: Added new rayo commands "invite"and "uninvite". Added events "inviteaccepted" and "invitecompleted"</li>
<li>Jisti Videobridge plugin: Implememted media recording</li>
<li>Ofmeet: Implememted feature to invite telphone user to conference</li>
<li>Ofmeet: Implememted feature to accept incoming telphone user to conference</li>
<li>Ofmeet: Implememted feature to record and save conference audio as a .au file</li>
<li>Ofmeet: Implememted feature to record and save conference video as a .webm file (windows only)</li>
</ul>

<p><b>1.2.2</b> -- Feb 18th, 2014</p>

<ul>
<li>Added username/password protection on web applications</li>
</ul>

<p><b>1.2.1</b> -- Feb 17th, 2014</p>

<ul>
<li>Updated Spark plugin and added source code to project files</li>
</ul>

<p><b>1.2</b> -- Feb 15th, 2014</p>

<ul>
<li>Added latest jitmeet web conference application</li>
<li>Restructured web apps folder</li>
<li>Added logging of XMPP messages in web browser console</li>
<li>Fixed warining message about channle expiry</li>
<li>jitsivideobridge: Updated to https://github.com/jitsi/jitsi-videobridge/commit/a63c8c0dd901e1f9a6f164264338d3a4a89e97fd</li>
</ul>

<p><b>1.1.1</b> -- Feb 9th, 2014</p>

<ul>
<li>Added support for PDF Presentations</li>
</ul>

<p><b>1.1</b> -- Jan 27th, 2014</p>

<ul>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,10 +3,10 @@
<author>jitsi.org and igniterealtime.org</author>
<class>org.jitsi.videobridge.openfire.PluginImpl</class>
<description>Integrates Jitsi Video Bridge into Openfire.</description>
<licenseType>other</licenseType>
<minServerVersion>3.9.0</minServerVersion>
<licenseType>LGPL</licenseType>
<minServerVersion>3.9.2</minServerVersion>
<name>Jitsi Video Bridge</name>
<version>1.1</version>
<version>1.3.0</version>

<adminconsole>
<tab id="tab-server">
@@ -19,5 +19,4 @@
</sidebar>
</tab>
</adminconsole>

</plugin>
@@ -62,26 +62,47 @@ <h2>Overview</h2>
Jitsi Videobridge does not mix the video channels into a composite video stream,
but only relays the received video channels to all call participants.
Therefore, while it does need to run on a server with good network bandwidth,
CPU horsepower is not that critical for performance.
It includes the OfMeet video conference application using WebRTC as well as a simple Spark plugin.

CPU horsepower is not that critical for performance. It also includes some useful web conference applications.
</p>

<h2>Known Issues</h2>

<p>
The first video conference creating after restarting Openfire may fail. Work around is remove all participants from the room and try again or use a new room.
</p>
<h2>Installation</h2>

<p>Copy jitsivideobridge.jar into the plugins directory of your Openfire server. The
plugin will then be automatically deployed. To upgrade to a new version, copy the new
jitsiVideobridge.jar file over the existing file.</p>
<ol>
<li>Copy the jitsivideobridge.jar file to the OPENFIRE_HOME/plugins directory.</li>
<li>Restart Openfire.</li>
<li>Configure the admin properties page.</li>
</ol>

<h2>Configuration</h2>

Under Server settings -> Jitsi Videobridge tab you can configure it.
Under Server settings -> Jitsi Videobridge tab you can configure various parameters.

<h2>How to use</h2>

To run the demo video conference application, point your browser at https://your_server:7443/ofmeet
<p>
Make sure:
<ul>
<li>you are using Google Chrome as your web browser</li>
<li>you have a webcam installed and ready for use for each user</li>
<li>you have opened ports 50000 - 60000 (or whatever you configured) on your openfire server</li>
</ul>
</p>
<h2>OfMeet</h2>
To run the ofmeet video conference application, point your browser at https://your_server:7443/jitsi/apps/ofmeet
<p/>
<h2>Candy</h2>
To run the Candy web application with multi-user video, point your browser at https://your_server:7443/jitsi/apps/candy
<p/>
<h2>Spark</h2>
To download the Spark plugin, point your browser at https://your_server:7443/jitsi/apps/spark/jitsivideobridge-plugin.jar
<p/>
To download the Spark plugin, point your browser at https://your_server:7443/ofmeet/jitsivideobridge-plugin.jar
<h2>JitMeet</h2>
To run the jitmeet video conference application, point your browser at https://your_server:7443/jitsi/apps/jitmeet
<p/>
You will need Google Chrome as your default browser to use the Spark plugin
</body>
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app>


<servlet>
<servlet-name>proxy</servlet-name>
<servlet-class>org.jitsi.videobridge.openfire.HttpProxy</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>proxy</servlet-name>
<url-pattern>/proxy</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>config</servlet-name>
<servlet-class>org.jitsi.videobridge.openfire.Config</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>config</servlet-name>
<url-pattern>/config</url-pattern>
</servlet-mapping>
</web-app>
@@ -0,0 +1,8 @@
<html>
<head><title></title>
<meta http-equiv="refresh" content="0;URL=example/index.html">
</head>
<body>
</body>
</html>

@@ -107,8 +107,8 @@ CandyShop.Videobridge = (function(self, Candy, $) {
$(this).removeClass('active');

} else {
var url = "../../publish.html?r=" + videobridge + "&screen=true";
//var url = "/ofmeet/publish.html?r=" + videobridge + "&screen=true";
//var url = "../../publish.html?r=" + videobridge + "&screen=true";
var url = "/jitsi/apps/ofmeet/publish.html?r=" + videobridge + "&screen=true";

$("body").append("<div id='screenshare'><iframe style='display:none' src='" + url + "'></iframe></div>");
$("#screen").addClass("fa-border");
@@ -66,7 +66,7 @@ function onMessage(message)
if (xmlns == "jabber:x:conference")
{
var roomJid = $(this).attr("jid");
window.location.href = "/ofmeet/?r=" + Strophe.getNodeFromJid(roomJid) + "&n=" + userForm.username + "&q=" + userForm.question;
window.location.href = "/jitsi/apps/ofmeet/?r=" + Strophe.getNodeFromJid(roomJid) + "&n=" + userForm.username + "&q=" + userForm.question;
}
});
return true;
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Jappix - Forbidden</title>
</head>

<body>
<h1>Forbidden</h1>
<h4>This is a private folder</h4>
</body>

</html>
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,5 @@
jitmeet - a colibri.js sample application
====
A WebRTC-powered multi-user videochat. For a live demo, check out either https://meet.estos.de/ or https://meet.jit.si/.

Built using [colibri.js](https://github.com/ESTOS/colibri.js) and [strophe.jingle](https://github.com/ESTOS/strophe.jingle), powered by the [jitsi-videobridge](https://github.com/jitsi/jitsi-videobridge)
@@ -0,0 +1,1691 @@
/* jshint -W117 */
/* application specific logic */
var connection = null;
var focus = null;
var activecall = null;
var RTC = null;
var nickname = null;
var sharedKey = '';
var roomUrl = null;
var ssrc2jid = {};

/**
* Indicates whether ssrc is camera video or desktop stream.
* FIXME: remove those maps
*/
var ssrc2videoType = {};
var videoSrcToSsrc = {};

var localVideoSrc = null;
var flipXLocalVideo = true;
var isFullScreen = false;
var toolbarTimeout = null;
var currentVideoWidth = null;
var currentVideoHeight = null;
/**
* Method used to calculate large video size.
* @type {function()}
*/
var getVideoSize;
/**
* Method used to get large video position.
* @type {function()}
*/
var getVideoPosition;

/* window.onbeforeunload = closePageWarning; */

function init() {
RTC = setupRTC();
if (RTC === null) {
window.location.href = 'webrtcrequired.html';
return;
} else if (RTC.browser !== 'chrome') {
window.location.href = 'chromeonly.html';
return;
}

connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');

if (nickname) {
connection.emuc.addDisplayNameToPresence(nickname);
}

if (connection.disco) {
// for chrome, add multistream cap
}
connection.jingle.pc_constraints = RTC.pc_constraints;
if (config.useIPv6) {
// https://code.google.com/p/webrtc/issues/detail?id=2828
if (!connection.jingle.pc_constraints.optional) connection.jingle.pc_constraints.optional = [];
connection.jingle.pc_constraints.optional.push({googIPv6: true});
}

var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;

connection.connect(jid, document.getElementById('password').value, function (status) {
if (status === Strophe.Status.CONNECTED) {
console.log('connected');
if (config.useStunTurn) {
connection.jingle.getStunAndTurnCredentials();
}
obtainAudioAndVideoPermissions(function(){
getUserMediaWithConstraints( ['audio'], audioStreamReady,
function(error){
console.error('failed to obtain audio stream - stop', error);
});
});

document.getElementById('connect').disabled = true;
} else {
console.log('status', status);
}
});
}

/**
* HTTPS only:
* We first ask for audio and video combined stream in order to get permissions and not to ask twice.
* Then we dispose the stream and continue with separate audio, video streams(required for desktop sharing).
*/
function obtainAudioAndVideoPermissions(callback){
// This makes sense only on https sites otherwise we'll be asked for permissions every time
if(location.protocol !== 'https:') {
callback();
return;
}
// Get AV
getUserMediaWithConstraints(
['audio', 'video'],
function(avStream) {
avStream.stop();
callback();
},
function(error){
console.error('failed to obtain audio/video stream - stop', error);
});
}

function audioStreamReady(stream) {

change_local_audio(stream);

if(RTC.browser !== 'firefox') {
getUserMediaWithConstraints( ['video'], videoStreamReady, videoStreamFailed, config.resolution || '360' );
} else {
doJoin();
}
}

function videoStreamReady(stream) {

change_local_video(stream, true);

doJoin();
}

function videoStreamFailed(error) {

console.warn("Failed to obtain video stream - continue anyway", error);

doJoin();
}

function doJoin() {
var roomnode = null;
var path = window.location.pathname;
var roomjid;

// determinde the room node from the url
// TODO: just the roomnode or the whole bare jid?
if (config.getroomnode && typeof config.getroomnode === 'function') {
// custom function might be responsible for doing the pushstate
roomnode = config.getroomnode(path);
} else {
/* fall back to default strategy
* this is making assumptions about how the URL->room mapping happens.
* It currently assumes deployment at root, with a rewrite like the
* following one (for nginx):
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
*/
if (path.length > 1) {
roomnode = path.substr(1).toLowerCase();
} else {
roomnode = Math.random().toString(36).substr(2, 20);
window.history.pushState('VideoChat',
'Room: ' + roomnode, window.location.pathname + roomnode);
}
}

roomjid = roomnode + '@' + config.hosts.muc;

if (config.useNicks) {
var nick = window.prompt('Your nickname (optional)');
if (nick) {
roomjid += '/' + nick;
} else {
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
}
} else {
roomjid += '/' + Strophe.getNodeFromJid(connection.jid).substr(0,8);
}
connection.emuc.doJoin(roomjid);
}

function change_local_audio(stream) {
connection.jingle.localAudio = stream;
RTC.attachMediaStream($('#localAudio'), stream);
document.getElementById('localAudio').autoplay = true;
document.getElementById('localAudio').volume = 0;
}

function change_local_video(stream, flipX) {

connection.jingle.localVideo = stream;

var localVideo = document.createElement('video');
localVideo.id = 'localVideo_'+stream.id;
localVideo.autoplay = true;
localVideo.volume = 0; // is it required if audio is separated ?
localVideo.oncontextmenu = function () { return false; };

var localVideoContainer = document.getElementById('localVideoWrapper');
localVideoContainer.appendChild(localVideo);

var localVideoSelector = $('#' + localVideo.id);
// Add click handler
localVideoSelector.click(function () { handleVideoThumbClicked(localVideo.src); } );
// Add stream ended handler
stream.onended = function () {
localVideoContainer.removeChild(localVideo);
checkChangeLargeVideo(localVideo.src);
};
// Flip video x axis if needed
flipXLocalVideo = flipX;
if(flipX) {
localVideoSelector.addClass("flipVideoX");
}
// Attach WebRTC stream
RTC.attachMediaStream(localVideoSelector, stream);

localVideoSrc = localVideo.src;
updateLargeVideo(localVideoSrc, 0);
}

$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
function waitForRemoteVideo(selector, sid, ssrc) {
if(selector.removed) {
console.warn("media removed before had started", selector);
return;
}
var sess = connection.jingle.sessions[sid];
if (data.stream.id === 'mixedmslabel') return;
var videoTracks = data.stream.getVideoTracks();
// console.log("waiting..", videoTracks, selector[0]);

if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?

// FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
// in order to get rid of too many maps
if(ssrc) {
videoSrcToSsrc[sel.attr('src')] = ssrc;
} else {
console.warn("No ssrc given for video", sel);
}

$(document).trigger('callactive.jingle', [selector, sid]);
console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
} else {
setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250);
}
}
var sess = connection.jingle.sessions[sid];

var thessrc;
// look up an associated JID for a stream id
if (data.stream.id.indexOf('mixedmslabel') === -1) {
var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
ssrclines = ssrclines.filter(function (line) {
return line.indexOf('mslabel:' + data.stream.label) !== -1;
});
if (ssrclines.length) {
thessrc = ssrclines[0].substring(7).split(' ')[0];
// ok to overwrite the one from focus? might save work in colibri.js
console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
if (ssrc2jid[thessrc]) {
data.peerjid = ssrc2jid[thessrc];
}
}
}

var container;
var remotes = document.getElementById('remoteVideos');

if (data.peerjid) {
container = document.getElementById(
'participant_' + Strophe.getResourceFromJid(data.peerjid));
if (!container) {
console.error('no container for', data.peerjid);
} else {
//console.log('found container for', data.peerjid);
}
} else {
if (data.stream.id !== 'mixedmslabel') {
console.error('can not associate stream', data.stream.id, 'with a participant');
// We don't want to add it here since it will cause troubles
return;
}
// FIXME: for the mixed ms we dont need a video -- currently
container = document.createElement('span');
container.className = 'videocontainer';
remotes.appendChild(container);
Util.playSoundNotification('userJoined');
}

var isVideo = data.stream.getVideoTracks().length > 0;
var vid = isVideo ? document.createElement('video') : document.createElement('audio');
var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id;

vid.id = id;
vid.autoplay = true;
vid.oncontextmenu = function () { return false; };

container.appendChild(vid);

// TODO: make mixedstream display:none via css?
if (id.indexOf('mixedmslabel') !== -1) {
container.id = 'mixedstream';
$(container).hide();
}

var sel = $('#' + id);
sel.hide();
RTC.attachMediaStream(sel, data.stream);

if(isVideo) {
waitForRemoteVideo(sel, sid, thessrc);
}

data.stream.onended = function () {
console.log('stream ended', this.id);

// Mark video as removed to cancel waiting loop(if video is removed before has started)
sel.removed = true;
sel.remove();

var audioCount = $('#'+container.id+'>audio').length;
var videoCount = $('#'+container.id+'>video').length;
if(!audioCount && !videoCount) {
console.log("Remove whole user");
// Remove whole container
container.remove();
Util.playSoundNotification('userLeft');
resizeThumbnails();
}

checkChangeLargeVideo(vid.src);
};

// Add click handler
sel.click(function () { handleVideoThumbClicked(vid.src); });

// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
if (isVideo
&& data.peerjid && sess.peerjid === data.peerjid &&
data.stream.getVideoTracks().length === 0 &&
connection.jingle.localVideo.getVideoTracks().length > 0) {
window.setTimeout(function() {
sendKeyframe(sess.peerconnection);
}, 3000);
}
});

function handleVideoThumbClicked(videoSrc) {

$(document).trigger("video.selected", [false]);

updateLargeVideo(videoSrc, 1);

$('audio').each(function (idx, el) {
if (el.id.indexOf('mixedmslabel') !== -1) {
el.volume = 0;
el.volume = 1;
}
});
}

/**
* Checks if removed video is currently displayed and tries to display another one instead.
* @param removedVideoSrc src stream identifier of the video.
*/
function checkChangeLargeVideo(removedVideoSrc){
if (removedVideoSrc === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);

if(!pick) {
console.info("Last visible video no longer exists");
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
if(!pick) {
// Try local video
console.info("Fallback to local video...");
pick = $('#remoteVideos>span>span>video').get(0);
}
}

// mute if localvideo
if (pick) {
updateLargeVideo(pick.src, pick.volume);
} else {
console.warn("Failed to elect large video");
}
}
}

// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
function sendKeyframe(pc) {
console.log('sendkeyframe', pc.iceConnectionState);
if (pc.iceConnectionState !== 'connected') return; // safe...
pc.setRemoteDescription(
pc.remoteDescription,
function () {
pc.createAnswer(
function (modifiedAnswer) {
pc.setLocalDescription(modifiedAnswer,
function () {
},
function (error) {
console.log('triggerKeyframe setLocalDescription failed', error);
}
);
},
function (error) {
console.log('triggerKeyframe createAnswer failed', error);
}
);
},
function (error) {
console.log('triggerKeyframe setRemoteDescription failed', error);
}
);
}

// really mute video, i.e. dont even send black frames
function muteVideo(pc, unmute) {
// FIXME: this probably needs another of those lovely state safeguards...
// which checks for iceconn == connected and sigstate == stable
pc.setRemoteDescription(pc.remoteDescription,
function () {
pc.createAnswer(
function (answer) {
var sdp = new SDP(answer.sdp);
if (sdp.media.length > 1) {
if (unmute)
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
else
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
sdp.raw = sdp.session + sdp.media.join('');
answer.sdp = sdp.raw;
}
pc.setLocalDescription(answer,
function () {
console.log('mute SLD ok');
},
function(error) {
console.log('mute SLD error');
}
);
},
function (error) {
console.log(error);
}
);
},
function (error) {
console.log('muteVideo SRD error');
}
);
}

$(document).bind('callincoming.jingle', function (event, sid) {
var sess = connection.jingle.sessions[sid];

// TODO: do we check activecall == null?
activecall = sess;

// TODO: check affiliation and/or role
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
sess.usedrip = true; // not-so-naive trickle ice
sess.sendAnswer();
sess.accept();

});

$(document).bind('callactive.jingle', function (event, videoelem, sid) {
if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
// ignore mixedmslabela0 and v0
videoelem.show();
resizeThumbnails();

updateLargeVideo(videoelem.attr('src'), 1);

showFocusIndicator();
}
});

$(document).bind('callterminated.jingle', function (event, sid, reason) {
// FIXME
});

$(document).bind('setLocalDescription.jingle', function (event, sid) {
// put our ssrcs into presence so other clients can identify our stream
var sess = connection.jingle.sessions[sid];
var newssrcs = {};
var directions = {};
var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media) {
var type = SDPUtil.parse_mline(media.split('\r\n')[0]).media;

if (SDPUtil.find_line(media, 'a=ssrc:')) {
// assumes a single local ssrc
var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0];
newssrcs[type] = ssrc;

directions[type] = (
SDPUtil.find_line(media, 'a=sendrecv')
|| SDPUtil.find_line(media, 'a=recvonly')
|| SDPUtil.find_line('a=sendonly')
|| SDPUtil.find_line('a=inactive')
|| 'a=sendrecv' ).substr(2);
}
});
console.log('new ssrcs', newssrcs);

// Have to clear presence map to get rid of removed streams
connection.emuc.clearPresenceMedia();
var i = 0;
Object.keys(newssrcs).forEach(function (mtype) {
i++;
var type = mtype;
// Change video type to screen
if(mtype === 'video' && isUsingScreenStream) {
type = 'screen';
}
connection.emuc.addMediaToPresence(i, type, newssrcs[mtype], directions[mtype]);
});
if (i > 0) {
connection.emuc.sendPresence();
}
});

$(document).bind('joined.muc', function (event, jid, info) {
updateRoomUrl(window.location.href);
document.getElementById('localNick').appendChild(
document.createTextNode(Strophe.getResourceFromJid(jid) + ' (me)')
);

if (Object.keys(connection.emuc.members).length < 1) {
focus = new ColibriFocus(connection, config.hosts.bridge);
}

if (focus && config.etherpad_base) {
Etherpad.init();
}

showFocusIndicator();

// Once we've joined the muc show the toolbar
showToolbar();

var displayName = '';
if (info.displayName)
displayName = info.displayName + ' (me)';

showDisplayName('localVideoContainer', displayName);
});

$(document).bind('entered.muc', function (event, jid, info, pres) {
console.log('entered', jid, info);
console.log('is focus?' + focus ? 'true' : 'false');

// Add Peer's container
ensurePeerContainerExists(jid);

if (focus !== null) {
// FIXME: this should prepare the video
if (focus.confid === null) {
console.log('make new conference with', jid);
focus.makeConference(Object.keys(connection.emuc.members));
} else {
console.log('invite', jid, 'into conference');
focus.addNewParticipant(jid);
}
}
else if (sharedKey) {
updateLockButton();
}
});

$(document).bind('left.muc', function (event, jid) {
console.log('left', jid);
connection.jingle.terminateByJid(jid);
var container = document.getElementById('participant_' + Strophe.getResourceFromJid(jid));
if (container) {
// hide here, wait for video to close before removing
$(container).hide();
resizeThumbnails();
}
if (focus === null && connection.emuc.myroomjid === connection.emuc.list_members[0]) {
console.log('welcome to our new focus... myself');
focus = new ColibriFocus(connection, config.hosts.bridge);
if (Object.keys(connection.emuc.members).length > 0) {
focus.makeConference(Object.keys(connection.emuc.members));
}
$(document).trigger('focusechanged.muc', [focus]);
}
else if (focus && Object.keys(connection.emuc.members).length === 0) {
console.log('everyone left');
// FIXME: closing the connection is a hack to avoid some
// problemswith reinit
disposeConference();
focus = new ColibriFocus(connection, config.hosts.bridge);
}
if (connection.emuc.getPrezi(jid)) {
$(document).trigger('presentationremoved.muc', [jid, connection.emuc.getPrezi(jid)]);
}
});

$(document).bind('presence.muc', function (event, jid, info, pres) {

// Remove old ssrcs coming from the jid
Object.keys(ssrc2jid).forEach(function(ssrc){
if(ssrc2jid[ssrc] == jid){
delete ssrc2jid[ssrc];
}
if(ssrc2videoType == jid){
delete ssrc2videoType[ssrc];
}
});

$(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
//console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
var ssrcV = ssrc.getAttribute('ssrc');
ssrc2jid[ssrcV] = jid;

var type = ssrc.getAttribute('type');
ssrc2videoType[ssrcV] = type;

// might need to update the direction if participant just went from sendrecv to recvonly
if (type === 'video' || type === 'screen') {
var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
switch(ssrc.getAttribute('direction')) {
case 'sendrecv':
el.show();
break;
case 'recvonly':
el.hide();
// FIXME: Check if we have to change large video
//checkChangeLargeVideo(el);
break;
}
}
});

if (info.displayName) {
if (jid === connection.emuc.myroomjid) {
showDisplayName('localVideoContainer', info.displayName + ' (me)');
} else {
ensurePeerContainerExists(jid);
showDisplayName('participant_' + Strophe.getResourceFromJid(jid), info.displayName);
}
}
});

$(document).bind('passwordrequired.muc', function (event, jid) {
console.log('on password required', jid);

$.prompt('<h2>Password required</h2>' +
'<input id="lockKey" type="text" placeholder="shared key" autofocus>',
{
persistent: true,
buttons: { "Ok": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('lockKey').focus();
},
submit: function(e,v,m,f){
if(v)
{
var lockKey = document.getElementById('lockKey');

if (lockKey.value !== null)
{
setSharedKey(lockKey.value);
connection.emuc.doJoin(jid, lockKey.value);
}
}
}
});
});

$(document).bind('audiomuted.muc', function (event, jid, isMuted) {
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
videoSpanId = 'localVideoContainer';
} else {
ensurePeerContainerExists(jid);
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
}

if (videoSpanId)
showAudioIndicator(videoSpanId, isMuted);
});

$(document).bind('videomuted.muc', function (event, jid, isMuted) {
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
videoSpanId = 'localVideoContainer';
} else {
ensurePeerContainerExists(jid);
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
}

if (videoSpanId)
showAudioIndicator(videoSpanId, isMuted);
});

/**
* Updates the large video with the given new video source.
*/
function updateLargeVideo(newSrc, vol) {
console.log('hover in', newSrc);

if ($('#largeVideo').attr('src') != newSrc) {

var isVisible = $('#largeVideo').is(':visible');

$('#largeVideo').fadeOut(300, function () {
$(this).attr('src', newSrc);

// Screen stream is already rotated
var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;

var videoTransform = document.getElementById('largeVideo').style.webkitTransform;
if (flipX && videoTransform !== 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)";
}
else if (!flipX && videoTransform === 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "none";
}

// Change the way we'll be measuring and positioning large video
var isDesktop = isVideoSrcDesktop(newSrc);
getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
getVideoPosition = isDesktop ? getDesktopVideoPosition : getCameraVideoPosition;

if (isVisible)
$(this).fadeIn(300);
});
}
}

/**
* Checks if video identified by given src is desktop stream.
* @param videoSrc eg. blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395
* @returns {boolean}
*/
function isVideoSrcDesktop(videoSrc){
// FIXME: fix this mapping mess...
// figure out if large video is desktop stream or just a camera
var isDesktop = false;
if(localVideoSrc === videoSrc) {
// local video
isDesktop = isUsingScreenStream;
} else {
// Do we have associations...
var videoSsrc = videoSrcToSsrc[videoSrc];
if(videoSsrc) {
var videoType = ssrc2videoType[videoSsrc];
if(videoType) {
// Finally there...
isDesktop = videoType === 'screen';
} else {
console.error("No video type for ssrc: " + videoSsrc);
}
} else {
console.error("No ssrc for src: " + videoSrc);
}
}
return isDesktop;
}

/**
* Shows/hides the large video.
*/
function setLargeVideoVisible(isVisible) {
if (isVisible) {
$('#largeVideo').css({visibility:'visible'});
$('.watermark').css({visibility:'visible'});
}
else {
$('#largeVideo').css({visibility:'hidden'});
$('.watermark').css({visibility:'hidden'});
}
}

function getConferenceHandler() {
return focus ? focus : activecall;
}

function toggleVideo() {
if (!(connection && connection.jingle.localVideo))
return;

var sess = getConferenceHandler();
if (sess) {
sess.toggleVideoMute(
function(isMuted){
if(isMuted) {
$('#video').removeClass("icon-camera");
$('#video').addClass("icon-camera icon-camera-disabled");
} else {
$('#video').removeClass("icon-camera icon-camera-disabled");
$('#video').addClass("icon-camera");
}
}
);
}

var sess = focus || activecall;
if (!sess) {
return;
}

sess.pendingop = ismuted ? 'unmute' : 'mute';
// connection.emuc.addVideoInfoToPresence(!ismuted);
// connection.emuc.sendPresence();

sess.modifySources();
}

/**
* Mutes / unmutes audio for the local participant.
*/
function toggleAudio() {
if (!(connection && connection.jingle.localAudio))
return;
var localAudio = connection.jingle.localAudio;
for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) {
var audioEnabled = localAudio.getAudioTracks()[idx].enabled;

localAudio.getAudioTracks()[idx].enabled = !audioEnabled;
connection.emuc.addAudioInfoToPresence(audioEnabled); //isMuted is the opposite of audioEnabled
connection.emuc.sendPresence();
}
}

/**
* Positions the large video.
*
* @param videoWidth the stream video width
* @param videoHeight the stream video height
*/
var positionLarge = function(videoWidth, videoHeight) {
var videoSpaceWidth = $('#videospace').width();
var videoSpaceHeight = window.innerHeight;

var videoSize = getVideoSize( videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight);

var largeVideoWidth = videoSize[0];
var largeVideoHeight = videoSize[1];

var videoPosition = getVideoPosition( largeVideoWidth,
largeVideoHeight,
videoSpaceWidth,
videoSpaceHeight);

var horizontalIndent = videoPosition[0];
var verticalIndent = videoPosition[1];

positionVideo( $('#largeVideo'),
largeVideoWidth,
largeVideoHeight,
horizontalIndent, verticalIndent);
};

/**
* Returns an array of the video horizontal and vertical indents,
* so that if fits its parent.
*
* @return an array with 2 elements, the horizontal indent and the vertical
* indent
*/
function getCameraVideoPosition( videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight) {
// Parent height isn't completely calculated when we position the video in
// full screen mode and this is why we use the screen height in this case.
// Need to think it further at some point and implement it properly.
var isFullScreen = document.fullScreen
|| document.mozFullScreen
|| document.webkitIsFullScreen;
if (isFullScreen)
videoSpaceHeight = window.innerHeight;

var horizontalIndent = (videoSpaceWidth - videoWidth)/2;
var verticalIndent = (videoSpaceHeight - videoHeight)/2;

return [horizontalIndent, verticalIndent];
}

/**
* Returns an array of the video horizontal and vertical indents.
* Centers horizontally and top aligns vertically.
*
* @return an array with 2 elements, the horizontal indent and the vertical
* indent
*/
function getDesktopVideoPosition( videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight) {

var horizontalIndent = (videoSpaceWidth - videoWidth)/2;

var verticalIndent = 0;// Top aligned

return [horizontalIndent, verticalIndent];
}

/**
* Returns an array of the video dimensions, so that it covers the screen.
* It leaves no empty areas, but some parts of the video might not be visible.
*
* @return an array with 2 elements, the video width and the video height
*/
function getCameraVideoSize(videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight) {
if (!videoWidth)
videoWidth = currentVideoWidth;
if (!videoHeight)
videoHeight = currentVideoHeight;

var aspectRatio = videoWidth / videoHeight;

var availableWidth = Math.max(videoWidth, videoSpaceWidth);
var availableHeight = Math.max(videoHeight, videoSpaceHeight);

if (availableWidth / aspectRatio < videoSpaceHeight) {
availableHeight = videoSpaceHeight;
availableWidth = availableHeight*aspectRatio;
}

if (availableHeight*aspectRatio < videoSpaceWidth) {
availableWidth = videoSpaceWidth;
availableHeight = availableWidth / aspectRatio;
}

return [availableWidth, availableHeight];
}

/**
* Returns an array of the video dimensions, so that it keeps it's aspect ratio and fits available area with it's
* larger dimension. This method ensures that whole video will be visible and can leave empty areas.
*
* @return an array with 2 elements, the video width and the video height
*/
function getDesktopVideoSize( videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight )
{
if (!videoWidth)
videoWidth = currentVideoWidth;
if (!videoHeight)
videoHeight = currentVideoHeight;

var aspectRatio = videoWidth / videoHeight;

var availableWidth = Math.max(videoWidth, videoSpaceWidth);
var availableHeight = Math.max(videoHeight, videoSpaceHeight);

videoSpaceHeight -= $('#remoteVideos').outerHeight();

if (availableWidth / aspectRatio >= videoSpaceHeight)
{
availableHeight = videoSpaceHeight;
availableWidth = availableHeight*aspectRatio;
}

if (availableHeight*aspectRatio >= videoSpaceWidth)
{
availableWidth = videoSpaceWidth;
availableHeight = availableWidth / aspectRatio;
}

return [availableWidth, availableHeight];
}

/**
* Sets the size and position of the given video element.
*
* @param video the video element to position
* @param width the desired video width
* @param height the desired video height
* @param horizontalIndent the left and right indent
* @param verticalIndent the top and bottom indent
*/
function positionVideo( video,
width,
height,
horizontalIndent,
verticalIndent) {
video.width(width);
video.height(height);
video.css({ top: verticalIndent + 'px',
bottom: verticalIndent + 'px',
left: horizontalIndent + 'px',
right: horizontalIndent + 'px'});
}

var resizeLargeVideoContainer = function () {
Chat.resizeChat();
var availableHeight = window.innerHeight;
var availableWidth = Util.getAvailableVideoWidth();

if (availableWidth < 0 || availableHeight < 0) return;

$('#videospace').width(availableWidth);
$('#videospace').height(availableHeight);
$('#largeVideoContainer').width(availableWidth);
$('#largeVideoContainer').height(availableHeight);

resizeThumbnails();
};

var calculateThumbnailSize = function() {
// Calculate the available height, which is the inner window height minus
// 39px for the header minus 2px for the delimiter lines on the top and
// bottom of the large video, minus the 36px space inside the remoteVideos
// container used for highlighting shadow.
var availableHeight = 100;

var numvids = $('#remoteVideos>span:visible').length;

// Remove the 1px borders arround videos and the chat width.
var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50;
var availableWidth = availableWinWidth / numvids;
var aspectRatio = 16.0 / 9.0;
var maxHeight = Math.min(160, availableHeight);
availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
}

return [availableWidth, availableHeight];
};

function resizeThumbnails() {
var thumbnailSize = calculateThumbnailSize();
var width = thumbnailSize[0];
var height = thumbnailSize[1];

// size videos so that while keeping AR and max height, we have a nice fit
$('#remoteVideos').height(height);
$('#remoteVideos>span').width(width);
$('#remoteVideos>span').height(height);
}

$(document).ready(function () {
Chat.init();

// Set the defaults for prompt dialogs.
jQuery.prompt.setDefaults({persistent: false});

// Set default desktop sharing method
setDesktopSharing(config.desktopSharing);
// Initialize Chrome extension inline installs
if(config.chromeExtensionId)
{
initInlineInstalls();
}

// By default we use camera
getVideoSize = getCameraVideoSize;
getVideoPosition = getCameraVideoPosition;

resizeLargeVideoContainer();
$(window).resize(function () {
resizeLargeVideoContainer();
positionLarge();
});
// Listen for large video size updates
document.getElementById('largeVideo')
.addEventListener('loadedmetadata', function(e){
currentVideoWidth = this.videoWidth;
currentVideoHeight = this.videoHeight;
positionLarge(currentVideoWidth, currentVideoHeight);
});

if (!$('#settings').is(':visible')) {
console.log('init');
init();
} else {
loginInfo.onsubmit = function (e) {
if (e.preventDefault) e.preventDefault();
$('#settings').hide();
init();
};
}
});

$(window).bind('beforeunload', function () {
if (connection && connection.connected) {
// ensure signout
$.ajax({
type: 'POST',
url: config.bosh,
async: false,
cache: false,
contentType: 'application/xml',
data: "<body rid='" + (connection.rid || connection._proto.rid) + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + (connection.sid || connection._proto.sid) + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>",
success: function (data) {
console.log('signed out');
console.log(data);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log('signout error', textStatus + ' (' + errorThrown + ')');
}
});
}
disposeConference();
});

function disposeConference() {
var handler = getConferenceHandler();
if(handler && handler.peerconnection) {
// FIXME: probably removing streams is not required and close() should be enough
if(connection.jingle.localAudio) {
handler.peerconnection.removeStream(connection.jingle.localAudio);
}
if(connection.jingle.localVideo) {
handler.peerconnection.removeStream(connection.jingle.localVideo);
}
handler.peerconnection.close();
}
focus = null;
activecall = null;
}

function dump(elem, filename){
elem = elem.parentNode;
elem.download = filename || 'meetlog.json';
elem.href = 'data:application/json;charset=utf-8,\n';
var data = {};
if (connection.jingle) {
Object.keys(connection.jingle.sessions).forEach(function (sid) {
var session = connection.jingle.sessions[sid];
if (session.peerconnection && session.peerconnection.updateLog) {
// FIXME: should probably be a .dump call
data["jingle_" + session.sid] = {
updateLog: session.peerconnection.updateLog,
stats: session.peerconnection.stats,
url: window.location.href}
;
}
});
}
metadata = {};
metadata.time = new Date();
metadata.url = window.location.href;
metadata.ua = navigator.userAgent;
if (connection.logger) {
metadata.xmpp = connection.logger.log;
}
data.metadata = metadata;
elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
return false;
}

/**
* Changes the style class of the element given by id.
*/
function buttonClick(id, classname) {
$(id).toggleClass(classname); // add the class to the clicked element
}

/**
* Opens the lock room dialog.
*/
function openLockDialog() {
// Only the focus is able to set a shared key.
if (focus === null) {
if (sharedKey)
$.prompt("This conversation is currently protected by a shared secret key.",
{
title: "Secrect key",
persistent: false
});
else
$.prompt("This conversation isn't currently protected by a secret key. Only the owner of the conference could set a shared key.",
{
title: "Secrect key",
persistent: false
});
}
else {
if (sharedKey)
$.prompt("Are you sure you would like to remove your secret key?",
{
title: "Remove secrect key",
persistent: false,
buttons: { "Remove": true, "Cancel": false},
defaultButton: 1,
submit: function(e,v,m,f){
if(v)
{
setSharedKey('');
lockRoom(false);
}
}
});
else
$.prompt('<h2>Set a secrect key to lock your room</h2>' +
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
buttons: { "Save": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('lockKey').focus();
},
submit: function(e,v,m,f){
if(v)
{
var lockKey = document.getElementById('lockKey');

if (lockKey.value)
{
setSharedKey(Util.escapeHtml(lockKey.value));
lockRoom(true);
}
}
}
});
}
}

/**
* Opens the invite link dialog.
*/
function openLinkDialog() {
$.prompt('<input id="inviteLinkRef" type="text" value="'
+ encodeURI(roomUrl) + '" onclick="this.select();" readonly>',
{
title: "Share this link with everyone you want to invite",
persistent: false,
buttons: { "Cancel": false},
loaded: function(event) {
document.getElementById('inviteLinkRef').select();
}
});
}

/**
* Opens the settings dialog.
*/
function openSettingsDialog() {
$.prompt('<h2>Configure your conference</h2>' +
'<input type="checkbox" id="initMuted"> Participants join muted<br/>' +
'<input type="checkbox" id="requireNicknames"> Require nicknames<br/><br/>' +
'Set a secrect key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
buttons: { "Save": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('lockKey').focus();
},
submit: function(e,v,m,f){
if(v)
{
if ($('#initMuted').is(":checked"))
{
// it is checked
}

if ($('#requireNicknames').is(":checked"))
{
// it is checked
}
/*
var lockKey = document.getElementById('lockKey');

if (lockKey.value)
{
setSharedKey(lockKey.value);
lockRoom(true);
}
*/
}
}
});
}

/**
* Locks / unlocks the room.
*/
function lockRoom(lock) {
if (lock)
connection.emuc.lockRoom(sharedKey);
else
connection.emuc.lockRoom('');

updateLockButton();
}

/**
* Sets the shared key.
*/
function setSharedKey(sKey) {
sharedKey = sKey;
}

/**
* Updates the lock button state.
*/
function updateLockButton() {
buttonClick("#lockIcon", "icon-security icon-security-locked");
}

/**
* Hides the toolbar.
*/
var hideToolbar = function() {

var isToolbarHover = false;
$('#header').find('*').each(function(){
var id = $(this).attr('id');
if ($("#" + id + ":hover").length > 0) {
isToolbarHover = true;
}
});

clearTimeout(toolbarTimeout);
toolbarTimeout = null;

if (!isToolbarHover) {
$('#header').hide("slide", { direction: "up", duration: 300});
}
else {
toolbarTimeout = setTimeout(hideToolbar, 2000);
}
};

/**
* Shows the call main toolbar.
*/
function showToolbar() {
if (!$('#header').is(':visible')) {
$('#header').show("slide", { direction: "up", duration: 300});

if (toolbarTimeout) {
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
}
toolbarTimeout = setTimeout(hideToolbar, 2000);
}

if (focus != null)
{
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"});
}

// Show/hide desktop sharing button
showDesktopSharingButton();
}

/**
* Docks/undocks the toolbar.
*
* @param isDock indicates what operation to perform
*/
function dockToolbar(isDock) {
if (isDock) {
// First make sure the toolbar is shown.
if (!$('#header').is(':visible')) {
showToolbar();
}
// Then clear the time out, to dock the toolbar.
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
}
else {
if (!$('#header').is(':visible')) {
showToolbar();
}
else {
toolbarTimeout = setTimeout(hideToolbar, 2000);
}
}
}

/**
* Updates the room invite url.
*/
function updateRoomUrl(newRoomUrl) {
roomUrl = newRoomUrl;
}

/**
* Warning to the user that the conference window is about to be closed.
*/
function closePageWarning() {
if (focus !== null)
return "You are the owner of this conference call and you are about to end it.";
else
return "You are about to leave this conversation.";
}

/**
* Shows a visual indicator for the focus of the conference.
* Currently if we're not the owner of the conference we obtain the focus
* from the connection.jingle.sessions.
*/
function showFocusIndicator() {
if (focus !== null) {
var indicatorSpan = $('#localVideoContainer .focusindicator');

if (indicatorSpan.children().length === 0)
{
createFocusIndicatorElement(indicatorSpan[0]);
}
}
else if (Object.keys(connection.jingle.sessions).length > 0) {
// If we're only a participant the focus will be the only session we have.
var session = connection.jingle.sessions[Object.keys(connection.jingle.sessions)[0]];
var focusId = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
var focusContainer = document.getElementById(focusId);
if(!focusContainer) {
console.error("No focus container!");
return;
}
var indicatorSpan = $('#' + focusId + ' .focusindicator');

if (!indicatorSpan || indicatorSpan.length === 0) {
indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator';
focusContainer.appendChild(indicatorSpan);

createFocusIndicatorElement(indicatorSpan);
}
}
}

/**
* Checks if container for participant identified by given peerJid exists in the document and creates it eventually.
* @param peerJid peer Jid to check.
*/
function ensurePeerContainerExists(peerJid){

var peerResource = Strophe.getResourceFromJid(peerJid);
var videoSpanId = 'participant_' + peerResource;

if($('#'+videoSpanId).length > 0) {
return;
}

var container = addRemoteVideoContainer(videoSpanId);

var nickfield = document.createElement('span');
nickfield.className = "nick";
nickfield.appendChild(document.createTextNode(peerResource));
container.appendChild(nickfield);
resizeThumbnails();
}

function addRemoteVideoContainer(id) {
var container = document.createElement('span');
container.id = id;
container.className = 'videocontainer';
var remotes = document.getElementById('remoteVideos');
remotes.appendChild(container);
return container;
}

/**
* Creates the element indicating the focus of the conference.
*/
function createFocusIndicatorElement(parentElement) {
var focusIndicator = document.createElement('i');
focusIndicator.className = 'fa fa-star';
focusIndicator.title = "The owner of this conference";
parentElement.appendChild(focusIndicator);
}

/**
* Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome).
*/
function toggleFullScreen() {
var fsElement = document.documentElement;

if (!document.mozFullScreen && !document.webkitIsFullScreen){

//Enter Full Screen
if (fsElement.mozRequestFullScreen) {
fsElement.mozRequestFullScreen();
}
else {
fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
//Exit Full Screen
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else {
document.webkitCancelFullScreen();
}
}
}

/**
* Shows the display name for the given video.
*/
function showDisplayName(videoSpanId, displayName) {
var nameSpan = $('#' + videoSpanId + '>span.displayname');

// If we already have a display name for this video.
if (nameSpan.length > 0) {
var nameSpanElement = nameSpan.get(0);

if (nameSpanElement.id === 'localDisplayName'
&& $('#localDisplayName').text() !== displayName)
$('#localDisplayName').text(displayName);
else
$('#' + videoSpanId + '_name').text(displayName);
}
else {
var editButton = null;

if (videoSpanId === 'localVideoContainer') {
editButton = createEditDisplayNameButton();
}
if (displayName.length) {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
nameSpan.innerText = displayName;
$('#' + videoSpanId)[0].appendChild(nameSpan);
}

if (!editButton) {
nameSpan.id = videoSpanId + '_name';
}
else {
nameSpan.id = 'localDisplayName';
$('#' + videoSpanId)[0].appendChild(editButton);

var editableText = document.createElement('input');
editableText.className = 'displayname';
editableText.id = 'editDisplayName';

if (displayName.length)
editableText.value
= displayName.substring(0, displayName.indexOf(' (me)'));

editableText.setAttribute('style', 'display:none;');
editableText.setAttribute('placeholder', 'ex. Jane Pink');
$('#' + videoSpanId)[0].appendChild(editableText);

$('#localVideoContainer .displayname').bind("click", function(e) {
e.preventDefault();
$('#localDisplayName').hide();
$('#editDisplayName').show();
$('#editDisplayName').focus();
$('#editDisplayName').select();

var inputDisplayNameHandler = function(name) {
if (nickname !== name) {
nickname = name;
window.localStorage.displayname = nickname;
connection.emuc.addDisplayNameToPresence(nickname);
connection.emuc.sendPresence();

Chat.setChatConversationMode(true);
}

if (!$('#localDisplayName').is(":visible")) {
$('#localDisplayName').text(nickname + " (me)");
$('#localDisplayName').show();
$('#editDisplayName').hide();
}
};

$('#editDisplayName').one("focusout", function (e) {
inputDisplayNameHandler(this.value);
});

$('#editDisplayName').on('keydown', function (e) {
if (e.keyCode === 13) {
e.preventDefault();
inputDisplayNameHandler(this.value);
}
});
});
}
}
}

/**
* Creates the edit display name button.
*
* @returns the edit button
*/
function createEditDisplayNameButton() {
var editButton = document.createElement('a');
editButton.className = 'displayname';
editButton.innerHTML = '<i class="fa fa-pencil"></i>';

return editButton;
}

/**
* Shows audio muted indicator over small videos.
*/
function showAudioIndicator(videoSpanId, isMuted) {
var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');

if (isMuted === 'false') {
if (audioMutedSpan.length > 0) {
audioMutedSpan.remove();
}
}
else {
var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');

audioMutedSpan = document.createElement('span');
audioMutedSpan.className = 'audioMuted';
if (videoMutedSpan) {
audioMutedSpan.right = '30px';
}
$('#' + videoSpanId)[0].appendChild(audioMutedSpan);

var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-mic-disabled';
mutedIndicator.title = "Participant is muted";
audioMutedSpan.appendChild(mutedIndicator);
}
}

/**
* Shows video muted indicator over small videos.
*/
function showVideoIndicator(videoSpanId, isMuted) {
var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');

if (isMuted === 'false') {
if (videoMutedSpan.length > 0) {
videoMutedSpan.remove();
}
}
else {
var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');

videoMutedSpan = document.createElement('span');
videoMutedSpan.className = 'videoMuted';
if (audioMutedSpan) {
videoMutedSpan.right = '30px';
}
$('#' + videoSpanId)[0].appendChild(videoMutedSpan);

var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-camera-disabled';
mutedIndicator.title = "Participant has stopped the camera.";
videoMutedSpan.appendChild(mutedIndicator);
}
}

/**
* Resizes and repositions videos in full screen mode.
*/
$(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange',
function() {
resizeLargeVideoContainer();
positionLarge();
isFullScreen = document.fullScreen
|| document.mozFullScreen
|| document.webkitIsFullScreen;

if (isFullScreen) {
setView("fullscreen");
}
else {
setView("default");
}
});

/**
* Sets the current view.
*/
function setView(viewName) {
// if (viewName == "fullscreen") {
// document.getElementById('videolayout_fullscreen').disabled = false;
// document.getElementById('videolayout_default').disabled = true;
// }
// else {
// document.getElementById('videolayout_default').disabled = false;
// document.getElementById('videolayout_fullscreen').disabled = true;
// }
}
@@ -0,0 +1,292 @@
/**
* Chat related user interface.
*/
var Chat = (function (my) {
var notificationInterval = false;
var unreadMessages = 0;

/**
* Initializes chat related interface.
*/
my.init = function () {
var storedDisplayName = window.localStorage.displayname;
if (storedDisplayName) {
nickname = storedDisplayName;

Chat.setChatConversationMode(true);
}

$('#nickinput').keydown(function(event) {
if (event.keyCode === 13) {
event.preventDefault();
var val = Util.escapeHtml(this.value);
this.value = '';
if (!nickname) {
nickname = val;
window.localStorage.displayname = nickname;

connection.emuc.addDisplayNameToPresence(nickname);
connection.emuc.sendPresence();

Chat.setChatConversationMode(true);

return;
}
}
});

$('#usermsg').keydown(function(event) {
if (event.keyCode === 13) {
event.preventDefault();
var message = Util.escapeHtml(this.value);
$('#usermsg').val('').trigger('autosize.resize');
this.focus();
connection.emuc.sendMessage(message, nickname);
}
});

var onTextAreaResize = function() {
resizeChatConversation();
scrollChatToBottom();
};
$('#usermsg').autosize({callback: onTextAreaResize});

$("#chatspace").bind("shown",
function() {
unreadMessages = 0;
setVisualNotification(false);
});
};

/**
* Appends the given message to the chat conversation.
*/
my.updateChatConversation = function (from, displayName, message) {
var divClassName = '';

if (connection.emuc.myroomjid === from) {
divClassName = "localuser";
}
else {
divClassName = "remoteuser";

if (!$('#chatspace').is(":visible")) {
unreadMessages++;
Util.playSoundNotification('chatNotification');
setVisualNotification(true);
}
}

//replace links and smileys
var escMessage = Util.escapeHtml(message);
var escDisplayName = Util.escapeHtml(displayName);
message = processReplacements(escMessage);

$('#chatconversation').append('<div class="' + divClassName + '"><b>'
+ escDisplayName + ': </b>'
+ message + '</div>');
$('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
};

/**
* Opens / closes the chat area.
*/
my.toggleChat = function () {
var chatspace = $('#chatspace');
var videospace = $('#videospace');

var chatSize = (chatspace.is(":visible")) ? [0, 0] : Chat.getChatSize();
var videospaceWidth = window.innerWidth - chatSize[0];
var videospaceHeight = window.innerHeight;
var videoSize
= getVideoSize(null, null, videospaceWidth, videospaceHeight);
var videoWidth = videoSize[0];
var videoHeight = videoSize[1];
var videoPosition = getVideoPosition( videoWidth,
videoHeight,
videospaceWidth,
videospaceHeight);
var horizontalIndent = videoPosition[0];
var verticalIndent = videoPosition[1];

if (chatspace.is(":visible")) {
videospace.animate( {right: chatSize[0],
width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500});

$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500
});

$('#largeVideo').animate({ width: videoWidth,
height: videoHeight,
top: verticalIndent,
bottom: verticalIndent,
left: horizontalIndent,
right: horizontalIndent},
{ queue: false,
duration: 500
});

$('#chatspace').hide("slide", { direction: "right",
queue: false,
duration: 500});
}
else {
videospace.animate({right: chatSize[0],
width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500,
complete: function() {
scrollChatToBottom();
chatspace.trigger('shown');
}
});

$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500
});

$('#largeVideo').animate({ width: videoWidth,
height: videoHeight,
top: verticalIndent,
bottom: verticalIndent,
left: horizontalIndent,
right: horizontalIndent},
{queue: false,
duration: 500
});

$('#chatspace').show("slide", { direction: "right",
queue: false,
duration: 500});
}

// Request the focus in the nickname field or the chat input field.
if ($('#nickname').css('visibility') === 'visible')
$('#nickinput').focus();
else {
$('#usermsg').focus();
}
};

/**
* Sets the chat conversation mode.
*/
my.setChatConversationMode = function (isConversationMode) {
if (isConversationMode) {
$('#nickname').css({visibility:"hidden"});
$('#chatconversation').css({visibility:'visible'});
$('#usermsg').css({visibility:'visible'});
$('#usermsg').focus();
}
};

/**
* Resizes the chat area.
*/
my.resizeChat = function () {
var chatSize = Chat.getChatSize();

$('#chatspace').width(chatSize[0]);
$('#chatspace').height(chatSize[1]);

resizeChatConversation();
};

/**
* Returns the size of the chat.
*/
my.getChatSize = function() {
var availableHeight = window.innerHeight;
var availableWidth = window.innerWidth;

var chatWidth = 200;
if (availableWidth*0.2 < 200)
chatWidth = availableWidth*0.2;

return [chatWidth, availableHeight];
};

/**
* Resizes the chat conversation.
*/
function resizeChatConversation() {
var usermsgStyleHeight = document.getElementById("usermsg").style.height;
var usermsgHeight = usermsgStyleHeight
.substring(0, usermsgStyleHeight.indexOf('px'));

$('#usermsg').width($('#chatspace').width() - 10);
$('#chatconversation').width($('#chatspace').width() - 10);
$('#chatconversation')
.height(window.innerHeight - 10 - parseInt(usermsgHeight));
};

/**
* Shows/hides a visual notification, indicating that a message has arrived.
*/
function setVisualNotification(show) {
var unreadMsgElement = document.getElementById('unreadMessages');

var glower = $('#chatButton');

if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString();

showToolbar();

var chatButtonElement
= document.getElementById('chatButton').parentNode;
var leftIndent = (Util.getTextWidth(chatButtonElement)
- Util.getTextWidth(unreadMsgElement))/2;
var topIndent = (Util.getTextHeight(chatButtonElement)
- Util.getTextHeight(unreadMsgElement))/2 - 3;

unreadMsgElement.setAttribute(
'style',
'top:' + topIndent
+ '; left:' + leftIndent +';');

if (!glower.hasClass('icon-chat-simple')) {
glower.removeClass('icon-chat');
glower.addClass('icon-chat-simple');
}
}
else {
unreadMsgElement.innerHTML = '';
glower.removeClass('icon-chat-simple');
glower.addClass('icon-chat');
}

if (show && !notificationInterval) {
notificationInterval = window.setInterval(function() {
glower.toggleClass('active');
}, 800);
}
else if (!show && notificationInterval) {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('active');
}
}

/**
* Scrolls chat to the bottom.
*/
function scrollChatToBottom() {
setTimeout(function() {
$('#chatconversation').scrollTop(
$('#chatconversation')[0].scrollHeight);
}, 5);
}

return my;
}(Chat || {}));
@@ -0,0 +1,20 @@
<html>
<head>
<title>JitMeet: Unsupported Browser</title>
<link rel="stylesheet" type="text/css" media="screen" href="css/chromeonly.css" />
</head>

<body>
<!-- wrap starts here -->
<div id="wrap">
<a href="http://google.com/chrome"><div id="left"></div></a>
<div id="middle"></div>
<div id="text">
<p>This application is currently only supported by <a href="http://google.com/chrome">Chrome</a>, <a href="http://www.chromium.org/">Chromium</a> and <a href="http://www.opera.com">Opera</a></p>
<p><a href="http://google.com/chrome">Download Chrome</a></p>
<p class="firefox">We are hoping that <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=977864">multistream support</a> for Firefox would not be long so that we could all use this application with our favorite browser.</p>
</div>
<!-- wrap ends here -->
</div>
</body>
</html>
@@ -0,0 +1,22 @@
config.desktopSharing = 'ext'; // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
config.chromeExtensionId = 'diibjkoicjeejcmhdnailmkgecihlobk'; // Id of desktop streamer Chrome extension
config.minChromeExtVersion = '0.1'; // Required version of Chrome extension

config.getroomnode = function (path)
{
console.log('getroomnode', path);
var name = "r";
var roomnode = null;

var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href);

if (!results)
roomnode = null;
else roomnode = results[1] || undefined;

if (!roomnode) {
roomnode = Math.random().toString(36).substr(2, 20);
window.history.pushState('VideoChat', 'Room: ' + roomnode, path + "?r=" + roomnode);
}
return roomnode;
};
@@ -0,0 +1,54 @@
body {
width:100%;
height:100%;
background-color: white;
color: #424242;
font-family:'YanoneKaffeesatzLight',Verdana,Tahoma,Arial;
margin:0;
padding:0;
}
#wrap{
display: block;
position: absolute;
width:900px;
height: 262px;
overflow:hidden;
text-align: center;
margin: auto;
top: 0; left: 0; bottom: 0; right: 0;
}
#left{
display:inline-block;
background-image:url(../images/chromelogo.png);
background-repeat:no-repeat;
width:246px;
height:262px;
float: left;
}
.firefox{
font-size: 11pt;
color: #c8c8c8;
}
#middle{
display:inline-block;
background-image:url(../images/chromepointer.png);
background-repeat:no-repeat;
width:53px;
height:262px;
float: left;
}
#text{
display:inline-block;
font-size: 18pt;
width: 560px;
vertical-align:middle;
padding-top: 30px;
}

a {
color: #087dba;
text-decoration:none;
}



@@ -0,0 +1,71 @@
@font-face {
font-family: 'jitsi';
src:url('../fonts/jitsi.eot?94d075');
src:url('../fonts/jitsi.eot?#iefix94d075') format('embedded-opentype'),
url('../fonts/jitsi.woff?94d075') format('woff'),
url('../fonts/jitsi.ttf?94d075') format('truetype'),
url('../fonts/jitsi.svg?94d075#jitsi') format('svg');
font-weight: normal;
font-style: normal;
}

[class^="icon-"], [class*=" icon-"] {
font-family: 'jitsi';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 0.75em;
font-size: 1.22em;

/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.icon-share-desktop:before {
content: "\e602";
}
.icon-chat-simple:before {
content: "\e606";
}
.icon-full-screen:before {
content: "\e60d";
}
.icon-exit-full-screen:before {
content: "\e60e";
}
.icon-prezi:before {
content: "\e60c";
}
.icon-link:before {
content: "\e600";
}
.icon-chat:before {
content: "\e601";
}
.icon-presentation:before {
content: "\e603";
}
.icon-security:before {
content: "\e604";
}
.icon-share-doc:before {
content: "\e605";
}
.icon-security-locked:before {
content: "\e607";
}
.icon-camera:before {
content: "\e608";
}
.icon-camera-disabled:before {
content: "\e609";
}
.icon-mic-disabled:before {
content: "\e60a";
}
.icon-microphone:before {
content: "\e60b";
}
@@ -0,0 +1,124 @@
/*
------------------------------
Impromptu
------------------------------
*/
.jqifade{
position: absolute;
background-color: #000;
}
div.jqi{
width: 400px;
position: absolute;
background-color: #ffffff;
font-size: 11px;
text-align: left;
border: solid 1px #eeeeee;
border-radius: 6px;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
padding: 7px;
}
div.jqi .jqicontainer{
}
div.jqi .jqiclose{
position: absolute;
top: 4px; right: -2px;
width: 18px;
cursor: default;
color: #bbbbbb;
font-weight: bold;
}
div.jqi .jqistate{
background-color: #fff;
}
div.jqi .jqititle{
padding: 5px 10px;
font-size: 16px;
line-height: 20px;
border-bottom: solid 1px #eeeeee;
}
div.jqi .jqimessage{
padding: 10px;
line-height: 20px;
color: #444444;
}
div.jqi .jqibuttons{
text-align: right;
margin: 0 -7px -7px -7px;
border-top: solid 1px #e4e4e4;
background-color: #f4f4f4;
border-radius: 0 0 6px 6px;
-moz-border-radius: 0 0 6px 6px;
-webkit-border-radius: 0 0 6px 6px;
}
div.jqi .jqibuttons button{
margin: 0;
padding: 5px 20px;
background-color: transparent;
font-weight: normal;
border: none;
border-left: solid 1px #e4e4e4;
color: #777;
font-weight: bold;
font-size: 12px;
}
div.jqi .jqibuttons button.jqidefaultbutton{
color: #489afe;
}
div.jqi .jqibuttons button:hover,
div.jqi .jqibuttons button:focus{
color: #287ade;
outline: none;
}
.jqiwarning .jqi .jqibuttons{
background-color: #b95656;
}

/* sub states */
div.jqi .jqiparentstate::after{
background-color: #777;
opacity: 0.6;
filter: alpha(opacity=60);
content: '';
position: absolute;
top:0;left:0;bottom:0;right:0;
border-radius: 6px;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
}
div.jqi .jqisubstate{
position: absolute;
top:0;
left: 20%;
width: 60%;
padding: 7px;
border: solid 1px #eeeeee;
border-top: none;
border-radius: 0 0 6px 6px;
-moz-border-radius: 0 0 6px 6px;
-webkit-border-radius: 0 0 6px 6px;
}
div.jqi .jqisubstate .jqibuttons button{
padding: 10px 18px;
}

/* arrows for tooltips/tours */
.jqi .jqiarrow{ position: absolute; height: 0; width:0; line-height: 0; font-size: 0; border: solid 10px transparent;}

.jqi .jqiarrowtl{ left: 10px; top: -20px; border-bottom-color: #ffffff; }
.jqi .jqiarrowtc{ left: 50%; top: -20px; border-bottom-color: #ffffff; margin-left: -10px; }
.jqi .jqiarrowtr{ right: 10px; top: -20px; border-bottom-color: #ffffff; }

.jqi .jqiarrowbl{ left: 10px; bottom: -20px; border-top-color: #ffffff; }
.jqi .jqiarrowbc{ left: 50%; bottom: -20px; border-top-color: #ffffff; margin-left: -10px; }
.jqi .jqiarrowbr{ right: 10px; bottom: -20px; border-top-color: #ffffff; }

.jqi .jqiarrowlt{ left: -20px; top: 10px; border-right-color: #ffffff; }
.jqi .jqiarrowlm{ left: -20px; top: 50%; border-right-color: #ffffff; margin-top: -10px; }
.jqi .jqiarrowlb{ left: -20px; bottom: 10px; border-right-color: #ffffff; }

.jqi .jqiarrowrt{ right: -20px; top: 10px; border-left-color: #ffffff; }
.jqi .jqiarrowrm{ right: -20px; top: 50%; border-left-color: #ffffff; margin-top: -10px; }
.jqi .jqiarrowrb{ right: -20px; bottom: 10px; border-left-color: #ffffff; }

@@ -0,0 +1,228 @@
html, body{
margin:0px;
height:100%;
color: #424242;
font-family:'Helvetica Neue', Helvetica, sans-serif;
font-weight: 400;
background: #000000;
overflow-x: hidden;
}

#chatspace {
display:none;
position:absolute;
float: right;
top: 0px;
bottom: 0px;
right: 0px;
width: 20%;
max-width: 200px;
overflow: hidden;
/* background-color:#dfebf1;*/
background-color:#FFFFFF;
border-left:1px solid #424242;
z-index: 5;
}

#chatconversation {
visibility: hidden;
position: relative;
top: 5px;
padding: 5px;
text-align: left;
line-height: 20px;
font-size: 10pt;
width: 100%;
height: 95%;
overflow-y: scroll;
overflow-x: hidden;
word-wrap: break-word;
}

.localuser {
color: #087dba;
}

.remoteuser {
color: #424242;
}

#usermsg {
visibility:hidden;
position: relative;
width: 100%;
height: 5%;
padding: 5px;
max-height:150px;
min-height:50px;
border: 0px none;
border-top: 1px solid #cccccc;
background: #FFFFFF;
box-shadow: none;
border-radius:0;
font-size: 10pt;
overflow: hidden;
}

#usermsg: hover {
border: 0px none;
border-top: 1px solid #cccccc;
box-shadow: none;
}

#nickname {
position: absolute;
text-align: center;
color: #9d9d9d;
font-size: 18;
top: 100px;
left: 5px;
right: 5px;
width: 95%;
}

#nickinput {
margin-top: 20px;
font-size: 14;
}

#settings {
display:none;
}

#nowebrtc {
display:none;
}

#settingsButton {
visibility: hidden;
}

.toolbar_span {
display: inline-block;
position: relative;
}

.button {
display: inline-block;
position: relative;
color: #FFFFFF;
top: 0;
padding: 10px 0px;
width: 39px;
cursor: pointer;
font-size: 11pt;
text-align: center;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
z-index: 1;
}

.toolbar_span>span {
display: inline-block;
position: absolute;
font-size: 7pt;
color: #ffffff;
text-align:center;
cursor: pointer;
}

#chatButton {
-webkit-transition: all .5s ease-in-out;;
-moz-transition: all .5s ease-in-out;;
transition: all .5s ease-in-out;;
}

#chatButton.active {
-webkit-text-shadow: 0 0 10px #ffffff;
-moz-text-shadow: 0 0 10px #ffffff;
text-shadow: 0 0 10px #ffffff;
}

a.button:hover {
top: 0;
cursor: pointer;
background: rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-clip: padding-box;
-webkit-border-radius: 5px;
-webkit-background-clip: padding-box;
}

.no-fa-video-camera, .fa-microphone-slash {
color: #636363;
}

.header_button_separator {
display: inline-block;
position:relative;
top: 5;
width: 1px;
height: 20px;
background: #676767;
}

input[type='text'], textarea {
display: inline-block;
font-size: 14px;
padding: 5px;
background: #f3f3f3;
border-radius: 3px;
font-weight: 100;
line-height: 20px;
height: 40px;
color: #333;
text-align: left;
border:1px solid #ACD8F0;
outline: none; /* removes the default outline */
resize: none; /* prevents the user-resizing, adjust to taste */
}

input[type='text'], textarea:focus {
box-shadow: inset 0 0 3px 2px #ACD8F0; /* provides a more style-able
replacement to the outline */
}

textarea {
overflow: hidden;
word-wrap: break-word;
resize: horizontal;
}

button.no-icon {
padding: 0 1em;
}

button {
border: none;
height: 35px;
padding: 0 1em 0 2em;
position: relative;
border-radius: 3px;
font-weight: bold;
color: #fff;
line-height: 35px;
background: #2c8ad2;
}

button, input, select, textarea {
font-size: 100%;
margin: 0;
vertical-align: baseline;
}

button, input[type="button"], input[type="reset"], input[type="submit"] {
cursor: pointer;
-webkit-appearance: button;
}

form {
display: block;
}

#downloadlog {
position: absolute;
bottom: 5;
left: 5;
overflow: visible;
z-index: 100;
}
@@ -0,0 +1,29 @@
.jqistates {
font-size: 14px;
}

.jqistates h2 {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
font-size: 18px;
line-height: 25px;
text-align: center;
color: #424242;
}

.jqistates input {
margin: 10px 0;
}

.jqistates input[type="text"] {
width: 100%;
}

.jqibuttons button {
margin-right: 5px;
float:right;
}

button.jqidefaultbutton #inviteLinkRef {
color: #2c8ad2;
}
@@ -0,0 +1,272 @@
#videospace {
display: block;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
}

#remoteVideos {
display:block;
position:absolute;
text-align:right;
height:196px;
padding: 18px;
bottom: 0;
left: 0;
right: 0;
width:auto;
overflow: hidden;
border:1px solid transparent;
z-index: 2;
}

.videocontainer {
position: relative;
margin-left: auto;
margin-right: auto;
}

#remoteVideos .videocontainer {
display: inline-block;
background-image:url(../images/avatar1.png);
background-size: contain;
border-radius:8px;
border: 2px solid #212425;
}

#remoteVideos .videocontainer:hover {
width: 100%;
height: 100%;
content:"";
cursor: pointer;
cursor: hand;
transform:scale(1.08, 1.08);
-webkit-transform:scale(1.08, 1.08);
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
-webkit-box-shadow: 0 0 18px #388396;
border: 2px solid #388396;
z-index: 3;
}

#localVideoWrapper {
display:inline-block;
-webkit-mask-box-image: url(../images/videomask.svg);
border-radius:0px !important;
border: 0px !important;
}

#remoteVideos .videocontainer>video {
border-radius:4px;
}

.flipVideoX {
transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-webkit-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
}

#localVideoWrapper>video {
border-radius:0px !important;
}

#largeVideo,
#largeVideoContainer {
overflow: hidden;
text-align: center;
}

#presentation,
#etherpad,
#localVideoWrapper>video,
#localVideoWrapper,
.videocontainer>video {
position: absolute;
left: 0;
top: 0;
z-index: 1;
width: 100%;
height: 100%;
}

#etherpad,
#presentation {
text-align: center;
}

#etherpad {
z-index: 0;
}

#etherpadButton {
display: none;
}

#remoteVideos .videocontainer>span.focusindicator {
display: inline-block;
position: absolute;
color: #FFFFFF;
top: 0;
left: 0;
padding: 5px 0px;
width: 25px;
font-size: 11pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 2;
}

#remoteVideos .nick {
display: none; /* enable when you want nicks to be shown */
position: absolute;
left: 0px;
bottom: -20px;
z-index: 0;
width: 100%;
font-size: 10pt;
}

.videocontainer>span.displayname,
.videocontainer>input.displayname {
display: inline-block;
position: absolute;
background: -webkit-linear-gradient(left, rgba(0,0,0,.7), rgba(0,0,0,0));
color: #FFFFFF;
bottom: 0;
left: 0;
padding: 3px 5px;
width: 100%;
height: auto;
max-height: 18px;
font-size: 9pt;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
z-index: 2;
box-sizing: border-box;
border-bottom-left-radius:4px;
border-bottom-right-radius:4px;
}

#localVideoContainer>span.displayname:hover {
cursor: text;
}

.videocontainer>span.displayname {
pointer-events: none;
}

#localDisplayName {
pointer-events: auto !important;
}

.videocontainer>a.displayname {
display: inline-block;
position: absolute;
color: #FFFFFF;
bottom: 0;
right: 0;
padding: 3px 5px;
font-size: 9pt;
cursor: pointer;
z-index: 2;
}

.videocontainer>span.audioMuted {
display: inline-block;
position: absolute;
color: #FFFFFF;
top: 0;
right: 0;
padding: 8px 5px;
width: 25px;
font-size: 8pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 3;
}

.videocontainer>span.videoMuted {
display: inline-block;
position: absolute;
color: #FFFFFF;
top: 0;
right: 0;
padding: 8px 5px;
width: 25px;
font-size: 8pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 3;
}

#reloadPresentation {
display: none;
position: absolute;
color: #FFFFFF;
top: 0;
right:0;
padding: 10px 10px;
font-size: 11pt;
cursor: pointer;
background: rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-clip: padding-box;
-webkit-border-radius: 5px;
-webkit-background-clip: padding-box;
z-index: 20; /*The reload button should appear on top of the header!*/
}

#header{
display:none;
position:absolute;
height:39px;
text-align:center;
top:0;
left:0;
right:0;
z-index:10;
}

#toolbar {
display:inline-block;
position:relative;
margin-left:auto;
margin-right:auto;
height:39px;
width:auto;
overflow: hidden;
background: linear-gradient(to bottom, rgba(103,103,103,.65) , rgba(0,0,0,.65));
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}

.watermark {
display: block;
position: absolute;
top: 15;
width: 20%;
height: 10%;
background-size: contain;
background-repeat: no-repeat;
z-index: 2;
}

#leftwatermark {
left: 15;
background-image:url(../images/watermark.png);
background-position: center left;
}

#rightwatermark {
right: 15;
background-image:url(../images/rightwatermark.png);
background-position: center right;
}
Loading