001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.supportform;
029
030import java.io.ByteArrayInputStream;
031import java.io.File;
032import java.io.InputStream;
033import java.util.ArrayList;
034import java.util.List;
035
036import org.apache.commons.httpclient.Header;
037import org.apache.commons.httpclient.HttpClient;
038import org.apache.commons.httpclient.methods.PostMethod;
039import org.apache.commons.httpclient.methods.multipart.FilePart;
040import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
041import org.apache.commons.httpclient.methods.multipart.Part;
042import org.apache.commons.httpclient.methods.multipart.PartSource;
043import org.apache.commons.httpclient.methods.multipart.StringPart;
044
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import ucar.unidata.util.IOUtil;
049//import ucar.unidata.util.Misc;
050import ucar.unidata.util.WrapperException;
051
052import edu.wisc.ssec.mcidasv.util.BackgroundTask;
053
054/**
055 * Abstraction of a background thread that is used to submit support requests
056 * to the McIDAS-V Help Desk Team.
057 */
058public class Submitter extends BackgroundTask<String> {
059
060    /** Error message to display if the server had problems. */
061    public static final String POST_ERROR = "Server encountered an error while attempting to forward message to mug@ssec.wisc.edu.\n\nPlease try sending email in your email client to mug@ssec.wisc.edu. We apologize for the inconvenience.";
062
063    /** Logging object. */
064    private static final Logger logger = LoggerFactory.getLogger(Submitter.class);
065
066    /** We'll follow up to this many redirects for {@code requestUrl}. */
067    private static final int POST_ATTEMPTS = 5;
068
069    /** Used to gather user input and system information. */
070    private final SupportForm form;
071
072    /** URL that we'll attempt to {@code POST} our requests at.*/
073    private final String requestUrl = "https://www.ssec.wisc.edu/mcidas/misc/mc-v/supportreq/support.php";
074
075    /** Keeps track of the most recent redirect for {@code requestUrl}. */
076    private String validFormUrl = requestUrl;
077
078    /** Number of redirects we've tried since starting. */
079    private int tryCount = 0;
080
081    /** Handy reference to the status code (and more) of our {@code POST}. */
082    private PostMethod method = null;
083
084    /**
085     * Prepare a support request to be sent (off of the event dispatch thread).
086     * 
087     * @param form Support request form to send. Cannot be {@code null}.
088     */
089    public Submitter(final SupportForm form) {
090        this.form = form;
091    }
092
093    /** 
094     * Creates a file attachment that's based upon a real file.
095     * 
096     * @param id The parameter ID. Usually something like 
097     * {@literal "form_data[att_two]"}.
098     * @param file Path to the file that's going to be attached.
099     * 
100     * @return {@code POST}-able file attachment using the name and contents of
101     * {@code file}.
102     */
103    private static FilePart buildRealFilePart(final String id, final String file) {
104        return new FilePart(id, new PartSource() {
105            public InputStream createInputStream() {
106                try {
107                    return IOUtil.getInputStream(file);
108                } catch (Exception e) {
109                    throw new WrapperException("Reading file: "+file, e);
110                }
111            }
112            public String getFileName() {
113                return new File(file).getName();
114            }
115            public long getLength() {
116                return new File(file).length();
117            }
118        });
119    }
120
121    /**
122     * Creates a file attachment that isn't based upon an actual file. Useful 
123     * for something like the {@literal "extra"} attachment where you collect
124     * a bunch of data but don't want to deal with creating a temporary file.
125     * 
126     * @param id Parameter ID. Typically something like 
127     * {@literal "form_data[att_extra]"}.
128     * @param file Fake name of the file. Can be whatever you like.
129     * @param data The actual data to place inside the attachment.
130     * 
131     * @return {@code POST}-able file attachment using a spoofed filename!
132     */
133    private static FilePart buildFakeFilePart(final String id, final String file, final byte[] data) {
134        return new FilePart(id, new PartSource() {
135            public InputStream createInputStream() {
136                return new ByteArrayInputStream(data);
137            }
138            public String getFileName() {
139                return file;
140            }
141            public long getLength() {
142                return data.length;
143            }
144        });
145    }
146
147    /**
148     * Attempts to {@code POST} to {@code url} using the information from 
149     * {@code form}.
150     * 
151     * @param url URL that'll accept the {@code POST}. Typically 
152     * {@link #requestUrl}.
153     * @param form The {@link SupportForm} that contains the data to use in the
154     * support request.
155     * 
156     * @return Big honkin' object that contains the support request.
157     */
158    private static PostMethod buildPostMethod(String url, SupportForm form) {
159        PostMethod method = new PostMethod(url);
160
161        List<Part> parts = new ArrayList<Part>();
162        parts.add(new StringPart("form_data[fromName]", form.getUser()));
163        parts.add(new StringPart("form_data[email]", form.getEmail()));
164        parts.add(new StringPart("form_data[organization]", form.getOrganization()));
165        parts.add(new StringPart("form_data[subject]", form.getSubject()));
166        parts.add(new StringPart("form_data[description]", form.getDescription()));
167        parts.add(new StringPart("form_data[submit]", ""));
168        parts.add(new StringPart("form_data[p_version]", "p_version=ignored"));
169        parts.add(new StringPart("form_data[opsys]", "opsys=ignored"));
170        parts.add(new StringPart("form_data[hardware]", "hardware=ignored"));
171        parts.add(new StringPart("form_data[cc_user]", Boolean.toString(form.getSendCopy())));
172
173        // attach the files the user has explicitly attached.
174        if (form.hasAttachmentOne()) {
175            parts.add(buildRealFilePart("form_data[att_two]", form.getAttachmentOne()));
176        }
177        if (form.hasAttachmentTwo()) {
178            parts.add(buildRealFilePart("form_data[att_three]", form.getAttachmentTwo()));
179        }
180        // if the user wants, attach an XML bundle of the state
181        if (form.canBundleState() && form.getSendBundle()) {
182            parts.add(buildFakeFilePart("form_data[att_state]", form.getBundledStateName(), form.getBundledState()));
183        }
184
185        // attach system properties
186        parts.add(buildFakeFilePart("form_data[att_extra]", form.getExtraStateName(), form.getExtraState()));
187
188        // attach mcidasv.log (if it exists)
189        if (form.canSendLog()) {
190            parts.add(buildRealFilePart("form_data[att_log]", form.getLogPath()));
191        }
192        
193        // attach RESOLV.SRV (if it exists)
194        if (form.canSendResolvSrv()) {
195            parts.add(buildRealFilePart("form_data[att_resolvsrv]", form.getResolvSrvPath()));
196        }
197        
198        // tack on the contents of runMcV.prefs
199        parts.add(buildRealFilePart("form_data[att_prefs]", form.getPrefsPath()));
200
201        Part[] arr = parts.toArray(new Part[0]);
202        MultipartRequestEntity mpr = new MultipartRequestEntity(arr, method.getParams());
203        method.setRequestEntity(mpr);
204        return method;
205    }
206
207    /**
208     * Attempt to POST contents of support request form to {@link #requestUrl}.
209     * 
210     * @throws WrapperException if there was a problem on the server.
211     */
212    protected String compute() {
213        // logic ripped from the IDV's HttpFormEntry#doPost(List, String)
214        try {
215            while ((tryCount++ < POST_ATTEMPTS) && !isCancelled()) {
216                method = buildPostMethod(validFormUrl, form);
217                HttpClient client = new HttpClient();
218                client.executeMethod(method);
219                if (method.getStatusCode() >= 300 && method.getStatusCode() <= 399) {
220                    Header location = method.getResponseHeader("location");
221                    if (location == null) {
222                        return "Error: No 'location' given on the redirect";
223                    }
224                    validFormUrl = location.getValue();
225                    if (method.getStatusCode() == 301) {
226                        logger.warn("form post has been permanently moved to: {}", validFormUrl);
227                    }
228                    continue;
229                }
230                break;
231            }
232            return IOUtil.readContents(method.getResponseBodyAsStream());
233        } catch (Exception e) {
234            throw new WrapperException(POST_ERROR, e);
235        }
236    }
237
238//    protected String compute() {
239//        try {
240//            Misc.sleep(2000);
241//            return "dummy success!";
242//        } catch (Exception e) {
243//            throw new WrapperException(POST_ERROR, e);
244//        }
245//    }
246
247    /**
248     * Handles completion of a support request.
249     * 
250     * @param result Result of {@link #compute()}.
251     * @param exception Exception thrown from {@link #compute()}, if any.
252     * @param cancelled Whether or not the user opted to cancel.
253     */
254    @Override protected void onCompletion(String result, Throwable exception, boolean cancelled) {
255        logger.trace("result={} exception={} cancelled={}", new Object[] { result, exception, cancelled });
256        if (cancelled) {
257            return;
258        }
259
260        if (exception == null) {
261            form.showSuccess();
262        } else {
263            form.showFailure(exception.getMessage());
264        }
265    }
266
267}