diff --git a/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java b/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java index 6066300e..9d542425 100644 --- a/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java +++ b/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java @@ -1,7 +1,9 @@ package com.commafeed.frontend.rest.resources; import java.io.StringWriter; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URLDecoder; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; @@ -17,8 +19,10 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import org.apache.commons.fileupload.FileItem; @@ -48,6 +52,7 @@ import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.SubscribeRequest; import com.commafeed.frontend.rest.Enums.ReadType; +import com.commafeed.frontend.utils.FetchFavicon; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.sun.syndication.feed.opml.Opml; @@ -245,6 +250,30 @@ public class FeedREST extends AbstractResourceREST { .getPublicUrl(), 0)).build(); } + @GET + @Path("/favicon") + @ApiOperation(value = "Fetch feed icon", notes = "Fetch icon of a feed") + public Response favicon(@QueryParam("url") String path) { + try { + path = URLDecoder.decode(path, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + byte[] icon = new FetchFavicon().get(path); + ResponseBuilder reponse = Response.ok(icon, "image/x-icon"); + + CacheControl cacheControl = new CacheControl(); + cacheControl.setMaxAge(2592000); + cacheControl.setPrivate(false); + reponse.cacheControl(cacheControl); // trying to replicate "public, max-age=2592000" + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, 1); + reponse.expires(calendar.getTime()); + + return reponse.build(); + } + @POST @Path("/subscribe") @ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed") diff --git a/src/main/java/com/commafeed/frontend/utils/FetchFavicon.java b/src/main/java/com/commafeed/frontend/utils/FetchFavicon.java new file mode 100644 index 00000000..c092c192 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/utils/FetchFavicon.java @@ -0,0 +1,150 @@ +package com.commafeed.frontend.utils; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.jsoup.Connection.Response; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.select.Elements; + +//Inspired/Ported from https://github.com/potatolondon/getfavicon +public class FetchFavicon { + void inf(String message) { + // + } + + static long MIN_ICON_LENGTH = 100; + static long MAX_ICON_LENGTH = 20000; + static String[] ICON_MIMETYPES = new String[] { "image/x-icon", + "image/vnd.microsoft.icon", "image/ico", "image/icon", "text/ico", + "application/ico", "image/x-ms-bmp", "image/x-bmp", "image/gif", + "image/png", "image/jpeg" }; + + static String[] ICON_MIMETYPE_BLACKLIST = new String[] { "application/xml", + "text/html" }; + + boolean in(String[] array, String value) { + for (String i : array) { + if (i.equals(value)) { + return true; + } + } + return false; + } + + boolean isValidIconResponse(Response iconResponse) { + long iconLength = iconResponse.bodyAsBytes().length; + + String iconContentType = iconResponse.header("Content-Type"); + if (!iconContentType.isEmpty()) + iconContentType = iconContentType.split(";")[0]; + + if (iconResponse.statusCode() != 200) { + inf("Status code isn't 200"); + return false; + } + + if (in(ICON_MIMETYPE_BLACKLIST, iconContentType)) { + inf("Content-Type in ICON_MIMETYPE_BLACKLIST"); + return false; + } + + if (iconLength < MIN_ICON_LENGTH) { + inf("Length below MIN_ICON_LENGTH"); + return false; + } + + if (iconLength > MAX_ICON_LENGTH) { + inf("Length greater than MAX_ICON_LENGTH"); + return false; + } + return true; + } + + byte[] iconAtRoot(String targetPath) { + Response rootIconPath; + try { + URL url = new URL(new URL(targetPath), "/favicon.ico"); + inf(url.toString()); + rootIconPath = Jsoup + .connect(url.toString()) + .followRedirects(true) + .ignoreContentType(true).execute(); + } catch (Exception e) { + inf("Failed to retrieve iconAtRoot"); + return null; + } + + if (isValidIconResponse(rootIconPath)) { + return rootIconPath.bodyAsBytes(); + } + return null; + } + + byte[] iconInPage(String targetPath) { + inf("iconInPage, trying " + targetPath); + + Document pageSoup; + try { + pageSoup = Jsoup.connect(targetPath).followRedirects(true).get(); + } catch (Exception e) { + inf("Failed to retrieve page to find icon"); + return null; + } + + Elements pageSoupIcon = pageSoup + .select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]"); + + if (pageSoupIcon.size() == 0) { + return null; + } + String pageIconHref = pageSoupIcon.get(0).attr("href"); + String pageIconPath; + if (pageIconHref.isEmpty()) { + inf("No icon found in page"); + return null; + } + + try { + pageIconPath = new URL(new URL(targetPath), pageIconHref).toString(); + } catch (MalformedURLException e1) { + inf("URL concatination faild"); + return null; + } + + inf("Found unconfirmed iconInPage at"); + + Response pagePathFaviconResult; + try { + pagePathFaviconResult = Jsoup.connect(pageIconPath) + .followRedirects(true).ignoreContentType(true) + .execute(); + } catch (Exception e) { + inf("Failed to retrieve icon found in page"); + return null; + } + + if (isValidIconResponse(pagePathFaviconResult)) { + return pagePathFaviconResult.bodyAsBytes(); + } + inf("Invalid icon found"); + return null; + } + + public byte[] get(String targetPath) { + byte[] icon; + + icon = iconAtRoot(targetPath); + if (icon != null) { + return icon; + } + + icon = iconInPage(targetPath); + if (icon != null) { + return icon; + } + + return null; // or returning default feed + } +}