com.zimbra.cs.service.UserServletContext.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.service.UserServletContext.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
 *
 * This program 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,
 * version 2 of the License.
 *
 * This program 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, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */
package com.zimbra.cs.service;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.zip.GZIPInputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.zimbra.common.account.Key.AccountBy;
import com.zimbra.common.mime.ContentDisposition;
import com.zimbra.common.mime.ContentType;
import com.zimbra.common.mime.MimeConstants;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.util.ByteUtil;
import com.zimbra.common.util.DateUtil;
import com.zimbra.common.util.HttpUtil;
import com.zimbra.common.util.L10nUtil;
import com.zimbra.common.util.L10nUtil.MsgKey;
import com.zimbra.common.util.StringUtil;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.AuthToken;
import com.zimbra.cs.account.GuestAccount;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.fb.FreeBusyQuery;
import com.zimbra.cs.index.SortBy;
import com.zimbra.cs.mailbox.MailItem;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.cs.mailbox.OperationContext;
import com.zimbra.cs.service.formatter.Formatter;
import com.zimbra.cs.service.formatter.FormatterFactory.FormatType;
import com.zimbra.cs.service.util.ItemId;
import com.zimbra.cs.servlet.CsrfFilter;
import com.zimbra.cs.servlet.util.CsrfUtil;

public class UserServletContext {
    public final HttpServletRequest req;
    public final HttpServletResponse resp;
    public final UserServlet servlet;
    public final Map<String, String> params;
    public FormatType format;
    public Formatter formatter;
    public boolean cookieAuthHappened;
    public boolean basicAuthHappened;
    public boolean qpAuthHappened;
    public String accountPath;
    public AuthToken authToken;
    public String itemPath;
    public String extraPath;
    public ItemId itemId;
    public MailItem target;
    public SortBy sortBy;
    public FakeFolder fakeTarget = null;
    public int[] reqListIds;
    public ArrayList<Item> requestedItems;
    public boolean fromDumpster;
    public boolean wantCustomHeaders = true;
    public int imapId = -1;
    public boolean sync;
    private Account authAccount;
    public Account targetAccount;
    public Mailbox targetMailbox;
    public OperationContext opContext;
    private Locale locale;
    private long mStartTime = -2;
    private long mEndTime = -2;
    private Throwable error;
    private boolean csrfAuthSucceeded;

    /**
     * @return the csrfAuthSucceeded
     */
    public boolean isCsrfAuthSucceeded() {
        return csrfAuthSucceeded;
    }

    /**
     * @param csrfAuthSucceeded the csrfAuthSucceeded to set
     */
    public void setCsrfAuthSucceeded(boolean csrfAuthSucceeded) {
        this.csrfAuthSucceeded = csrfAuthSucceeded;
    }

    public static class Item {
        public int id;
        public String acctId;
        public int ver;
        public boolean versioned;
        public MailItem mailItem;

        public Item(String itemId, Account targetAccount) throws ServiceException {
            String[] vals = itemId.split("\\.");
            ItemId iid = new ItemId((vals.length > 0) ? vals[0] : itemId,
                    targetAccount == null ? (String) null : targetAccount.getId());
            id = iid.getId();
            if (targetAccount != null && !targetAccount.getId().equals(iid.getAccountId())) {
                acctId = iid.getAccountId();
            }
            if (vals.length == 2) {
                versioned = true;
                ver = Integer.parseInt(vals[1]);
            }
        }
    }

    private static class ItemIterator implements Iterator<MailItem> {

        private final Iterator<Item> items;

        public ItemIterator(ArrayList<Item> items) {
            this.items = items.iterator();
        }

        @Override
        public boolean hasNext() {
            return items.hasNext();
        }

        @Override
        public MailItem next() {
            return items.next().mailItem;
        }

        @Override
        public void remove() {
        }
    }

    public static class FakeFolder {
        private final String account;
        private final String path;
        private final String name;

