Unverified Commit 6c32335b authored by Dave Cridland's avatar Dave Cridland Committed by GitHub

Merge pull request #1051 from guusdk/OF-1515_Migrate-private-xml-storage-to-PEP

OF-1515: Migrate Private XML Storage to PEP
parents d16c186d 820cd403
......@@ -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,61 +16,51 @@
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;
/**
* PubSub 7.1.5 specificy Publishing Options that are applicable to private data storage (as described in XEP-0223).
*/
private static final DataForm PRIVATE_DATA_PUBLISHING_OPTIONS;
// 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=?";
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);
/**
* Pool of SAX Readers. SAXReader is not thread safe so we need to have a pool of readers.
*/
private BlockingQueue<SAXReader> xmlReaders = new LinkedBlockingQueue<>(POOL_SIZE);
/**
* 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;
if (!enabled)
{
return;
}
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();
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 );
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
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;
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
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()) {
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();
String result = rs.getString(1).trim();
Document doc = xmlReader.read(new StringReader(result));
data = doc.getRootElement();
data = item.getPayload();
}
}
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);
}
}
}
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