weblib: urls -> class
[nikiroo-utils.git] / src / be / nikiroo / fanfix / library / WebLibraryServer.java
index 982d224d03cc7f12c1415dbf4b0dd3324210139d..b6d11f9baa2f1bf91c6743ba4d5e06da5c558639 100644 (file)
@@ -29,9 +29,9 @@ import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.web.WebLibraryServerIndex;
 import be.nikiroo.fanfix.reader.TextOutput;
-import be.nikiroo.utils.CookieUtils;
 import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
+import be.nikiroo.utils.LoginResult;
 import be.nikiroo.utils.NanoHTTPD;
 import be.nikiroo.utils.NanoHTTPD.IHTTPSession;
 import be.nikiroo.utils.NanoHTTPD.Response;
@@ -40,100 +40,40 @@ import be.nikiroo.utils.TraceHandler;
 import be.nikiroo.utils.Version;
 
 public class WebLibraryServer implements Runnable {
-       static private String VIEWER_URL_BASE = "/view/story/";
-       static private String VIEWER_URL = VIEWER_URL_BASE + "{luid}/{chap}/{para}";
-       static private String STORY_URL_BASE = "/story/";
-       static private String STORY_URL = STORY_URL_BASE + "{luid}/{chap}/{para}";
-       static private String STORY_URL_COVER = STORY_URL_BASE + "{luid}/cover";
-       static private String LIST_URL = "/list/";
-
-       private class LoginResult {
-               private boolean success;
+       private class WLoginResult extends LoginResult {
                private boolean rw;
                private boolean wl;
-               private String wookie;
-               private String token;
-               private boolean badLogin;
-               private boolean badToken;
-
-               public LoginResult(String who, String key, String subkey,
-                               boolean success, boolean rw, boolean wl) {
-                       this.success = success;
+               private boolean bl;
+
+               public WLoginResult(boolean badLogin, boolean badCookie) {
+                       super(badLogin, badCookie);
+               }
+
+               public WLoginResult(String who, String key, String subkey, boolean rw,
+                               boolean wl, boolean bl) {
+                       super(who, key, subkey, (rw ? "|rw" : "") + (wl ? "|wl" : "")
+                                       + (bl ? "|bl" : "") + "|");
                        this.rw = rw;
                        this.wl = wl;
-                       this.wookie = CookieUtils.generateCookie(who + key, 0);
-
-                       String opts = "";
-                       if (rw)
-                               opts += "|rw";
-                       if (!wl)
-                               opts += "|wl";
-
-                       this.token = wookie + "~"
-                                       + CookieUtils.generateCookie(wookie + subkey + opts, 0)
-                                       + "~" + opts;
-                       this.badLogin = !success;
+                       this.bl = bl;
                }
 
-               public LoginResult(String token, String who, String key,
+               public WLoginResult(String cookie, String who, String key,
                                List<String> subkeys) {
-
-                       if (token != null) {
-                               String hashes[] = token.split("~");
-                               if (hashes.length >= 2) {
-                                       String wookie = hashes[0];
-                                       String rehashed = hashes[1];
-                                       String opts = hashes.length > 2 ? hashes[2] : "";
-
-                                       if (CookieUtils.validateCookie(who + key, wookie)) {
-                                               if (subkeys == null) {
-                                                       subkeys = new ArrayList<String>();
-                                               }
-                                               subkeys = new ArrayList<String>(subkeys);
-                                               subkeys.add("");
-
-                                               for (String subkey : subkeys) {
-                                                       if (CookieUtils.validateCookie(
-                                                                       wookie + subkey + opts, rehashed)) {
-                                                               this.wookie = wookie;
-                                                               this.token = token;
-                                                               this.success = true;
-
-                                                               this.rw = opts.contains("|rw");
-                                                               this.wl = !opts.contains("|wl");
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               this.badToken = !success;
-                       }
-
-                       // No token -> no bad token
-               }
-
-               public boolean isSuccess() {
-                       return success;
+                       super(cookie, who, key, subkeys,
+                                       subkeys == null || subkeys.isEmpty());
                }
 
                public boolean isRw() {
-                       return rw;
+                       return getOption().contains("|rw|");
                }
 
                public boolean isWl() {
-                       return wl;
+                       return getOption().contains("|wl|");
                }
 
-               public String getToken() {
-                       return token;
-               }
-
-               public boolean isBadLogin() {
-                       return badLogin;
-               }
-
-               public boolean isBadToken() {
-                       return badToken;
+               public boolean isBl() {
+                       return getOption().contains("|bl|");
                }
        }
 
@@ -144,6 +84,9 @@ public class WebLibraryServer implements Runnable {
        private long maxStoryCacheSize;
        private TraceHandler tracer = new TraceHandler();
 
+       private List<String> whitelist;
+       private List<String> blacklist;
+
        public WebLibraryServer(boolean secure) throws IOException {
                Integer port = Instance.getInstance().getConfig()
                                .getInteger(Config.SERVER_PORT);
@@ -158,6 +101,11 @@ public class WebLibraryServer implements Runnable {
 
                setTraceHandler(Instance.getInstance().getTraceHandler());
 
+               whitelist = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_WHITELIST, new ArrayList<String>());
+               blacklist = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_BLACKLIST, new ArrayList<String>());
+
                SSLServerSocketFactory ssf = null;
                if (secure) {
                        String keystorePath = Instance.getInstance().getConfig()
@@ -211,33 +159,22 @@ public class WebLibraryServer implements Runnable {
                                        cookies.put(cookie, session.getCookies().read(cookie));
                                }
 
-                               List<String> whitelist = Instance.getInstance().getConfig()
-                                               .getList(Config.SERVER_WHITELIST);
-                               if (whitelist == null) {
-                                       whitelist = new ArrayList<String>();
-                               }
-
-                               LoginResult login = null;
+                               WLoginResult login = null;
                                Map<String, String> params = session.getParms();
                                String who = session.getRemoteHostName()
                                                + session.getRemoteIpAddress();
                                if (params.get("login") != null) {
                                        login = login(who, params.get("password"),
-                                                       params.get("login"), whitelist);
+                                                       params.get("login"));
                                } else {
-                                       String token = cookies.get("token");
-                                       login = login(who, token, Instance.getInstance().getConfig()
-                                                       .getList(Config.SERVER_ALLOWED_SUBKEYS));
+                                       String cookie = cookies.get("cookie");
+                                       login = login(who, cookie);
                                }
 
                                if (login.isSuccess()) {
-                                       if (!login.isWl()) {
-                                               whitelist.clear();
-                                       }
-
-                                       // refresh token
-                                       session.getCookies().set(new Cookie("token",
-                                                       login.getToken(), "30; path=/"));
+                                       // refresh cookie
+                                       session.getCookies().set(new Cookie("cookie",
+                                                       login.getCookie(), "30; path=/"));
 
                                        // set options
                                        String optionName = params.get("optionName");
@@ -257,27 +194,39 @@ public class WebLibraryServer implements Runnable {
                                }
 
                                Response rep = null;
-                               if (!login.isSuccess() && (uri.equals("/") //
-                                               || uri.startsWith(STORY_URL_BASE) //
-                                               || uri.startsWith(VIEWER_URL_BASE) //
-                                               || uri.startsWith(LIST_URL))) {
+                               if (!login.isSuccess() && WebLibraryUrls.isSupportedUrl(uri)) {
                                        rep = loginPage(login, uri);
                                }
 
                                if (rep == null) {
                                        try {
-                                               if (uri.equals("/")) {
-                                                       rep = root(session, cookies, whitelist);
-                                               } else if (uri.startsWith(LIST_URL)) {
-                                                       rep = getList(uri, whitelist);
-                                               } else if (uri.startsWith(STORY_URL_BASE)) {
-                                                       rep = getStoryPart(uri, whitelist);
-                                               } else if (uri.startsWith(VIEWER_URL_BASE)) {
-                                                       rep = getViewer(cookies, uri, whitelist);
-                                               } else if (uri.equals("/logout")) {
-                                                       session.getCookies().delete("token");
-                                                       cookies.remove("token");
-                                                       rep = loginPage(login, uri);
+                                               if (WebLibraryUrls.isSupportedUrl(uri)) {
+                                                       if (WebLibraryUrls.INDEX_URL.equals(uri)) {
+                                                               rep = root(session, cookies, login);
+                                                       } else if (WebLibraryUrls.VERSION_URL.equals(uri)) {
+                                                               rep = newFixedLengthResponse(Status.OK,
+                                                                               MIME_PLAINTEXT,
+                                                                               Version.getCurrentVersion().toString());
+                                                       } else if (WebLibraryUrls.isListUrl(uri)) {
+                                                               rep = getList(uri, login);
+                                                       } else if (WebLibraryUrls.isStoryUrl(uri)) {
+                                                               rep = getStoryPart(uri, login);
+                                                       } else if (WebLibraryUrls.isViewUrl(uri)) {
+                                                               rep = getViewer(cookies, uri, login);
+                                                       } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
+                                                               session.getCookies().delete("cookie");
+                                                               cookies.remove("cookie");
+                                                               rep = loginPage(new WLoginResult(false, false),
+                                                                               uri);
+                                                       } else {
+                                                               getTraceHandler().error(
+                                                                               "Supported URL was not processed: "
+                                                                                               + uri);
+                                                               rep = newFixedLengthResponse(
+                                                                               Status.INTERNAL_ERROR,
+                                                                               NanoHTTPD.MIME_PLAINTEXT,
+                                                                               "An error happened");
+                                                       }
                                                } else {
                                                        if (uri.startsWith("/"))
                                                                uri = uri.substring(1);
@@ -294,14 +243,13 @@ public class WebLibraryServer implements Runnable {
                                                                }
                                                                rep = newChunkedResponse(Status.OK, mimeType,
                                                                                in);
-                                                       } else {
-                                                               getTraceHandler().trace("404: " + uri);
                                                        }
-                                               }
 
-                                               if (rep == null) {
-                                                       rep = newFixedLengthResponse(Status.NOT_FOUND,
-                                                                       NanoHTTPD.MIME_PLAINTEXT, "Not Found");
+                                                       if (rep == null) {
+                                                               getTraceHandler().trace("404: " + uri);
+                                                               rep = newFixedLengthResponse(Status.NOT_FOUND,
+                                                                               NanoHTTPD.MIME_PLAINTEXT, "Not Found");
+                                                       }
                                                }
                                        } catch (Exception e) {
                                                Instance.getInstance().getTraceHandler().error(
@@ -313,24 +261,6 @@ public class WebLibraryServer implements Runnable {
                                }
 
                                return rep;
-
-                               // Get status: for story, use "luid" + active map of current
-                               // luids
-                               // map must use a addRef/removeRef and delete at 0
-
-                               // http://localhost:2000/?token=ok
-
-                               //
-                               // MetaData meta = new MetaData();
-                               // meta.setTitle("Title");
-                               // meta.setLuid("000");
-                               //
-                               // JSONObject json = new JSONObject();
-                               // json.put("", MetaData.class.getName());
-                               // json.put("title", meta.getTitle());
-                               // json.put("luid", meta.getLuid());
-                               //
-                               // return newFixedLengthResponse(json.toString());
                        }
                };
 
@@ -385,69 +315,71 @@ public class WebLibraryServer implements Runnable {
                this.tracer = tracer;
        }
 
-       private LoginResult login(String who, String token, List<String> subkeys) {
+       private WLoginResult login(String who, String cookie) {
+               List<String> subkeys = Instance.getInstance().getConfig()
+                               .getList(Config.SERVER_ALLOWED_SUBKEYS);
                String realKey = Instance.getInstance().getConfig()
                                .getString(Config.SERVER_KEY);
-               realKey = realKey == null ? "" : realKey;
-               return new LoginResult(token, who, realKey, subkeys);
+
+               return new WLoginResult(cookie, who, realKey, subkeys);
        }
 
        // allow rw/wl
-       private LoginResult login(String who, String key, String subkey,
-                       List<String> whitelist) {
+       private WLoginResult login(String who, String key, String subkey) {
                String realKey = Instance.getInstance().getConfig()
-                               .getString(Config.SERVER_KEY);
+                               .getString(Config.SERVER_KEY, "");
 
                // I don't like NULLs...
-               realKey = realKey == null ? "" : realKey;
                key = key == null ? "" : key;
                subkey = subkey == null ? "" : subkey;
 
                if (!realKey.equals(key)) {
-                       return new LoginResult(null, null, null, false, false, false);
+                       return new WLoginResult(true, false);
                }
 
-               // defaults are positive (as previous versions without the feature)
+               // defaults are true (as previous versions without the feature)
                boolean rw = true;
                boolean wl = true;
-
-               if (whitelist.isEmpty()) {
-                       wl = false;
-               }
+               boolean bl = true;
 
                rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
                                rw);
-               if (!subkey.isEmpty()) {
-                       List<String> allowed = Instance.getInstance().getConfig()
-                                       .getList(Config.SERVER_ALLOWED_SUBKEYS);
-                       if (allowed != null && allowed.contains(subkey)) {
-                               if ((subkey + "|").contains("|rw|")) {
-                                       rw = true;
-                               }
-                               if ((subkey + "|").contains("|wl|")) {
-                                       wl = false; // |wl| = bypass whitelist
-                               }
-                       } else {
-                               return new LoginResult(null, null, null, false, false, false);
+
+               List<String> allowed = Instance.getInstance().getConfig().getList(
+                               Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
+
+               if (!allowed.isEmpty()) {
+                       if (!allowed.contains(subkey)) {
+                               return new WLoginResult(true, false);
+                       }
+
+                       if ((subkey + "|").contains("|rw|")) {
+                               rw = true;
+                       }
+                       if ((subkey + "|").contains("|wl|")) {
+                               wl = false; // |wl| = bypass whitelist
+                       }
+                       if ((subkey + "|").contains("|bl|")) {
+                               bl = false; // |bl| = bypass blacklist
                        }
                }
 
-               return new LoginResult(who, key, subkey, true, rw, wl);
+               return new WLoginResult(who, key, subkey, rw, wl, bl);
        }
 
-       private Response loginPage(LoginResult login, String uri) {
+       private Response loginPage(WLoginResult login, String uri) {
                StringBuilder builder = new StringBuilder();
 
                appendPreHtml(builder, true);
 
                if (login.isBadLogin()) {
                        builder.append("<div class='error'>Bad login or password</div>");
-               } else if (login.isBadToken()) {
+               } else if (login.isBadCookie()) {
                        builder.append("<div class='error'>Your session timed out</div>");
                }
 
-               if (uri.equals("/logout")) {
-                       uri = "/";
+               if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
+                       uri = WebLibraryUrls.INDEX_URL;
                }
 
                builder.append(
@@ -465,13 +397,11 @@ public class WebLibraryServer implements Runnable {
                                NanoHTTPD.MIME_HTML, builder.toString());
        }
 
-       protected Response getList(String uri, List<String> whitelist)
+       protected Response getList(String uri, WLoginResult login)
                        throws IOException {
-               if (uri.equals("/list/luids")) {
-                       BasicLibrary lib = Instance.getInstance().getLibrary();
-                       List<MetaData> metas = lib.getList().filter(whitelist, null, null);
+               if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) {
                        List<JSONObject> jsons = new ArrayList<JSONObject>();
-                       for (MetaData meta : metas) {
+                       for (MetaData meta : metas(login)) {
                                jsons.add(JsonIO.toJson(meta));
                        }
 
@@ -485,10 +415,9 @@ public class WebLibraryServer implements Runnable {
        }
 
        private Response root(IHTTPSession session, Map<String, String> cookies,
-                       List<String> whitelist) throws IOException {
+                       WLoginResult login) throws IOException {
                BasicLibrary lib = Instance.getInstance().getLibrary();
-               MetaResultList result = lib.getList();
-               result = new MetaResultList(result.filter(whitelist, null, null));
+               MetaResultList result = new MetaResultList(metas(login));
                StringBuilder builder = new StringBuilder();
 
                appendPreHtml(builder, true);
@@ -626,7 +555,8 @@ public class WebLibraryServer implements Runnable {
 
                        builder.append("<div class='book_line'>");
                        builder.append("<a href='");
-                       builder.append(getViewUrl(meta.getLuid(), 0, null));
+                       builder.append(
+                                       WebLibraryUrls.getViewUrl(meta.getLuid(), null, null));
                        builder.append("'");
                        builder.append(" class='link'>");
 
@@ -662,7 +592,7 @@ public class WebLibraryServer implements Runnable {
        // /story/luid/cover <-- image
        // /story/luid/metadata <-- json
        // /story/luid/json <-- json, whole chapter (no images)
-       private Response getStoryPart(String uri, List<String> whitelist) {
+       private Response getStoryPart(String uri, WLoginResult login) {
                String[] cover = uri.split("/");
                int off = 2;
 
@@ -709,22 +639,24 @@ public class WebLibraryServer implements Runnable {
                InputStream in = null;
                try {
                        if ("cover".equals(chapterStr)) {
-                               Image img = getCover(luid, whitelist);
+                               Image img = getCover(luid, login);
                                if (img != null) {
                                        in = img.newInputStream();
                                }
+                               // TODO: get correct image type
+                               mimeType = "image/png";
                        } else if ("metadata".equals(chapterStr)) {
-                               MetaData meta = meta(luid, whitelist);
+                               MetaData meta = meta(luid, login);
                                JSONObject json = JsonIO.toJson(meta);
                                mimeType = "application/json";
                                in = new ByteArrayInputStream(json.toString().getBytes());
                        } else if ("json".equals(chapterStr)) {
-                               Story story = story(luid, whitelist);
+                               Story story = story(luid, login);
                                JSONObject json = JsonIO.toJson(story);
                                mimeType = "application/json";
                                in = new ByteArrayInputStream(json.toString().getBytes());
                        } else {
-                               Story story = story(luid, whitelist);
+                               Story story = story(luid, login);
                                if (story != null) {
                                        if (chapter == 0) {
                                                StringBuilder builder = new StringBuilder();
@@ -767,7 +699,7 @@ public class WebLibraryServer implements Runnable {
        }
 
        private Response getViewer(Map<String, String> cookies, String uri,
-                       List<String> whitelist) {
+                       WLoginResult login) {
                String[] cover = uri.split("/");
                int off = 2;
 
@@ -782,7 +714,7 @@ public class WebLibraryServer implements Runnable {
                String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
 
                // 1-based (0 = desc)
-               int chapter = -1;
+               int chapter = 0;
                if (chapterStr != null) {
                        try {
                                chapter = Integer.parseInt(chapterStr);
@@ -810,7 +742,7 @@ public class WebLibraryServer implements Runnable {
                }
 
                try {
-                       Story story = story(luid, whitelist);
+                       Story story = story(luid, login);
                        if (story == null) {
                                return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
                                                NanoHTTPD.MIME_PLAINTEXT, "Story not found");
@@ -848,20 +780,25 @@ public class WebLibraryServer implements Runnable {
                        String disabledZoomHeight = "";
 
                        if (paragraph <= 0) {
-                               first = getViewUrl(luid, 0, null);
-                               previous = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
-                               next = getViewUrl(luid,
+                               first = WebLibraryUrls.getViewUrl(luid, 0, null);
+                               previous = WebLibraryUrls.getViewUrl(luid,
+                                               (Math.max(chapter - 1, 0)), null);
+                               next = WebLibraryUrls.getViewUrl(luid,
                                                (Math.min(chapter + 1, story.getChapters().size())),
                                                null);
-                               last = getViewUrl(luid, story.getChapters().size(), null);
+                               last = WebLibraryUrls.getViewUrl(luid,
+                                               story.getChapters().size(), null);
 
                                StringBuilder desc = new StringBuilder();
 
                                if (chapter <= 0) {
+                                       desc.append("<h1 class='title'>");
+                                       desc.append(story.getMeta().getTitle());
+                                       desc.append("</h1>\n");
                                        desc.append("<div class='desc'>\n");
-                                       desc.append("\t<div class='cover'>\n");
+                                       desc.append("\t<a href='" + next + "' class='cover'>\n");
                                        desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
-                                       desc.append("\t</div>\n");
+                                       desc.append("\t</a>\n");
                                        desc.append("\t<table class='details'>\n");
                                        Map<String, String> details = BasicLibrary
                                                        .getMetaDesc(story.getMeta());
@@ -877,9 +814,9 @@ public class WebLibraryServer implements Runnable {
                                content.append(desc);
                                String description = new TextOutput(false).convert(chap,
                                                chapter > 0);
-                               content.append(
-                                               description.isEmpty() ? "No description provided."
-                                                               : description);
+                               content.append(chap.getParagraphs().size() <= 0
+                                               ? "No content provided."
+                                               : description);
                                content.append("</div>\n");
 
                                if (chapter <= 0)
@@ -887,12 +824,13 @@ public class WebLibraryServer implements Runnable {
                                if (chapter >= story.getChapters().size())
                                        disabledRight = " disabled='disbaled'";
                        } else {
-                               first = getViewUrl(luid, chapter, 1);
-                               previous = getViewUrl(luid, chapter,
+                               first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
+                               previous = WebLibraryUrls.getViewUrl(luid, chapter,
                                                (Math.max(paragraph - 1, 1)));
-                               next = getViewUrl(luid, chapter,
+                               next = WebLibraryUrls.getViewUrl(luid, chapter,
                                                (Math.min(paragraph + 1, chap.getParagraphs().size())));
-                               last = getViewUrl(luid, chapter, chap.getParagraphs().size());
+                               last = WebLibraryUrls.getViewUrl(luid, chapter,
+                                               chap.getParagraphs().size());
 
                                if (paragraph <= 1)
                                        disabledLeft = " disabled='disbaled'";
@@ -902,7 +840,8 @@ public class WebLibraryServer implements Runnable {
                                // First -> previous *chapter*
                                if (chapter > 0)
                                        disabledLeft = "";
-                               first = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
+                               first = WebLibraryUrls.getViewUrl(luid,
+                                               (Math.max(chapter - 1, 0)), null);
                                if (paragraph <= 1) {
                                        previous = first;
                                }
@@ -935,13 +874,16 @@ public class WebLibraryServer implements Runnable {
                                                }
                                        }
 
+                                       String javascript = "document.getElementById(\"previous\").click(); return false;";
                                        content.append(String.format("" //
-                                                       + "<a class='viewer link' href='%s'>"
+                                                       + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
                                                        + "<img class='viewer img' style='%s' src='%s'/>"
                                                        + "</a>", //
+                                                       javascript, //
                                                        next, //
                                                        zoomStyle, //
-                                                       getStoryUrl(luid, chapter, paragraph)));
+                                                       WebLibraryUrls.getStoryUrl(luid, chapter,
+                                                                       paragraph)));
                                } else {
                                        content.append(String.format("" //
                                                        + "<div class='viewer text'>%s</div>", //
@@ -952,7 +894,7 @@ public class WebLibraryServer implements Runnable {
                        builder.append(String.format("" //
                                        + "<div class='bar navbar'>\n" //
                                        + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
-                                       + "\t<a%s class='button previous' href='%s'>&lt;</a>\n" //
+                                       + "\t<a%s id='previous' class='button previous' href='%s'>&lt;</a>\n" //
                                        + "\t<div class='gotobox itemsbox'>\n" //
                                        + "\t\t<div class='button goto'>%d</div>\n" //
                                        + "\t\t<div class='items goto'>\n", //
@@ -963,15 +905,13 @@ public class WebLibraryServer implements Runnable {
 
                        // List of chap/para links
 
-                       String blink = "/view/story/" + luid + "/";
-                       appendItemA(builder, 3, blink + "0", "Description",
-                                       paragraph == 0 && chapter == 0);
-
+                       appendItemA(builder, 3, WebLibraryUrls.getViewUrl(luid, 0, null),
+                                       "Description", paragraph == 0 && chapter == 0);
                        if (paragraph > 0) {
-                               blink = blink + chapter + "/";
                                for (int i = 1; i <= chap.getParagraphs().size(); i++) {
-                                       appendItemA(builder, 3, blink + i, "Image " + i,
-                                                       paragraph == i);
+                                       appendItemA(builder, 3,
+                                                       WebLibraryUrls.getViewUrl(luid, chapter, i),
+                                                       "Image " + i, paragraph == i);
                                }
                        } else {
                                int i = 1;
@@ -981,7 +921,9 @@ public class WebLibraryServer implements Runnable {
                                                chapName += ": " + c.getName();
                                        }
 
-                                       appendItemA(builder, 3, blink + i, chapName, chapter == i);
+                                       appendItemA(builder, 3,
+                                                       WebLibraryUrls.getViewUrl(luid, i, null), chapName,
+                                                       chapter == i);
 
                                        i++;
                                }
@@ -1057,40 +999,41 @@ public class WebLibraryServer implements Runnable {
                return "";
        }
 
-       private String getViewUrl(String luid, int chap, Integer para) {
-               return VIEWER_URL //
-                               .replace("{luid}", luid) //
-                               .replace("{chap}", Integer.toString(chap)) //
-                               .replace("/{para}",
-                                               para == null ? "" : "/" + Integer.toString(para));
-       }
+       private boolean isAllowed(MetaData meta, WLoginResult login) {
+               if (login.isWl() && !whitelist.isEmpty()
+                               && !whitelist.contains(meta.getSource())) {
+                       return false;
+               }
+               if (login.isBl() && blacklist.contains(meta.getSource())) {
+                       return false;
+               }
 
-       private String getStoryUrl(String luid, int chap, Integer para) {
-               return STORY_URL //
-                               .replace("{luid}", luid) //
-                               .replace("{chap}", Integer.toString(chap)) //
-                               .replace("{para}", para == null ? "" : Integer.toString(para));
+               return true;
        }
 
-       private String getStoryUrlCover(String luid) {
-               return STORY_URL_COVER //
-                               .replace("{luid}", luid);
+       private List<MetaData> metas(WLoginResult login) throws IOException {
+               BasicLibrary lib = Instance.getInstance().getLibrary();
+               List<MetaData> metas = new ArrayList<MetaData>();
+               for (MetaData meta : lib.getList().getMetas()) {
+                       if (isAllowed(meta, login)) {
+                               metas.add(meta);
+                       }
+               }
+
+               return metas;
        }
 
-       private MetaData meta(String luid, List<String> whitelist)
-                       throws IOException {
+       private MetaData meta(String luid, WLoginResult login) throws IOException {
                BasicLibrary lib = Instance.getInstance().getLibrary();
                MetaData meta = lib.getInfo(luid);
-               if (!whitelist.isEmpty() && !whitelist.contains(meta.getSource())) {
+               if (!isAllowed(meta, login))
                        return null;
-               }
 
                return meta;
        }
 
-       private Image getCover(String luid, List<String> whitelist)
-                       throws IOException {
-               MetaData meta = meta(luid, whitelist);
+       private Image getCover(String luid, WLoginResult login) throws IOException {
+               MetaData meta = meta(luid, login);
                if (meta != null) {
                        BasicLibrary lib = Instance.getInstance().getLibrary();
                        return lib.getCover(meta.getLuid());
@@ -1100,22 +1043,19 @@ public class WebLibraryServer implements Runnable {
        }
 
        // NULL if not whitelist OK or if not found
-       private Story story(String luid, List<String> whitelist)
-                       throws IOException {
+       private Story story(String luid, WLoginResult login) throws IOException {
                synchronized (storyCache) {
                        if (storyCache.containsKey(luid)) {
                                Story story = storyCache.get(luid);
-                               if (!whitelist.isEmpty()
-                                               && !whitelist.contains(story.getMeta().getSource())) {
+                               if (!isAllowed(story.getMeta(), login))
                                        return null;
-                               }
 
                                return story;
                        }
                }
 
                Story story = null;
-               MetaData meta = meta(luid, whitelist);
+               MetaData meta = meta(luid, login);
                if (meta != null) {
                        BasicLibrary lib = Instance.getInstance().getLibrary();
                        story = lib.getStory(luid, null);