        public FakeFolder(String targetAccount, String calPath, String calName) {
            account = targetAccount;
            path = calPath;
            name = calName;
        }

        public String getAccount() {
            return account;
        }

        public String getPath() {
            return path;
        }

        public String getName() {
            return name;
        }
    }

    public UserServletContext(HttpServletRequest request, HttpServletResponse response, UserServlet srvlt)
            throws UserServletException, ServiceException {
        this.req = request;
        this.resp = response;
        this.servlet = srvlt;
        this.params = HttpUtil.getURIParams(request);
        this.sortBy = getSortBy();

        //rest url override for locale
        String language = this.params.get(UserServlet.QP_LANGUAGE);
        if (language != null) {
            String country = this.params.get(UserServlet.QP_COUNTRY);
            if (country != null) {
                String variant = this.params.get(UserServlet.QP_VARIANT);
                if (variant != null) {
                    this.locale = new Locale(language, country, variant);
                } else {
                    this.locale = new Locale(language, country);
                }
            } else {
                this.locale = new Locale(language);
            }
        }

        parseParams(request, authToken);
    }

    protected void parseParams(HttpServletRequest request, AuthToken authToken)
            throws UserServletException, ServiceException {
        Provisioning prov = Provisioning.getInstance();

        String pathInfo = request.getPathInfo();
        if (pathInfo == null || pathInfo.equals("/") || pathInfo.equals("") || !pathInfo.startsWith("/"))
            throw new UserServletException(HttpServletResponse.SC_BAD_REQUEST,
                    L10nUtil.getMessage(MsgKey.errInvalidPath, request));
        int pos = pathInfo.indexOf('/', 1);
        if (pos == -1)
            pos = pathInfo.length();
        if (pos < 1)
            throw new UserServletException(HttpServletResponse.SC_BAD_REQUEST,
                    L10nUtil.getMessage(MsgKey.errInvalidPath, request));

        this.accountPath = pathInfo.substring(1, pos).toLowerCase();

        if (pos < pathInfo.length()) {
            this.itemPath = pathInfo.substring(pos + 1);
            if (itemPath.equals(""))
                itemPath = "/";
        } else {
            itemPath = "/";
        }
        this.extraPath = this.params.get(UserServlet.QP_NAME);
        this.format = FormatType.fromString(this.params.get(UserServlet.QP_FMT));
        String id = this.params.get(UserServlet.QP_ID);
        try {
            this.itemId = id == null ? null : new ItemId(id, (String) null);
        } catch (ServiceException e) {
            throw new UserServletException(HttpServletResponse.SC_BAD_REQUEST,
                    L10nUtil.getMessage(MsgKey.errInvalidId, request));
        }

        String imap = this.params.get(UserServlet.QP_IMAP_ID);
        try {
            this.imapId = imap == null ? -1 : Integer.parseInt(imap);
        } catch (NumberFormatException nfe) {
            throw new UserServletException(HttpServletResponse.SC_BAD_REQUEST,
                    L10nUtil.getMessage(MsgKey.errInvalidImapId, request));
        }

        if (this.format != null) {
            this.formatter = UserServlet.getFormatter(this.format);
            if (this.formatter == null) {
                throw new UserServletException(HttpServletResponse.SC_NOT_IMPLEMENTED,
                        L10nUtil.getMessage(MsgKey.errNotImplemented, request));
            } else {
                formatter.validateParams(this);
            }
        }

        // see if we can get target account or not
        if (itemId != null && itemId.getAccountId() != null) {
            targetAccount = prov.get(AccountBy.id, itemId.getAccountId(), authToken);
        } else if (accountPath.equals("~")) {
            // can't resolve this yet
        } else {
            if (accountPath.startsWith("~")) {
                accountPath = accountPath.substring(1);
            }
            targetAccount = prov.get(AccountBy.name, accountPath, authToken);
        }

        String listParam = this.params.get(UserServlet.QP_LIST);
        if (listParam != null && listParam.length() > 0) {
            String[] ids = listParam.split(",");
            requestedItems = new ArrayList<Item>();
            reqListIds = new int[ids.length];
            String proxyAcct = null;
            for (int i = 0; i < ids.length; ++i) {
                Item item = new Item(ids[i], targetAccount);
                requestedItems.add(item);
                reqListIds[i] = item.id;
                if (targetAccount != null && !targetAccount.getId().equals(item.acctId)) {
                    if (proxyAcct != null && !proxyAcct.equals(item.acctId)) {
                        throw ServiceException.INVALID_REQUEST(
                                "Cross account multi list is not supported. already requested item from "
                                        + proxyAcct + " also found " + item.acctId + ":" + item.id,
                                null);
                    } else if (proxyAcct == null) {
                        proxyAcct = item.acctId;
                    }
                }
            }
            if (proxyAcct != null) {
                targetAccount = prov.get(AccountBy.id, proxyAcct, authToken);
            }
        }

        String dumpsterParam = params.get(UserServlet.QP_DUMPSTER);
        fromDumpster = (dumpsterParam != null && !dumpsterParam.equals("0")
                && !dumpsterParam.equalsIgnoreCase("false"));
    }

