Commit 820cd403 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1515: Migrate Private XML storage to PEP.

Openfire supports XEP-0049, Private XML Storage. However, Openfire can also be used for
XEP-0223: Persistent Storage of Private Data via PubSub. When a user previously used more
than one client, where each client would use a different XEP to store private data, the
private data that was stored by one client would not be shared with the other client. This
is undesirable, as such data typically includes data that benefits every client used by the
user.

In this commit, all data that is stored in the Private XML Storage database table will be
migrated to PEP. After a successful migration, the old database table is deleted.

Openfire continues to offer XEP-0049: Private XML Storage functionality. However, whenever
data is obtained or modified through methods defined in that XEP, the PEP data is modified.
parent d16c186d
......@@ -35,15 +35,6 @@ CREATE INDEX ofUserFlag_sTime_idx ON ofUserFlag (startTime ASC);
CREATE INDEX ofUserFlag_eTime_idx ON ofUserFlag (endTime ASC);
CREATE TABLE ofPrivate (
username VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
namespace VARCHAR(200) NOT NULL,
privateData VARCHAR(2000) NOT NULL,
CONSTRAINT ofPrivate_pk PRIMARY KEY (username, name, namespace)
);
CREATE TABLE ofOffline (
username VARCHAR(64) NOT NULL,
messageID INTEGER NOT NULL,
......@@ -393,7 +384,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
-- Entry for admin user
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
......@@ -35,15 +35,6 @@ CREATE INDEX ofUserFlag_sTime_idx ON ofUserFlag (startTime);
CREATE INDEX ofUserFlag_eTime_idx ON ofUserFlag (endTime);
CREATE TABLE ofPrivate (
username VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
namespace VARCHAR(200) NOT NULL,
privateData LONGVARCHAR NOT NULL,
CONSTRAINT ofPrivate_pk PRIMARY KEY (username, name, namespace)
);
CREATE TABLE ofOffline (
username VARCHAR(64) NOT NULL,
messageID BIGINT NOT NULL,
......@@ -379,7 +370,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
// Entry for admin user
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
......@@ -32,14 +32,6 @@ CREATE TABLE ofUserFlag (
INDEX ofUserFlag_eTime_idx (endTime)
);
CREATE TABLE ofPrivate (
username VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
namespace VARCHAR(200) NOT NULL,
privateData TEXT NOT NULL,
PRIMARY KEY (username, name, namespace(100))
);
CREATE TABLE ofOffline (
username VARCHAR(64) NOT NULL,
messageID BIGINT NOT NULL,
......@@ -368,7 +360,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
# Entry for admin user
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
......@@ -35,15 +35,6 @@ CREATE INDEX ofUserFlag_sTime_idx ON ofUserFlag (startTime ASC);
CREATE INDEX ofUserFlag_eTime_idx ON ofUserFlag (endTime ASC);
CREATE TABLE ofPrivate (
username VARCHAR2(64) NOT NULL,
name VARCHAR2(100) NOT NULL,
namespace VARCHAR2(200) NOT NULL,
privateData LONG NOT NULL,
CONSTRAINT ofPrivate_pk PRIMARY KEY (username, name, namespace)
);
CREATE TABLE ofOffline (
username VARCHAR2(64) NOT NULL,
messageID INTEGER NOT NULL,
......@@ -377,7 +368,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
-- Entry for admin user
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
......@@ -37,15 +37,6 @@ CREATE INDEX ofUserFlag_sTime_idx ON ofUserFlag (startTime);
CREATE INDEX ofUserFlag_eTime_idx ON ofUserFlag (endTime);
CREATE TABLE ofPrivate (
username VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
namespace VARCHAR(200) NOT NULL,
privateData TEXT NOT NULL,
CONSTRAINT ofPrivate_pk PRIMARY KEY (username, name, namespace)
);
CREATE TABLE ofOffline (
username VARCHAR(64) NOT NULL,
messageID INTEGER NOT NULL,
......@@ -385,7 +376,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
-- Entry for admin user
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
......@@ -35,15 +35,6 @@ CREATE INDEX ofUserFlag_sTime_idx ON ofUserFlag (startTime ASC);
CREATE INDEX ofUserFlag_eTime_idx ON ofUserFlag (endTime ASC);
CREATE TABLE ofPrivate (
username NVARCHAR(64) NOT NULL,
name NVARCHAR(100) NOT NULL,
namespace NVARCHAR(200) NOT NULL,
privateData NTEXT NOT NULL,
CONSTRAINT ofPrivate_pk PRIMARY KEY (username, name, namespace)
);
CREATE TABLE ofOffline (
username NVARCHAR(64) NOT NULL,
messageID INTEGER NOT NULL,
......@@ -382,7 +373,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
/* Entry for admin user */
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
......@@ -35,15 +35,6 @@ CREATE INDEX ofUserFlag_sTime_idx ON ofUserFlag (startTime ASC);
CREATE INDEX ofUserFlag_eTime_idx ON ofUserFlag (endTime ASC);
CREATE TABLE ofPrivate (
username NVARCHAR(64) NOT NULL,
name NVARCHAR(100) NOT NULL,
namespace NVARCHAR(200) NOT NULL,
privateData TEXT NOT NULL,
CONSTRAINT ofPrivate_pk PRIMARY KEY (username, name, namespace)
);
CREATE TABLE ofOffline (
username NVARCHAR(64) NOT NULL,
messageID INTEGER NOT NULL,
......@@ -383,7 +374,7 @@ INSERT INTO ofID (idType, id) VALUES (19, 1);
INSERT INTO ofID (idType, id) VALUES (23, 1);
INSERT INTO ofID (idType, id) VALUES (26, 2);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 26);
INSERT INTO ofVersion (name, version) VALUES ('openfire', 28);
/* Entry for admin user */
INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate)
......
-- The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java
-- Update version
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
// The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java
// Update version
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
# The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java
# Update version
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
-- The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java
-- Update version
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
COMMIT;
-- The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java
-- Update version
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
/* The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java */
/* Update version */
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
/* The database update has been implemented in org.jivesoftware.database.bugfix.OF1515.java */
/* Update version */
UPDATE ofVersion SET version = 27 WHERE name = 'openfire';
-- Only when the update in 27 succeeded, drop the table that was used as its source.
DROP TABLE ofPrivate;
-- Update version
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
// Only when the update in 27 succeeded, drop the table that was used as its source.
DROP TABLE ofPrivate;
// Update version
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
# Only when the update in 27 succeeded, drop the table that was used as its source.
DROP TABLE ofPrivate;
# Update version
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
-- Only when the update in 27 succeeded, drop the table that was used as its source.
DROP TABLE ofPrivate;
-- Update version
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
COMMIT;
-- Only when the update in 27 succeeded, drop the table that was used as its source.
DROP TABLE ofPrivate;
-- Update version
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
/* Only when the update in 27 succeeded, drop the table that was used as its source. */
DROP TABLE ofPrivate;
/* Update version */
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
/* Only when the update in 27 succeeded, drop the table that was used as its source. */
DROP TABLE ofPrivate;
/* Update version */
UPDATE ofVersion SET version = 28 WHERE name = 'openfire';
......@@ -30,6 +30,7 @@ import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import org.jivesoftware.database.bugfix.OF1515;
import org.jivesoftware.database.bugfix.OF33;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin;
......@@ -66,7 +67,7 @@ public class SchemaManager {
/**
* Current Openfire database schema version.
*/
private static final int DATABASE_VERSION = 26;
private static final int DATABASE_VERSION = 28;
/**
* Checks the Openfire database schema to ensure that it's installed and up to date.
......@@ -270,6 +271,9 @@ public class SchemaManager {
if (i == 21 && schemaKey.equals("openfire")) {
OF33.executeFix(con);
}
if (i == 27 && schemaKey.equals("openfire")) {
OF1515.executeFix();
}
} catch (Exception e) {
Log.error(e.getMessage(), e);
return false;
......
/*
* Copyright (C) 2018 Ignite Realtime Foundation. 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.database.bugfix;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SchemaManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.pubsub.CollectionNode;
import org.jivesoftware.openfire.pubsub.Node;
import org.jivesoftware.openfire.pubsub.NodeAffiliate;
import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.pubsub.models.OnlyPublishers;
import org.jivesoftware.openfire.pubsub.models.PublisherModel;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* This class implements a fix for a problem identified as issue OF-1515 in the bugtracker of Openfire.
*
* The code in this class is intended to be executed only once, under very strict circumstances. The only class
* responsible for calling this code should be an instance of {@link SchemaManager}. The database update version
* corresponding to this fix is 27.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
* @see <a href="http://www.igniterealtime.org/issues/browse/OF-1515">Openfire bugtracker: OF-1515</a>
*/
public class OF1515
{
private static final Logger Log = LoggerFactory.getLogger( OF1515.class );
public static void executeFix() throws SQLException
{
try
{
Log.info( "Migrating data from Private XML Storage to Pubsub." );
final List<PrivateXmlRecord> oldRecords = getPrivateXmlStorageData();
final List<PubsubRecordData> newRecords = transform( oldRecords );
toPubsubData( newRecords );
Log.info( "Finished mgrating data from Private XML Storage to Pubsub. {} records migrated.", newRecords.size() );
}
catch ( SQLException e )
{
Log.error( "An exception occurred while migrating private XML data to PEP!", e );
throw e;
}
}
/**
* Retrieves all data stored using XEP-0049 Private XML Storage
* @return A collection of data (can be empty, cannot be null).
*/
private static List<PrivateXmlRecord> getPrivateXmlStorageData() throws SQLException
{
Log.info( "Retrieving all data from Private XML Storage." );
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try
{
final List<PrivateXmlRecord> result = new ArrayList<>();
con = DbConnectionManager.getConnection();
stmt = con.prepareStatement( "SELECT privateData, name, username, namespace FROM ofPrivate" );
rs = stmt.executeQuery();
while ( rs.next() ) {
result.add( new PrivateXmlRecord( rs.getString( "privateData" ),
rs.getString( "name" ),
rs.getString( "username" ),
rs.getString( "namespace" ) ) );
}
return result;
}
finally
{
DbConnectionManager.closeConnection( rs, stmt, con );
}
}
/**
* Transforms XML data storage records into Pubsub node records
*
* @param oldRecords The records to transform (cannot be null)
* @return Transformed records (never null, can be empty).
*/
private static List<PubsubRecordData> transform( List<PrivateXmlRecord> oldRecords )
{
Log.info( "Transforming all data from Private XML Storage into Pubsub entities." );
String domain;
try
{
domain = JiveGlobals.getProperty( "xmpp.domain", JiveGlobals.getProperty( "xmpp.fqdn", InetAddress.getLocalHost().getCanonicalHostName() ) ).toLowerCase();
}
catch ( UnknownHostException e )
{
domain = "localhost";
}
final List<PubsubRecordData> result = new ArrayList<>();
for ( final PrivateXmlRecord oldRecord : oldRecords )
{
final PubsubRecordData newRecord = new PubsubRecordData( oldRecord.username + '@' + domain, oldRecord.namespace, oldRecord.privateData );
result.add( newRecord );
}
return result;
}
/**
* Creates appropriate database entries for each pubsub record
* @param newRecords A collection of pubsub representations (can be empty, cannot be null).
*/
private static void toPubsubData( List<PubsubRecordData> newRecords )
{
Log.info( "Writing Pubsub entities." );
Connection con = null;
boolean abortTransaction = false;
try
{
con = DbConnectionManager.getTransactionConnection();
for ( final PubsubRecordData newRecord : newRecords )
{
if ( !hasRootNode( con, newRecord.serviceID ) )
{
writeRootNode( con, newRecord.serviceID );
}
writeNode( con, newRecord );
writeItem( con, newRecord );
writeAffiliation( con, newRecord );
}
}
catch ( SQLException e )
{
abortTransaction = true;
}
finally
{
DbConnectionManager.closeTransactionConnection( con, abortTransaction );
}
}
private static boolean hasRootNode( Connection con, String serviceID ) throws SQLException
{
PreparedStatement pstmt = null;
ResultSet rs = null;
try
{
pstmt = con.prepareStatement("SELECT serviceID FROM ofPubsubNode WHERE serviceID = ? AND nodeID = ? AND parent IS NULL" );
pstmt.setString(1, serviceID);
pstmt.setString(2, serviceID);
rs = pstmt.executeQuery();
return rs.next();
}
finally
{
DbConnectionManager.fastcloseStmt( rs, pstmt );
}
}
private static void writeRootNode( Connection con, String serviceID ) throws SQLException
{
PreparedStatement pstmt = null;
try
{
pstmt = con.prepareStatement( "INSERT INTO ofPubsubNode (serviceID, nodeID, leaf, creationDate, modificationDate, " +
"parent, deliverPayloads, maxPayloadSize, persistItems, maxItems, " +
"notifyConfigChanges, notifyDelete, notifyRetract, presenceBased, " +
"sendItemSubscribe, publisherModel, subscriptionEnabled, configSubscription, " +
"accessModel, payloadType, bodyXSLT, dataformXSLT, creator, description, " +
"language, name, replyPolicy, associationPolicy, maxLeafNodes) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" );
// ServiceID
pstmt.setString( 1, serviceID );
// NodeID
pstmt.setString( 2, serviceID );
// is Leaf?
pstmt.setInt( 3, 0 );
// creation date
pstmt.setString( 4, StringUtils.dateToMillis( new Date() ) );
// modification date
pstmt.setString( 5, StringUtils.dateToMillis( new Date() ) );
// parent
pstmt.setString( 6, null );
// deliver Payloads
pstmt.setInt( 7, 0 );
// max payload size
pstmt.setInt( 8, 0 );
// persist items
pstmt.setInt( 9, 0 );
// max items
pstmt.setInt( 10, 0 );
// NotifyConfigChanges
pstmt.setInt( 11, 1 );
// Notify delete
pstmt.setInt( 12, 1 );
// notidfy retract
pstmt.setInt( 13, 1 );
// presence based
pstmt.setInt( 14, 0 );
// Send item subscribe
pstmt.setInt( 15, 0 );
// publisher model
pstmt.setString( 16, PublisherModel.publishers.getName() );
// subscritpionEnabled
pstmt.setInt( 17, 1 );
// config subscription
pstmt.setInt( 18, 0 );
// access model
pstmt.setString( 19, AccessModel.presence.getName() );
// payload type
pstmt.setString( 20, "" );
// body xslt
pstmt.setString( 21, "" );
// dataform xslt
pstmt.setString( 22, "" );
// creator
pstmt.setString( 23, serviceID );
// description
pstmt.setString( 24, "" );
// language
pstmt.setString( 25, "English" );
// name
pstmt.setString( 26, "" );
// reply policy
pstmt.setString( 27, null );
// association policy
pstmt.setString( 28, CollectionNode.LeafNodeAssociationPolicy.all.name() );
// max leaf nodes
pstmt.setInt( 29, -1 );
pstmt.executeUpdate();
}
finally
{
DbConnectionManager.fastcloseStmt( pstmt );
}
}
private static void writeNode( Connection con, PubsubRecordData record ) throws SQLException
{
PreparedStatement pstmt = null;
try
{
pstmt = con.prepareStatement("INSERT INTO ofPubsubNode (serviceID, nodeID, leaf, creationDate, modificationDate, " +
"parent, deliverPayloads, maxPayloadSize, persistItems, maxItems, " +
"notifyConfigChanges, notifyDelete, notifyRetract, presenceBased, " +
"sendItemSubscribe, publisherModel, subscriptionEnabled, configSubscription, " +
"accessModel, payloadType, bodyXSLT, dataformXSLT, creator, description, " +
"language, name, replyPolicy, associationPolicy, maxLeafNodes) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" );
pstmt.setString(1, record.serviceID);
pstmt.setString(2, record.nodeID);
pstmt.setInt(3, record.leaf);
pstmt.setString(4, record.creationDate);
pstmt.setString(5, record.modificationDate);
pstmt.setString(6, record.parent);
pstmt.setInt(7, record.deliverPayloads);
pstmt.setInt(8, record.maxPayloadSize);
pstmt.setInt(9, record.persistPublishedItems);
pstmt.setInt(10, record.maxPublishedItems);
pstmt.setInt(11, record.notifiedOfConfigChanges);
pstmt.setInt(12, record.notifiedOfDelete);
pstmt.setInt(13, record.notifiedOfRetract);
pstmt.setInt(14, record.presenceBasedDelivery);
pstmt.setInt(15, record.sendItemsubscribe);
pstmt.setString(16, record.publisherModel);
pstmt.setInt(17, record.subscriptionEnabled);
pstmt.setInt(18, record.subscriptionConfigurationRequired);
pstmt.setString(19, record.accessModel);
pstmt.setString(20, record.payloadType);
pstmt.setString(21, record.bodyXSLT);
pstmt.setString(22, record.dataformXSLT);
pstmt.setString(23, record.creator);
pstmt.setString(24, record.description);
pstmt.setString(25, record.language);
pstmt.setString(26, record.name);
pstmt.setString(27, record.replyPolicy);
pstmt.setString(28, record.associationPolicy);
pstmt.setInt(29, record.maxLeafNodes);
pstmt.executeUpdate();
}
finally
{
DbConnectionManager.fastcloseStmt( pstmt );
}
}
private static void writeItem( Connection con, PubsubRecordData record ) throws SQLException
{
PreparedStatement pstmt = null;
try
{
pstmt = con.prepareStatement("INSERT INTO ofPubsubItem (serviceID,nodeID,id,jid,creationDate,payload) VALUES (?,?,?,?,?,?)");
pstmt.setString(1, record.serviceID);
pstmt.setString(2, record.nodeID);
pstmt.setString(3, record.itemID);
pstmt.setString(4, record.creator);
pstmt.setString(5, record.creationDate);
pstmt.setString(6, record.payload);
pstmt.executeUpdate();
}
finally
{
DbConnectionManager.fastcloseStmt( pstmt );
}
}
private static void writeAffiliation( Connection con, PubsubRecordData record ) throws SQLException
{
PreparedStatement pstmt = null;
try
{
pstmt = con.prepareStatement("INSERT INTO ofPubsubAffiliation (serviceID,nodeID,jid,affiliation) VALUES (?,?,?,?)" );
pstmt.setString(1, record.serviceID);
pstmt.setString(2, record.nodeID);
pstmt.setString(3, record.creator);
pstmt.setString( 4, NodeAffiliate.Affiliation.owner.name() );
pstmt.executeUpdate();
}
finally
{
DbConnectionManager.fastcloseStmt( pstmt );
}
}
/**
* Representation of a data record stored in the XML private data storage.
*/
private static class PrivateXmlRecord
{
final String privateData; // which is a string representation of a XML element, but there's no need to deserialize for migration purposes.
final String name;
final String username;
final String namespace;
private PrivateXmlRecord( String privateData, String name, String username, String namespace )
{
this.privateData = privateData;
this.name = name;
this.username = username;
this.namespace = namespace;
}
}
/**
* Representation of a data record stored as a PEP node.
*/
private static class PubsubRecordData
{
final String serviceID; // JID
final String nodeID; // namespace
final int leaf = 1;
final String creationDate = StringUtils.dateToMillis( new Date() );
final String modificationDate = creationDate;
final String parent; // JID
final int deliverPayloads = 1;
final int maxPayloadSize = 5120;
final int persistPublishedItems = 1;
final int maxPublishedItems = 1;
final int notifiedOfConfigChanges = 1;
final int notifiedOfDelete = 1;
final int notifiedOfRetract = 1;
final int presenceBasedDelivery = 0;
final int sendItemsubscribe = 1;
final String publisherModel = OnlyPublishers.publishers.getName();
final int subscriptionEnabled = 1;
final int subscriptionConfigurationRequired = 0;
final String accessModel = AccessModel.whitelist.getName();
final String payloadType = "";
final String bodyXSLT = "";
final String dataformXSLT = "";
final String creator; // JID
final String description = "";
final String language = "English";
final String name = "";
final String replyPolicy = Node.ItemReplyPolicy.owner.name();
final String associationPolicy = null;
final int maxLeafNodes = 0;
final String itemID = "current";
final String payload;
private PubsubRecordData( String jid, String namespace, String payload )
{
this.serviceID = jid;
this.nodeID = namespace;
this.parent = jid;
this.creator = jid;
this.payload = payload;
}
}
}
......@@ -16,60 +16,50 @@
package org.jivesoftware.openfire;
import java.io.StringReader;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.pep.PEPService;
import org.jivesoftware.openfire.pep.PEPServiceManager;
import org.jivesoftware.openfire.pubsub.LeafNode;
import org.jivesoftware.openfire.pubsub.Node;
import org.jivesoftware.openfire.pubsub.PubSubEngine;
import org.jivesoftware.openfire.pubsub.PublishedItem;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.JID;
import java.util.Collections;
/**
* Private storage for user accounts (JEP-0049). It is used by some XMPP systems
* for saving client settings on the server.
* Private storage for user accounts (XEP-0049). It is used by some XMPP systems for saving client settings on the server.
*
* This implementation uses the Personal Eventing Protocol implementation to store and retrieve data. This ensures that
* XEP-0049 operates on the same data as XEP-0223.
*
* @author Iain Shigeoka
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class PrivateStorage extends BasicModule implements UserEventListener {
public class PrivateStorage extends BasicModule {
private static final Logger Log = LoggerFactory.getLogger(PrivateStorage.class);
private static final String LOAD_PRIVATE =
"SELECT privateData FROM ofPrivate WHERE username=? AND name=? AND namespace=?";
private static final String INSERT_PRIVATE =
"INSERT INTO ofPrivate (privateData, name, username, namespace) VALUES (?,?,?,?)";
private static final String UPDATE_PRIVATE =
"UPDATE ofPrivate SET privateData=? WHERE name=? AND username=? AND namespace=?";
private static final String DELETE_PRIVATES =
"DELETE FROM ofPrivate WHERE username=?";
private static final int POOL_SIZE = 10;
// Currently no delete supported, we can detect an add of an empty element and
// use that to signal a delete but that optimization doesn't seem necessary.
// private static final String DELETE_PRIVATE =
// "DELETE FROM ofPrivate WHERE userID=? AND name=? AND namespace=?";
private boolean enabled = JiveGlobals.getBooleanProperty("xmpp.privateStorageEnabled", true);
/**
* Pool of SAX Readers. SAXReader is not thread safe so we need to have a pool of readers.
* PubSub 7.1.5 specificy Publishing Options that are applicable to private data storage (as described in XEP-0223).
*/
private BlockingQueue<SAXReader> xmlReaders = new LinkedBlockingQueue<>(POOL_SIZE);
private static final DataForm PRIVATE_DATA_PUBLISHING_OPTIONS;
static {
PRIVATE_DATA_PUBLISHING_OPTIONS = new DataForm( DataForm.Type.submit );
PRIVATE_DATA_PUBLISHING_OPTIONS.addField( "FORM_TYPE", null, FormField.Type.hidden ).addValue( "http://jabber.org/protocol/pubsub#publish-options" );
PRIVATE_DATA_PUBLISHING_OPTIONS.addField( "pubsub#persist_items", null, null ).addValue( "true" );
PRIVATE_DATA_PUBLISHING_OPTIONS.addField( "pubsub#access_model", null, null ).addValue( "whitelist" );
}
private boolean enabled = JiveGlobals.getBooleanProperty("xmpp.privateStorageEnabled", true);
/**
* Constructs a new PrivateStore instance.
......@@ -105,43 +95,44 @@ public class PrivateStorage extends BasicModule implements UserEventListener {
* @param username the username of the account where private data is being stored
*/
public void add(String username, Element data) {
if (enabled) {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
StringWriter writer = new StringWriter();
data.write(writer);
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_PRIVATE);
pstmt.setString(1, username);
pstmt.setString(2, data.getName());
pstmt.setString(3, data.getNamespaceURI());
rs = pstmt.executeQuery();
boolean update = false;
if (rs.next()) {
update = true;
}
DbConnectionManager.fastcloseStmt(rs, pstmt);
if (update) {
pstmt = con.prepareStatement(UPDATE_PRIVATE);
}
else {
pstmt = con.prepareStatement(INSERT_PRIVATE);
}
pstmt.setString(1, writer.toString());
pstmt.setString(2, data.getName());
pstmt.setString(3, username);
pstmt.setString(4, data.getNamespaceURI());
pstmt.executeUpdate();
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
if (!enabled)
{
return;
}
final JID owner = XMPPServer.getInstance().createJID( username, null );
final PEPServiceManager serviceMgr = XMPPServer.getInstance().getIQPEPHandler().getServiceManager();
PEPService pepService = serviceMgr.getPEPService( owner );
if ( pepService == null )
{
pepService = serviceMgr.create( owner );
}
Node node = pepService.getNode( data.getNamespaceURI() );
if ( node == null )
{
PubSubEngine.CreateNodeResponse response = PubSubEngine.createNodeHelper( pepService, owner, pepService.getDefaultNodeConfiguration( true ).getConfigurationForm().getElement(), data.getNamespaceURI(), PRIVATE_DATA_PUBLISHING_OPTIONS );
node = response.newNode;
if ( node == null )
{
Log.error( "Unable to create new PEP node, to be used to store private data. Error condition: {}", response.creationStatus.toXMPP() );
return;
}
}
if (!(node instanceof LeafNode))
{
Log.error( "Unable to store private data into a PEP node. The node that is available is not a leaf node." );
return;
}
data.detach();
final Element item = DocumentHelper.createElement( "item" );
item.addAttribute( "id", "current" );
item.add( data );
((LeafNode) node).publishItems( owner, Collections.singletonList( item ) );
}
/**
......@@ -158,91 +149,26 @@ public class PrivateStorage extends BasicModule implements UserEventListener {
* @param username the username of the account where private data is being stored.
* @return the data stored under the given key or the data element.
*/
public Element get(String username, Element data) {
if (enabled) {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
SAXReader xmlReader = null;
try {
// Get a sax reader from the pool
xmlReader = xmlReaders.take();
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_PRIVATE);
pstmt.setString(1, username);
pstmt.setString(2, data.getName());
pstmt.setString(3, data.getNamespaceURI());
rs = pstmt.executeQuery();
if (rs.next()) {
data.clearContent();
String result = rs.getString(1).trim();
Document doc = xmlReader.read(new StringReader(result));
data = doc.getRootElement();
}
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
// Return the sax reader to the pool
if (xmlReader != null) {
xmlReaders.add(xmlReader);
public Element get(String username, Element data)
{
if (enabled)
{
final PEPServiceManager serviceMgr = XMPPServer.getInstance().getIQPEPHandler().getServiceManager();
final PEPService pepService = serviceMgr.getPEPService( XMPPServer.getInstance().createJID( username, null ) );
if ( pepService != null )
{
final Node node = pepService.getNode( data.getNamespaceURI() );
if ( node != null )
{
final PublishedItem item = node.getPublishedItem( "current" );
if ( item != null )
{
data.clearContent();
data = item.getPayload();
}
}
}
}
return data;
}
@Override
public void userCreated(User user, Map params) {
//Do nothing
}
@Override
public void userDeleting(User user, Map params) {
// Delete all private properties of the user
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_PRIVATES);
pstmt.setString(1, user.getUsername());
pstmt.executeUpdate();
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
@Override
public void userModified(User user, Map params) {
//Do nothing
}
@Override
public void start() throws IllegalStateException {
super.start();
// Initialize the pool of sax readers
for (int i=0; i<POOL_SIZE; i++) {
SAXReader xmlReader = new SAXReader();
xmlReader.setEncoding("UTF-8");
xmlReaders.add(xmlReader);
}
// Add this module as a user event listener so we can delete
// all user properties when a user is deleted
UserEventDispatcher.addListener(this);
}
@Override
public void stop() {
super.stop();
// Clean up the pool of sax readers
xmlReaders.clear();
// Remove this module as a user event listener
UserEventDispatcher.removeListener(this);
}
}
......@@ -56,6 +56,18 @@ public class PEPServiceManager {
private PubSubEngine pubSubEngine = null;
/**
* Retrieves a PEP service -- attempting first from memory, then from the
* database.
*
* @param jid
* the JID of the user that owns the PEP service.
* @return the requested PEP service if found or null if not found.
*/
public PEPService getPEPService(JID jid) {
return getPEPService( jid.toBareJID() );
}
/**
* Retrieves a PEP service -- attempting first from memory, then from the
* database.
......
......@@ -336,7 +336,7 @@ public class PubSubEngine {
if (node == null) {
if (service instanceof PEPService && service.isServiceAdmin(owner) && canAutoCreate( publishOptions ) ) {
// If it is a PEP service & publisher is service owner - auto create nodes.
CreateNodeResponse response = createNodeHelper(service, iq, iq.getChildElement(), publishElement, publishOptions);
CreateNodeResponse response = createNodeHelper(service, iq.getFrom(), iq.getChildElement().element("configure"), publishElement.attributeValue("node"), publishOptions);
if (response.newNode == null) {
// New node creation failed. Since pep#auto-create is advertised
......@@ -1238,7 +1238,7 @@ public class PubSubEngine {
private void createNode(PubSubService service, IQ iq, Element childElement, Element createElement, DataForm publishOptions) {
// Call createNodeHelper and get the node creation status.
CreateNodeResponse response = createNodeHelper(service, iq, childElement, createElement, publishOptions);
CreateNodeResponse response = createNodeHelper(service, iq.getFrom(), childElement.element("configure"), createElement.attributeValue("node"), publishOptions);
if (response.newNode == null) {
// New node creation failed
sendErrorPacket(iq, response.creationStatus, response.pubsubError);
......@@ -1258,7 +1258,7 @@ public class PubSubEngine {
/**
* Response Object returned by createNodeHelper method
*/
private class CreateNodeResponse {
public static class CreateNodeResponse {
public final PacketError.Condition creationStatus;
public final Node newNode;
public final Element pubsubError;
......@@ -1281,19 +1281,21 @@ public class PubSubEngine {
* <br/>NOTE 2: This method calls UserManager::isRegisteredUser(JID) which can block waiting for a response - so
* do not call this method in the same thread in which a response might arrive
*
* @param service The service instance that's responsible for processing (cannot be null)
* @param requester The (full) JID of the entity that performs the action (cannot be null)
* @param configuration Optional Configuration dataform, if user requested to configure the node (can be null)
* @param nodeID The ID of the node to be created, or null when an instant node is to be created.
* @param publishOptions Optional Publishing Options, which are either preconditions or configuration overrides (can be null)
* @return {@link CreateNodeResponse}
*/
private CreateNodeResponse createNodeHelper(PubSubService service, IQ iq, Element childElement, Element createElement, DataForm publishOptions) {
// Get sender of the IQ packet
JID from = iq.getFrom();
public static CreateNodeResponse createNodeHelper(PubSubService service, JID requester, Element configuration, String nodeID, DataForm publishOptions) {
// Verify that sender has permissions to create nodes
if (!service.canCreateNode(from) || (!isComponent(from) && !UserManager.getInstance().isRegisteredUser(from))) {
if (!service.canCreateNode(requester) || (!isComponent(requester) && !UserManager.getInstance().isRegisteredUser(requester))) {
// The user is not allowed to create nodes so return an error
return new CreateNodeResponse(PacketError.Condition.forbidden, null, null);
}
DataForm completedForm = null;
CollectionNode parentNode = null;
String nodeID = createElement.attributeValue("node");
String newNodeID = nodeID;
if (nodeID == null) {
// User requested an instant node
......@@ -1312,11 +1314,11 @@ public class PubSubEngine {
while (service.getNode(newNodeID) != null);
}
boolean collectionType = false;
// Check if user requested to configure the node (using a data form)
Element configureElement = childElement.element("configure");
if (configureElement != null) {
if (configuration!= null) {
// Get the data form that contains the parent nodeID
completedForm = getSentConfigurationForm( configureElement );
completedForm = getSentConfigurationForm( configuration );
}
if (publishOptions != null) {
......@@ -1384,7 +1386,7 @@ public class PubSubEngine {
if (parentNode != null && !collectionType) {
// Check if requester is allowed to add a new leaf child node to the parent node
if (!parentNode.isAssociationAllowed(from)) {
if (!parentNode.isAssociationAllowed(requester)) {
// User is not allowed to add child leaf node to parent node. Return an error.
return new CreateNodeResponse(PacketError.Condition.forbidden, null, null);
}
......@@ -1402,15 +1404,15 @@ public class PubSubEngine {
Node newNode = null;
try {
// TODO Assumed that the owner of the subscription is the bare JID of the subscription JID. Waiting StPeter answer for explicit field.
JID owner = from.asBareJID();
JID owner = requester.asBareJID();
synchronized (newNodeID.intern()) {
if (service.getNode(newNodeID) == null) {
// Create the node
if (collectionType) {
newNode = new CollectionNode(service, parentNode, newNodeID, from);
newNode = new CollectionNode(service, parentNode, newNodeID, requester);
}
else {
newNode = new LeafNode(service, parentNode, newNodeID, from);
newNode = new LeafNode(service, parentNode, newNodeID, requester);
}
// Add the creator as the node owner
newNode.addOwner(owner);
......@@ -1863,7 +1865,7 @@ public class PubSubEngine {
* @return the data form included in the configure element sent by the node owner or
* <tt>null</tt> if none was included or access model was defined.
*/
private DataForm getSentConfigurationForm(Element configureElement) {
private static DataForm getSentConfigurationForm(Element configureElement) {
DataForm completedForm = null;
FormField formField;
Element formElement = configureElement.element(QName.get("x", "jabber:x:data"));
......@@ -2048,7 +2050,7 @@ public class PubSubEngine {
* @param jid
* @return <tt>true</tt> if the JID is a component, <tt>false<.tt> if not.
*/
private boolean isComponent(JID jid) {
private static boolean isComponent(JID jid) {
final RoutingTable routingTable = XMPPServer.getInstance().getRoutingTable();
if (routingTable != null) {
return routingTable.hasComponentRoute(jid);
......@@ -2061,7 +2063,7 @@ public class PubSubEngine {
* @param jid the JID representing the remote server
* @return true if the supplied JID is a connected server session
*/
private boolean isRemoteServer(final JID jid) {
private static boolean isRemoteServer(final JID jid) {
final String jidString = jid.toString();
final SessionManager sessionManager = SessionManager.getInstance();
for (final String incomingServer : sessionManager.getIncomingServers()) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment