/*
* SystemRequestProcessor.java
*
* This work is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* This work is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
* Copyright (c) 2004-2006 Per Cederberg. All rights reserved.
*/
package org.liquidsite.app.admin;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
import org.liquidsite.app.admin.view.AdminView;
import org.liquidsite.core.content.Content;
import org.liquidsite.core.content.ContentException;
import org.liquidsite.core.content.ContentManager;
import org.liquidsite.core.content.ContentSecurityException;
import org.liquidsite.core.content.ContentSelector;
import org.liquidsite.core.content.Domain;
import org.liquidsite.core.content.DomainHost;
import org.liquidsite.core.content.Group;
import org.liquidsite.core.content.Lock;
import org.liquidsite.core.content.Permission;
import org.liquidsite.core.content.PermissionList;
import org.liquidsite.core.content.User;
import org.liquidsite.core.web.Request;
import org.liquidsite.util.log.Log;
/**
* The request processor for the system view in the administration
* site(s).
*
* @author Per Cederberg, <per at percederberg dot net>
* @version 1.0
*/
class SystemRequestProcessor {
/**
* The class logger.
*/
private static final Log LOG = new Log(SystemRequestProcessor.class);
/**
* The name of the XML file containing the backup data. This file
* is packaged inside a ZIP file together with all the binary
* files referenced by the XML.
*/
private static final String BACKUP_FILE = "liquidsite.data";
/**
* Creates a new administration request processor.
*/
public SystemRequestProcessor() {
// Nothing to initialize
}
/**
* Processes a request.
*
* @param request the request object
* @param path the request path
*/
public void process(Request request, String path) {
User user = request.getUser();
if (user != null && user.isSuperUser()) {
processAuthorized(request, path);
} else {
processUnauthorized(request, path);
}
}
/**
* Processes an authorized request. This is a request from a user
* with permissions to access the system view.
*
* @param request the request object
* @param path the request path
*/
private void processAuthorized(Request request, String path) {
String action = request.getParameter("action", "");
if (action.equals("restart")) {
handleRestart(request);
} else if (action.equals("backup")) {
handleBackup(request);
} else if (action.equals("restore")) {
handleRestore(request);
} else {
AdminView.SYSTEM.viewSystem(request);
}
}
/**
* Processes an unauthorized request. This is a request from a
* user without permissions to access the system view.
*
* @param request the request object
* @param path the request path
*/
private void processUnauthorized(Request request, String path) {
LOG.warning("unauthorized access admin system view by user " +
request.getUser());
AdminView.BASE.viewError(request,
"Access denied for the current user.",
"index.html");
}
/**
* Handles the system restart requests.
*
* @param request the request object
*/
private void handleRestart(Request request) {
AdminUtils.getApplication().restart();
AdminView.BASE.viewInfo(request,
"System restarted successfully",
null,
"system.html");
}
/**
* Handles the system backup requests.
*
* @param request the request object
*/
private void handleBackup(Request request) {
SimpleDateFormat df;
String domain;
String name;
File dir;
File file;
try {
if (!validateBackup(request)) {
AdminView.SYSTEM.viewBackup(request);
} else {
df = new SimpleDateFormat("yyyy-MM-dd-HHmm");
domain = request.getParameter("domain");
name = domain + "." + df.format(new Date()) + ".liquidsite";
dir = AdminUtils.getBackupDir();
file = new File(dir, name);
backup(file, domain, request.getUser());
AdminView.BASE.viewInfo(request,
"Backup stored successfully",
null,
"system.html");
}
} catch (ContentException e) {
LOG.error(e.getMessage());
AdminView.BASE.viewError(request,
"Failed to backup: " + e.getMessage(),
"system.html");
} catch (ContentSecurityException e) {
LOG.warning(e.getMessage());
AdminView.BASE.viewError(request,
"Access denied for the current user.",
"system.html");
}
}
/**
* Handles the system restore requests.
*
* @param request the request object
*/
private void handleRestore(Request request) {
File dir;
File file;
String domain;
int mode;
String log;
String str;
try {
if (!validateRestore(request)) {
AdminView.SYSTEM.viewRestore(request);
} else {
dir = AdminUtils.getBackupDir();
file = new File(dir, request.getParameter("backup"));
domain = request.getParameter("domain");
str = request.getParameter("revisions", "");
if (str.equals("latest")) {
mode = 1;
} else if (str.equals("work")) {
mode = 2;
} else {
mode = 0;
}
log = restore(file, domain, mode, request.getUser());
if (log == null || log.length() == 0) {
str = "Successfully restored backup";
log = null;
} else {
str = "Successfully restored backup, but some " +
"elements were omitted:";
}
AdminView.BASE.viewInfo(request, str, log, "system.html");
}
} catch (ContentException e) {
LOG.error(e.getMessage());
AdminView.BASE.viewError(request,
"Restore failed: " + e.getMessage(),
"system.html");
} catch (ContentSecurityException e) {
LOG.warning(e.getMessage());
AdminView.BASE.viewError(request,
"Access denied for the current user.",
"system.html");
}
}
/**
* Validates the backup form.
*
* @param request the request object
*
* @return true if the form parameters validated correctly, or
* false otherwise
*/
private boolean validateBackup(Request request) {
String message;
if (request.getParameter("domain") == null) {
return false;
}
if (request.getParameter("domain", "").equals("")) {
message = "Please select a domain to backup";
request.setAttribute("error", message);
return false;
}
return true;
}
/**
* Validates the restore form.
*
* @param request the request object
*
* @return true if the form parameters validated correctly, or
* false otherwise
*
* @throws ContentException if the database couldn't be accessed
* properly
* @throws ContentSecurityException if some object that wasn't
* readable by the user
*/
private boolean validateRestore(Request request)
throws ContentException, ContentSecurityException {
ContentManager manager = AdminUtils.getContentManager();
String message;
String name;
if (request.getParameter("backup") == null) {
return false;
}
if (request.getParameter("backup", "").equals("")) {
message = "Please select a backup file to restore";
request.setAttribute("error", message);
return false;
}
name = request.getParameter("domain", "");
if (name.equals("")) {
message = "Please enter the name of a domain to create";
request.setAttribute("error", message);
return false;
}
if (manager.getDomain(request.getUser(), name) != null) {
message = "The domain specified already exists";
request.setAttribute("error", message);
return false;
}
for (int i = 0; i < name.length(); i++) {
if (Domain.NAME_CHARS.indexOf(name.charAt(i)) < 0) {
message = "invalid character in domain name: '" +
name.charAt(i) + "'";
request.setAttribute("error", message);
return false;
}
}
return true;
}
/**
* Creates a complete backup for the specified domain.
*
* @param dest the destination file
* @param domainName the name of the domain to backup
* @param user the user performing the operation
*
* @throws ContentException if the database couldn't be accessed
* properly
* @throws ContentSecurityException if some object that wasn't
* readable by the user
*/
private void backup(File dest, String domainName, User user)
throws ContentException, ContentSecurityException {
ContentManager manager = AdminUtils.getContentManager();
ZipOutputStream zip;
PrintWriter writer;
Domain domain;
String message;
try {
zip = new ZipOutputStream(new FileOutputStream(dest));
zip.putNextEntry(new ZipEntry(BACKUP_FILE));
writer = new PrintWriter(new OutputStreamWriter(zip, "UTF-8"));
domain = manager.getDomain(user, domainName);
backupXml(writer, domain, user);
writer.flush();
zip.closeEntry();
backupFile(zip, "", domain.getDirectory());
zip.close();
manager.reset();
} catch (IOException e) {
dest.delete();
message = "IO error while writing to " + dest;
LOG.error(message, e);
throw new ContentException(message, e);
}
}
/**
* Creates a complete XML backup for the specified domain.
*
* @param out the output stream
* @param domain the domain to backup
* @param user the user performing the operation
*
* @throws ContentException if the database couldn't be accessed
* properly
*/
private void backupXml(PrintWriter out, Domain domain, User user)
throws ContentException {
ContentManager manager = AdminUtils.getContentManager();
Group[] groups;
User[] users;
ArrayList hosts;
DomainHost host;
Content[] content;
ContentSelector selector;
int count;
out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.println();
out.println("<liquidsite-data version=\"1\">");
out.print(" <domain name=\"");
out.print(AdminUtils.getXmlString(domain.getName()));
out.print("\" description=\"");
out.print(AdminUtils.getXmlString(domain.getDescription()));
out.print("\" created=\"");
out.print(domain.getCreatedDate().getTime());
out.print("\" modified=\"");
out.print(domain.getModifiedDate().getTime());
if (domain.getMailFrom() != null) {
out.print("\" mailfrom=\"");
out.print(AdminUtils.getXmlString(domain.getMailFrom()));
}
out.println("\">");
hosts = domain.getHosts();
for (int i = 0; i < hosts.size(); i++) {
host = (DomainHost) hosts.get(i);
out.print(" <host name=\"");
out.print(AdminUtils.getXmlString(host.getName()));
out.print("\" description=\"");
out.print(AdminUtils.getXmlString(host.getDescription()));
out.println("\" />");
}
groups = manager.getGroups(domain, "");
for (int i = 0; i < groups.length; i++) {
backupXml(out, groups[i]);
}
count = manager.getUserCount(domain, "");
for (int i = 0; i < count; i += 100) {
users = manager.getUsers(domain, "", i, 100);
for (int j = 0; j < users.length; j++) {
backupXml(out, users[j]);
}
}
backupXml(out, domain.getPermissions());
selector = new ContentSelector(domain);
selector.requireRootParent();
selector.sortByCategory(false);
count = manager.getContentCount(selector);
for (int i = 0; i < count; i += 100) {
selector.limitResults(i, 100);
content = manager.getContentObjects(user, selector);
for (int j = 0; j < content.length; j++) {
backupXml(out, content[j], user);
}
}
out.println(" </domain>");
out.println("</liquidsite-data>");
}
/**
* Creates an XML backup for the specified group.
*
* @param out the output stream
* @param group the group to backup
*/
private void backupXml(PrintWriter out, Group group) {
out.print(" <group name=\"");
out.print(AdminUtils.getXmlString(group.getName()));
out.print("\" description=\"");
out.print(AdminUtils.getXmlString(group.getDescription()));
if (group.isPublic()) {
out.print("\" public=\"public");
}
out.print("\" comment=\"");
out.print(AdminUtils.getXmlString(group.getComment()));
out.println("\" />");
}
/**
* Creates an XML backup for the specified user.
*
* @param out the output stream
* @param user the user to backup
*
* @throws ContentException if the database couldn't be accessed
* properly
*/
private void backupXml(PrintWriter out, User user)
throws ContentException {
Group[] groups;
out.print(" <user name=\"");
out.print(AdminUtils.getXmlString(user.getName()));
out.print("\" password=\"");
out.print(AdminUtils.getXmlString(user.getPassword()));
if (!user.getEnabled()) {
out.print("\" disabled=\"disabled");
}
out.print("\" realname=\"");
out.print(AdminUtils.getXmlString(user.getRealName()));
out.print("\" email=\"");
out.print(AdminUtils.getXmlString(user.getEmail()));
out.print("\" comment=\"");
out.print(AdminUtils.getXmlString(user.getComment()));
out.println("\">");
groups = user.getGroups();
for (int i = 0; i < groups.length; i++) {
out.print(" <member group=\"");
out.print(AdminUtils.getXmlString(groups[i].getName()));
out.println("\" />");
}
out.println(" </user>");
}
/**
* Creates an XML backup for the specified content object.
*
* @param out the output stream
* @param content the content to backup
* @param user the user performing the operation
*
* @throws ContentException if the database couldn't be accessed
* properly
*/
private void backupXml(PrintWriter out, Content content, User user)
throws ContentException {
ContentManager manager = AdminUtils.getContentManager();
Content[] contents;
ContentSelector selector;
Iterator iter;
String str;
Date date;
int count;
out.print(" <content id=\"");
out.print(content.getId());
out.print("\" category=\"");
out.print(content.getCategory());
out.println("\">");
contents = content.getAllRevisions();
for (int i = 0; i < contents.length; i++) {
out.print(" <revision nr=\"");
out.print(contents[i].getRevisionNumber());
out.print("\" name=\"");
out.print(AdminUtils.getXmlString(contents[i].getName()));
out.print("\" parent=\"");
out.print(contents[i].getParentId());
out.print("\" online=\"");
date = contents[i].getOnlineDate();
if (date == null) {
out.print(0);
} else {
out.print(date.getTime());
}
out.print("\" offline=\"");
date = contents[i].getOfflineDate();
if (date == null) {
out.print(0);
} else {
out.print(date.getTime());
}
out.print("\" modified=\"");
date = contents[i].getModifiedDate();
if (date == null) {
out.print(0);
} else {
out.print(date.getTime());
}
out.print("\" author=\"");
out.print(AdminUtils.getXmlString(contents[i].getAuthorName()));
out.print("\" comment=\"");
out.print(AdminUtils.getXmlString(contents[i].getComment()));
out.println("\">");
iter = contents[i].getAttributeNames();
while (iter.hasNext()) {
str = iter.next().toString();
out.print(" <attribute name=\"");
out.print(AdminUtils.getXmlString(str));
out.print("\">");
str = contents[i].getAttribute(str);
out.print(AdminUtils.getXmlString(str));
out.println("</attribute>");
}
out.println(" </revision>");
}
backupXml(out, content.getPermissions(false));
backupXml(out, content.getLock());
out.println(" </content>");
selector = new ContentSelector(content.getDomain());
selector.requireParent(content);
selector.sortById(true);
count = manager.getContentCount(selector);
for (int i = 0; i < count; i += 100) {
selector.limitResults(i, 100);
contents = manager.getContentObjects(user, selector);
for (int j = 0; j < contents.length; j++) {
backupXml(out, contents[j], user);
}
}
}
/**
* Creates an XML backup for the specified permission list.
*
* @param out the output stream
* @param permlist the permission list to backup
*/
private void backupXml(PrintWriter out, PermissionList permlist) {
Permission[] perms;
if (permlist != null && !permlist.isEmpty()) {
perms = permlist.getPermissions();
for (int i = 0; i < perms.length; i++) {
if (permlist.getContentId() == 0) {
out.print(" ");
} else {
out.print(" ");
}
out.print("<permission user=\"");
out.print(AdminUtils.getXmlString(perms[i].getUserName()));
out.print("\" group=\"");
out.print(AdminUtils.getXmlString(perms[i].getGroupName()));
out.print("\" flags=\"");
out.print(perms[i].getRead() ? "r" : "");
out.print(perms[i].getWrite() ? "w" : "");
out.print(perms[i].getPublish() ? "p" : "");
out.print(perms[i].getAdmin() ? "a" : "");
out.println("\" />");
}
}
}
/**
* Creates an XML backup for the specified content lock.
*
* @param out the output stream
* @param lock the content lock to backup, or null
*/
private void backupXml(PrintWriter out, Lock lock) {
if (lock != null) {
out.print(" <lock user=\"");
out.print(AdminUtils.getXmlString(lock.getUserName()));
out.print("\" acquired=\"");
out.print(lock.getAcquiredDate().getTime());
out.println("\" />");
}
}
/**
* Creates a ZIP file backup of the specified file or directory.
* In the case of a directory, all the files in the directory
* will be written to the backup.
*
* @param out the output ZIP stream
* @param dir the base directory name
* @param file the file to backup
*
* @throws IOException if the file couldn't be read correctly
*/
private void backupFile(ZipOutputStream out, String dir, File file)
throws IOException {
File[] files;
FileInputStream in;
byte[] buffer;
int size;
if (file.isDirectory()) {
files = file.listFiles();
for (int i = 0; i < files.length; i++) {
backupFile(out, dir + file.getName() + "/", files[i]);
}
} else {
out.putNextEntry(new ZipEntry(dir + file.getName()));
in = new FileInputStream(file);
buffer = new byte[4096];
while ((size = in.read(buffer)) > 0) {
out.write(buffer, 0, size);
}
in.close();
out.closeEntry();
}
}
/**
* Restores a complete backup to the specified domain. In case of
* failure this method attempts to not leave any partial data
* behind.
*
* @param file the backup file
* @param domain the name of the domain to create
* @param mode the content revision policy
* @param user the user performing the operation
*
* @return the restore error log, or
* an empty string if no error occurred
*
* @throws ContentException if the database couldn't be accessed
* properly
*/
private String restore(File file, String domain, int mode, User user)
throws ContentException {
ZipFile zip = null;
ZipEntry entry;
SAXParser parser;
XmlRestoreHandler handler;
HashMap files;
Iterator iter;
String name;
String message;
try {
zip = new ZipFile(file);
entry = zip.getEntry(BACKUP_FILE);
if (entry == null) {
message = "failed to locate XML data file " + BACKUP_FILE;
LOG.error(message);
throw new ContentException(message);
}
handler = new XmlRestoreHandler(domain, mode, user);
parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(zip.getInputStream(entry), handler);
files = handler.getContentFiles();
iter = files.keySet().iterator();
while (iter.hasNext()) {
name = iter.next().toString();
entry = zip.getEntry(name);
restoreFile(zip, entry, (File) files.get(name));
}
AdminUtils.getContentManager().reset();
return handler.getLog();
} catch (IOException e) {
message = "IO error while reading " + file;
LOG.error(message, e);
throw new ContentException(message, e);
} catch (ParserConfigurationException e) {
message = "XML parser configuration error";
LOG.error(message, e);
throw new ContentException(message, e);
} catch (SAXException e) {
restoreUndo(domain, user);
if (e.getException() == null) {
message = "XML parser error while reading " + file;
LOG.error(message, e);
throw new ContentException(message, e);
} else {
message = "error while reading " + file;
LOG.error(message, e.getException());
throw new ContentException(message, e.getException());
}
} finally {
if (zip != null) {
try {
zip.close();
} catch (IOException ignore) {
// Do nothing
}
}
}
}
/**
* Restores a file from a ZIP archive. The contents of the file
* will be saved in the specified file.
*
* @param zip the ZIP archive
* @param entry the ZIP archive entry to extract
* @param dest the destination file
*
* @throws IOException if the file date couldn't be extracted or
* written correctly
*/
private void restoreFile(ZipFile zip, ZipEntry entry, File dest)
throws IOException {
InputStream in;
FileOutputStream out;
byte[] buffer = new byte[4096];
int size;
dest.getParentFile().mkdirs();
in = zip.getInputStream(entry);
out = new FileOutputStream(dest);
try {
do {
size = in.read(buffer);
if (size > 0) {
out.write(buffer, 0, size);
}
} while (size > 0);
} finally {
try {
in.close();
} catch (IOException ignore) {
// Do nothing
}
try {
out.close();
} catch (IOException ignore) {
// Do nothing
}
}
}
/**
* Attempts to undo a restore operation. This method will remove
* the domain specified if it exists and the specified user has
* permission to remove it.
*
* @param domainName the name of the domain
* @param user the user
*/
private void restoreUndo(String domainName, User user) {
ContentManager manager;
Domain domain;
try {
manager = AdminUtils.getContentManager();
domain = manager.getDomain(user, domainName);
if (domain != null) {
domain.delete(user);
}
} catch (ContentException e) {
LOG.error(e.getMessage());
} catch (ContentSecurityException e) {
LOG.warning(e.getMessage());
}
}
}