    public Locale getLocale() {
        return locale != null ? locale : req.getLocale();
    }

    public void setAuthAccount(Account value) throws ServiceException {
        authAccount = value;
        if (locale == null && authAccount != null && authAccount.getLocale() != null) {
            locale = authAccount.getLocale();
        }
    }

    public Account getAuthAccount() {
        return authAccount;
    }

    public Iterator<MailItem> getRequestedItems() {
        return new ItemIterator(requestedItems);
    }

    public boolean isUsingAdminPrivileges() {
        return authToken != null && AuthToken.isAnyAdmin(authToken);
    }

    public UserServlet getServlet() {
        return servlet;
    }

    public long getStartTime() {
        if (mStartTime == -2) {
            String st = params.get(UserServlet.QP_START);
            long defaultStartTime = formatter.getDefaultStartTime();
            mStartTime = (st != null) ? DateUtil.parseDateSpecifier(st, defaultStartTime) : defaultStartTime;
        }
        return mStartTime;
    }

    public long getEndTime() {
        if (mEndTime == -2) {
            String et = params.get(UserServlet.QP_END);
            long defaultEndTime = formatter.getDefaultEndTime();
            mEndTime = (et != null) ? DateUtil.parseDateSpecifier(et, defaultEndTime) : defaultEndTime;
        }
        return mEndTime;
    }

    public int getFreeBusyCalendar() {
        int folder = FreeBusyQuery.CALENDAR_FOLDER_ALL;
        String str = params.get(UserServlet.QP_FREEBUSY_CALENDAR);
        if (str != null) {
            try {
                folder = Integer.parseInt(str);
            } catch (NumberFormatException e) {
            }
        }
        return folder;
    }

    public boolean ignoreAndContinueOnError() {
        String val = params.get(UserServlet.QP_IGNORE_ERROR);
        if (val != null) {
            try {
                int n = Integer.parseInt(val);
                return n != 0;
            } catch (NumberFormatException e) {
            }
        }
        return false;
    }

    public boolean preserveAlarms() {
        String val = params.get(UserServlet.QP_PRESERVE_ALARMS);
        if (val != null) {
            try {
                int n = Integer.parseInt(val);
                return n != 0;
            } catch (NumberFormatException e) {
            }
        }
        return false;
    }

    public boolean noHierarchy() {
        String val = params.get(UserServlet.QP_NOHIERARCHY);
        if (val != null) {
            try {
                int n = Integer.parseInt(val);
                return n != 0;
            } catch (NumberFormatException e) {
            }
        }
        return false;
    }

    public String getQueryString() {
        return params.get(UserServlet.QP_QUERY);
    }

