package hudson.plugins.translation;
import com.trilead.ssh2.crypto.Base64;
import com.sun.mail.util.BASE64EncoderStream;
import hudson.Extension;
import hudson.Util;
import hudson.plugins.translation.Locales.Entry;
import hudson.model.Hudson;
import hudson.model.PageDecorator;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebApp;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.jelly.InternationalizedStringExpressionListener;
import org.kohsuke.stapler.jelly.JellyFacet;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.CipherInputStream;
import static javax.crypto.Cipher.ENCRYPT_MODE;
import static javax.crypto.Cipher.DECRYPT_MODE;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.OutputStreamWriter;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import java.util.UUID;
import java.util.zip.GZIPOutputStream;
import java.util.zip.GZIPInputStream;
import net.sf.json.JSONObject;
/**
* {@link PageDecorator} that adds the translation dialog.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class L10nDecorator extends PageDecorator {
public final ContributedL10nStore store = new ContributedL10nStore();
private final ResourceBundleFactoryImpl bundleFactory = new ResourceBundleFactoryImpl(store);
private final Hudson hudson;
public L10nDecorator() {
super(L10nDecorator.class);
// hook into Stapler to activate contributed l10n
hudson = Hudson.getInstance();
WebApp.get(hudson.servletContext).getFacet(JellyFacet.class).resourceBundleFactory
= bundleFactory;
}
public List<Locales.Entry> listLocales() {
return Locales.LIST;
}
/**
* Activate the recording of the localized resources.
*/
public void startRecording(StaplerRequest request) {
request.setAttribute(InternationalizedStringExpressionListener.class.getName(), new MsgRecorder());
}
public Collection<Msg> getRecording(StaplerRequest request) {
return ((MsgRecorder)request.getAttribute(InternationalizedStringExpressionListener.class.getName())).set;
}
public String encodeRecording(StaplerRequest request) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// string -> gzip -> encrypt -> base64 -> string
PrintStream w = new PrintStream(new GZIPOutputStream(new CipherOutputStream(baos,getCipher(ENCRYPT_MODE))));
for (Msg e : getRecording(request)) {
w.println(e.resourceBundle.getBaseName());
w.println(e.key);
}
w.close();
return new String(Base64.encode(baos.toByteArray()));
}
/**
* Does the opposite of {@link #encodeRecording(StaplerRequest)}.
*/
public List<Msg> decode(StaplerRequest request) throws IOException {
BufferedReader r = new BufferedReader(new InputStreamReader(new GZIPInputStream(new CipherInputStream(
new ByteArrayInputStream(Base64.decode(request.getParameter("bundles").toCharArray())),
getCipher(DECRYPT_MODE)))));
List<Msg> l = new ArrayList<Msg>();
String s;
while((s=r.readLine())!=null) {
l.add(new Msg(bundleFactory.create(s),r.readLine()));
}
return l;
}
private Cipher getCipher(int mode) {
try {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(mode, hudson.getSecretKeyAsAES128());
return cipher;
} catch (GeneralSecurityException e) {
throw new Error(e); // impossible
}
}
/**
* Looks at {@code Accept-Language} header manually and decide which locale
* this user is likely capable of translating.
*/
public String getPrimaryTranslationLocale(StaplerRequest req) {
String lang = req.getHeader("Accept-Language");
if(lang==null) return null;
for (String t : lang.split(",")) {
// ignore q=N
int idx = t.indexOf(';');
if(idx>=0) t=t.substring(0,idx);
// HTTP uses en-US but Java uses en_US
t=t.replace('-','_').toLowerCase(Locale.ENGLISH);
// First, look for an exact matching, so that 'en_US' matches 'en_US' and not 'en_UK' nor 'en'
for (Entry e : Locales.LIST)
if(t.equals(e.lcode))
return e.lcode;
// Eventually, look for the generic locale, so that 'en_US' matches 'en'
for (Entry e : Locales.LIST)
if(t.startsWith(e.lcode))
return e.lcode;
}
return null;
}
public static final class SubmissionEntry {
public final String text, baseName, key, original;
@DataBoundConstructor
public SubmissionEntry(String text, String baseName, String key, String original) {
this.text = text;
this.baseName = baseName;
this.key = key;
this.original = original;
}
public boolean isUpdated() {
return !original.equals(text);
}
}
/**
* Handles the submission from the browser.
*/
public void doSubmit(StaplerRequest req, StaplerResponse rsp, @QueryParameter String locale) throws IOException, ServletException {
JSONObject json = req.getSubmittedForm();
if(hudson.hasPermission(Hudson.ADMINISTER)) {
// let this submission reflected to this Hudson right away
// organize contributions by baseName
Map<String,Properties> updates = new HashMap<String,Properties>();
for (SubmissionEntry e : req.bindJSONToList(SubmissionEntry.class, json.get("entry"))) {
if(!e.isUpdated()) continue;
Properties p = updates.get(e.baseName);
if(p==null) {
p = new Properties();
store.loadTo(locale,e.baseName,p);
updates.put(e.baseName,p);
}
p.put(e.key,e.text);
}
// then write them back
for (Map.Entry<String,Properties> p : updates.entrySet())
store.save(locale,p.getKey(),p.getValue());
bundleFactory.clearCache();
}
json.remove("bundles"); // we don't need this bulky data send to Hudson project website
json.put("id", UUID.randomUUID().toString()); // simplifies the correlation between multiple posting sites
json.put("installation", Util.getDigestOf(hudson.getSecretKey()));
json.put("version", hudson.VERSION);
// write back the data that the browser should then submit to the Hudson website
rsp.setContentType("text/plain;charset=UTF-8");
OutputStreamWriter w = new OutputStreamWriter(new GZIPOutputStream(new BASE64EncoderStream(rsp.getOutputStream())),"UTF-8");
json.write(w);
w.close();
rsp.setStatus(SC_OK);
}
}