    private SortBy getSortBy() throws UserServletException {
        String sort = params.get(UserServlet.QP_SORT);
        if (sort == null) {
            return SortBy.DATE_DESC;
        } else {
            SortBy sortBy = SortBy.of(sort);
            if (sortBy == null) {
                throw UserServletException.badRequest(sort + " is not a valid sort order");
            } else {
                return sortBy;
            }
        }
    }

    /**
     * Shortcut to {@code params.get("charset")}.
     *
     * @return value of charset parameter, or UTF-8 if null
     * @throws ServiceException if the charset name is invalid
     */
    public Charset getCharset() throws ServiceException {
        String charset = params.get("charset");
        if (charset != null) {
            try {
                return Charset.forName(charset);
            } catch (IllegalArgumentException e) {
                throw ServiceException.INVALID_REQUEST("invalid charset: " + charset, e);
            }
        } else {
            return Charsets.UTF_8;
        }
    }

    public boolean cookieAuthAllowed() {
        return getAuth().indexOf(UserServlet.AUTH_COOKIE) != -1;
    }

    public boolean isAuthedAcctGuest() {
        return authAccount != null && authAccount instanceof GuestAccount;
    }

    // bug 42782
    public boolean setCookie() {
        return (!isAuthedAcctGuest() && getAuth().indexOf(UserServlet.AUTH_SET_COOKIE) != -1
                && getAuth().indexOf(UserServlet.AUTH_NO_SET_COOKIE) == -1);
    }

    public boolean basicAuthAllowed() {
        String auth = getAuth();
        return auth.indexOf(UserServlet.AUTH_NO_SET_COOKIE) != -1 || auth.indexOf(UserServlet.AUTH_BASIC) != -1
                || auth.indexOf(UserServlet.AUTH_SET_COOKIE) != -1;
    }

    public boolean queryParamAuthAllowed() {
        return getAuth().indexOf(UserServlet.AUTH_QUERYPARAM) != -1;
    }

    public String getAuth() {
        String a = params.get(UserServlet.QP_AUTH);
        return (a == null || a.length() == 0) ? UserServlet.AUTH_DEFAULT : a;
    }

    public boolean hasPart() {
        String p = getPart();
        return p != null && p.length() > 0;
    }

    public String getPart() {
        return params.get(UserServlet.QP_PART);
    }

    public boolean hasBody() {
        String p = getBody();
        return p != null;
    }

    public String getBody() {
        return params.get(UserServlet.QP_BODY);
    }

    public boolean hasView() {
        String v = getView();
        return v != null && v.length() > 0;
    }

    public String getView() {
        return params.get(UserServlet.QP_VIEW);
    }

    public int getOffset() {
        String s = params.get(UserServlet.QP_OFFSET);
        if (s != null) {
            int offset = Integer.parseInt(s);
            if (offset > 0)
                return offset;
        }
        return 0;
    }

    public int getLimit() {
        String s = params.get(UserServlet.QP_LIMIT);
        if (s != null) {
            int limit = Integer.parseInt(s);
            if (limit > 0)
                return limit;
        }
        return 50;
    }

    public String getTypesString() {
        return params.get(UserServlet.QP_TYPES);
    }

    /** Returns <tt>true</tt> if {@link UserServlet#QP_BODY} is not
     *  specified or is set to a non-zero value. */
    public boolean shouldReturnBody() {
        String bodyVal = params.get(UserServlet.QP_BODY);
        if (bodyVal != null && bodyVal.equals("0"))
            return false;
        return true;
    }

    public void setAnonymousRequest() {
        authAccount = GuestAccount.ANONYMOUS_ACCT;
    }

    public boolean isAnonymousRequest() {
        return authAccount.equals(GuestAccount.ANONYMOUS_ACCT);
    }

    public boolean hasMaxWidth() {
        return getMaxWidth() != null;
    }

    /**
     * Returns the maximum width of the image returned by this request, or
     * {@code null} if the max with is not specified or invalid.
     */
    public Integer getMaxWidth() {
        String s = params.get(UserServlet.QP_MAX_WIDTH);
        if (StringUtil.isNullOrEmpty(s)) {
            return null;
        }
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            UserServlet.log.warn("Ignoring invalid maxWidth value: " + s);
            return null;
        }
    }

    public boolean hasMaxHeight() {
        return getMaxHeight() != null;
    }

    /**
     * Returns the maximum height of the image returned by this request, or
     * {@code null} if the max height is not specified or invalid.
     */
    public Integer getMaxHeight() {
        String s = params.get(UserServlet.QP_MAX_HEIGHT);
        if (StringUtil.isNullOrEmpty(s)) {
            return null;
        }
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            UserServlet.log.warn("Ignoring invalid maxHeight value: " + s);
            return null;
        }
    }

    /** Default maximum upload size for PUT/POST write ops: 10MB. */
    private static final long DEFAULT_MAX_POST_SIZE = 10 * 1024 * 1024;

    // don't use this for a large upload.  use getUpload() instead.
    public byte[] getPostBody() throws ServiceException, IOException, UserServletException {
        long sizeLimit = Provisioning.getInstance().getLocalServer()
                .getLongAttr(Provisioning.A_zimbraFileUploadMaxSize, DEFAULT_MAX_POST_SIZE);
        InputStream is = getRequestInputStream(sizeLimit);
        try {
            return ByteUtil.getContent(is, req.getContentLength(), sizeLimit);
        } finally {
            is.close();
        }
    }

    private static final class UploadInputStream extends InputStream {
        private FileItem fi = null;
        private final InputStream is;
        private long curSize = 0;
        private final long maxSize;
        private long markSize = 0;

        UploadInputStream(InputStream is, long maxSize) {
            this.is = is;
            this.maxSize = maxSize;
        }

        @Override
        public void close() throws IOException {
            try {
                is.close();
            } finally {
                if (fi != null)
                    fi.delete();
                fi = null;
            }
        }

        @Override
        public int available() throws IOException {
            return is.available();
        }

        @Override
        public void mark(int where) {
            is.mark(where);
            markSize = curSize;
        }

        @Override
        public boolean markSupported() {
            return is.markSupported();
        }

        @Override
        public int read() throws IOException {
            int value = is.read();
            if (value != -1)
                check(1);
            return value;
        }

        @Override
        public int read(byte b[]) throws IOException {
            return (int) check(is.read(b));
        }

        @Override
        public int read(byte b[], int off, int len) throws IOException {
            return (int) check(is.read(b, off, len));
        }

        @Override
        public void reset() throws IOException {
            is.reset();
            curSize = markSize;
        }

        @Override
        public long skip(long n) throws IOException {
            return check(is.skip(n));
        }

        private long check(long in) throws IOException {
            if (in > 0) {
                curSize += in;
                if (maxSize > 0 && curSize > maxSize)
                    throw new IOException("upload over " + maxSize + " byte limit");
            }
            return in;
        }
    }

    public InputStream getRequestInputStream() throws IOException, ServiceException, UserServletException {
        return getRequestInputStream(0);
    }

    public InputStream getRequestInputStream(long limit)
            throws IOException, ServiceException, UserServletException {
        String contentType = MimeConstants.CT_APPLICATION_OCTET_STREAM;
        String filename = null;
        InputStream is = null;
        final long DEFAULT_MAX_SIZE = 10 * 1024 * 1024;

        if (limit == 0) {
            if (req.getParameter("lbfums") != null) {
                limit = Provisioning.getInstance().getLocalServer()
                        .getLongAttr(Provisioning.A_zimbraFileUploadMaxSize, DEFAULT_MAX_SIZE);
            } else {
                limit = Provisioning.getInstance().getConfig().getLongAttr(Provisioning.A_zimbraMtaMaxMessageSize,
                        DEFAULT_MAX_SIZE);
            }
        }

        boolean doCsrfCheck = false;
        if (req.getAttribute(CsrfFilter.CSRF_TOKEN_CHECK) != null) {
            doCsrfCheck = (Boolean) req.getAttribute(CsrfFilter.CSRF_TOKEN_CHECK);
        }

        if (ServletFileUpload.isMultipartContent(req)) {
            ServletFileUpload sfu = new ServletFileUpload();

            try {
                FileItemIterator iter = sfu.getItemIterator(req);

                while (iter.hasNext()) {
                    FileItemStream fis = iter.next();

                    if (fis.isFormField()) {
                        is = fis.openStream();
                        params.put(fis.getFieldName(), new String(ByteUtil.getContent(is, -1), "UTF-8"));
                        if (doCsrfCheck && !this.csrfAuthSucceeded) {
                            String csrfToken = params.get(FileUploadServlet.PARAM_CSRF_TOKEN);
                            if (UserServlet.log.isDebugEnabled()) {
                                String paramValue = req.getParameter(UserServlet.QP_AUTH);
                                UserServlet.log.debug(
                                        "CSRF check is: %s, CSRF token is: %s, Authentication recd with request is: %s",
                                        doCsrfCheck, csrfToken, paramValue);
                            }

                            if (!CsrfUtil.isValidCsrfToken(csrfToken, authToken)) {
                                setCsrfAuthSucceeded(Boolean.FALSE);
                                UserServlet.log.debug(
                                        "CSRF token validation failed for account: %s"
                                                + ", Auth token is CSRF enabled:  %s" + "CSRF token is: %s",
                                        authToken, authToken.isCsrfTokenEnabled(), csrfToken);
                                throw new UserServletException(HttpServletResponse.SC_UNAUTHORIZED,
                                        L10nUtil.getMessage(MsgKey.errMustAuthenticate));
                            } else {
                                setCsrfAuthSucceeded(Boolean.TRUE);
                            }

                        }

                        is.close();
                        is = null;
                    } else {
                        is = new UploadInputStream(fis.openStream(), limit);
                        break;
                    }
                }
            } catch (UserServletException e) {
                throw new UserServletException(e.getHttpStatusCode(), e.getMessage(), e);
            } catch (Exception e) {
                throw new UserServletException(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, e.toString());
            }
            if (is == null)
                throw new UserServletException(HttpServletResponse.SC_NO_CONTENT, "No file content");
        } else {
            ContentType ctype = new ContentType(req.getContentType());
            String contentEncoding = req.getHeader("Content-Encoding");

            contentType = ctype.getContentType();
            filename = ctype.getParameter("name");
            if (filename == null || filename.trim().equals(""))
                filename = new ContentDisposition(req.getHeader("Content-Disposition")).getParameter("filename");
            is = new UploadInputStream(contentEncoding != null && contentEncoding.indexOf("gzip") != -1
                    ? new GZIPInputStream(req.getInputStream())
                    : req.getInputStream(), limit);
        }
        if (filename == null || filename.trim().equals(""))
            filename = "unknown";
        else
            params.put(UserServlet.UPLOAD_NAME, filename);
        params.put(UserServlet.UPLOAD_TYPE, contentType);
        ZimbraLog.mailbox.info("UserServlet received file %s - %d request bytes", filename, req.getContentLength());
        return is;
    }

    public void logError(Throwable e) {
        error = e;
    }

    public Throwable getLoggedError() {
        return error;
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this).add("account", accountPath).add("item", itemPath).add("format", format)
                .add("locale", locale).add("extraPath", extraPath).add("itemId", itemId).add("target", target)
                .add("authAccount", authAccount).add("targetAccount", targetAccount)
                .add("targetMailbox", targetMailbox).add("params", params).toString();
    }
}