add blacklist support
[nikiroo-utils.git] / src / be / nikiroo / fanfix / library / WebLibraryServer.java
index a59fa3456d3d294b7614db718eaacadadb2d4415..c4be6633dc563949a5f146a86225017d43b987c4 100644 (file)
@@ -51,16 +51,18 @@ public class WebLibraryServer implements Runnable {
                private boolean success;
                private boolean rw;
                private boolean wl;
+               private boolean bl;
                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) {
+                               boolean success, boolean rw, boolean wl, boolean bl) {
                        this.success = success;
                        this.rw = rw;
                        this.wl = wl;
+                       this.bl = bl;
                        this.wookie = CookieUtils.generateCookie(who + key, 0);
 
                        String opts = "";
@@ -68,6 +70,8 @@ public class WebLibraryServer implements Runnable {
                                opts += "|rw";
                        if (!wl)
                                opts += "|wl";
+                       if (!bl)
+                               opts += "|bl";
 
                        this.token = wookie + "~"
                                        + CookieUtils.generateCookie(wookie + subkey + opts, 0)
@@ -101,6 +105,7 @@ public class WebLibraryServer implements Runnable {
 
                                                                this.rw = opts.contains("|rw");
                                                                this.wl = !opts.contains("|wl");
+                                                               this.bl = !opts.contains("|bl");
                                                        }
                                                }
                                        }
@@ -124,6 +129,10 @@ public class WebLibraryServer implements Runnable {
                        return wl;
                }
 
+               public boolean isBl() {
+                       return bl;
+               }
+
                public String getToken() {
                        return token;
                }
@@ -144,6 +153,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 +170,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,30 +228,19 @@ 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;
                                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));
+                                       login = login(who, token);
                                }
 
                                if (login.isSuccess()) {
-                                       if (!login.isWl()) {
-                                               whitelist.clear();
-                                       }
-
                                        // refresh token
                                        session.getCookies().set(new Cookie("token",
                                                        login.getToken(), "30; path=/"));
@@ -242,8 +248,10 @@ public class WebLibraryServer implements Runnable {
                                        // set options
                                        String optionName = params.get("optionName");
                                        if (optionName != null && !optionName.isEmpty()) {
+                                               String optionNo = params.get("optionNo");
                                                String optionValue = params.get("optionValue");
-                                               if (optionValue == null || optionValue.isEmpty()) {
+                                               if (optionNo != null || optionValue == null
+                                                               || optionValue.isEmpty()) {
                                                        session.getCookies().delete(optionName);
                                                        cookies.remove(optionName);
                                                } else {
@@ -265,13 +273,13 @@ public class WebLibraryServer implements Runnable {
                                if (rep == null) {
                                        try {
                                                if (uri.equals("/")) {
-                                                       rep = root(session, cookies, whitelist);
+                                                       rep = root(session, cookies, login);
                                                } else if (uri.startsWith(LIST_URL)) {
-                                                       rep = getList(uri, whitelist);
+                                                       rep = getList(uri, login);
                                                } else if (uri.startsWith(STORY_URL_BASE)) {
-                                                       rep = getStoryPart(uri, whitelist);
+                                                       rep = getStoryPart(uri, login);
                                                } else if (uri.startsWith(VIEWER_URL_BASE)) {
-                                                       rep = getViewer(cookies, uri, whitelist);
+                                                       rep = getViewer(cookies, uri, login);
                                                } else if (uri.equals("/logout")) {
                                                        session.getCookies().delete("token");
                                                        cookies.remove("token");
@@ -311,24 +319,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());
                        }
                };
 
@@ -383,35 +373,33 @@ public class WebLibraryServer implements Runnable {
                this.tracer = tracer;
        }
 
-       private LoginResult login(String who, String token, List<String> subkeys) {
+       private LoginResult login(String who, String token) {
+               List<String> subkeys = Instance.getInstance().getConfig().getList(
+                               Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
                String realKey = Instance.getInstance().getConfig()
-                               .getString(Config.SERVER_KEY);
-               realKey = realKey == null ? "" : realKey;
+                               .getString(Config.SERVER_KEY, "");
+
                return new LoginResult(token, who, realKey, subkeys);
        }
 
        // allow rw/wl
-       private LoginResult login(String who, String key, String subkey,
-                       List<String> whitelist) {
+       private LoginResult 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 LoginResult(null, null, null, false, false, false,
+                                       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);
@@ -425,12 +413,16 @@ public class WebLibraryServer implements Runnable {
                                if ((subkey + "|").contains("|wl|")) {
                                        wl = false; // |wl| = bypass whitelist
                                }
+                               if ((subkey + "|").contains("|bl|")) {
+                                       bl = false; // |bl| = bypass blacklist
+                               }
                        } else {
-                               return new LoginResult(null, null, null, false, false, false);
+                               return new LoginResult(null, null, null, false, false, false,
+                                               false);
                        }
                }
 
-               return new LoginResult(who, key, subkey, true, rw, wl);
+               return new LoginResult(who, key, subkey, true, rw, wl, bl);
        }
 
        private Response loginPage(LoginResult login, String uri) {
@@ -463,13 +455,11 @@ public class WebLibraryServer implements Runnable {
                                NanoHTTPD.MIME_HTML, builder.toString());
        }
 
-       protected Response getList(String uri, List<String> whitelist)
+       protected Response getList(String uri, LoginResult login)
                        throws IOException {
                if (uri.equals("/list/luids")) {
-                       BasicLibrary lib = Instance.getInstance().getLibrary();
-                       List<MetaData> metas = lib.getList().filter(whitelist, null, null);
                        List<JSONObject> jsons = new ArrayList<JSONObject>();
-                       for (MetaData meta : metas) {
+                       for (MetaData meta : metas(login)) {
                                jsons.add(JsonIO.toJson(meta));
                        }
 
@@ -483,20 +473,22 @@ public class WebLibraryServer implements Runnable {
        }
 
        private Response root(IHTTPSession session, Map<String, String> cookies,
-                       List<String> whitelist) throws IOException {
+                       LoginResult 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);
 
+               Map<String, String> params = session.getParms();
+
                String filter = cookies.get("filter");
+               if (params.get("optionNo") != null)
+                       filter = null;
                if (filter == null) {
                        filter = "";
                }
 
-               Map<String, String> params = session.getParms();
                String browser = params.get("browser") == null ? ""
                                : params.get("browser");
                String browser2 = params.get("browser2") == null ? ""
@@ -585,11 +577,12 @@ public class WebLibraryServer implements Runnable {
 
                // TODO: javascript in realtime, using visible=false + hide [submit]
                builder.append("<div class='filter'>\n");
-               builder.append("\tFilter: \n");
+               builder.append("\t<span class='label'>Filter: </span>\n");
                builder.append(
                                "\t<input name='optionName'  type='hidden' value='filter' />\n");
                builder.append("\t<input name='optionValue' type='text'   value='"
                                + filter + "' place-holder='...' />\n");
+               builder.append("\t<input name='optionNo'  type='submit' value='x' />");
                builder.append(
                                "\t<input name='submit' type='submit' value='Filter' />\n");
                builder.append("</div>\n");
@@ -620,7 +613,7 @@ public class WebLibraryServer implements Runnable {
 
                        builder.append("<div class='book_line'>");
                        builder.append("<a href='");
-                       builder.append(getViewUrl(meta.getLuid(), 0, null));
+                       builder.append(getViewUrl(meta.getLuid(), null, null));
                        builder.append("'");
                        builder.append(" class='link'>");
 
@@ -655,7 +648,8 @@ public class WebLibraryServer implements Runnable {
        // /story/luid/chapter/para <-- text/image
        // /story/luid/cover <-- image
        // /story/luid/metadata <-- json
-       private Response getStoryPart(String uri, List<String> whitelist) {
+       // /story/luid/json <-- json, whole chapter (no images)
+       private Response getStoryPart(String uri, LoginResult login) {
                String[] cover = uri.split("/");
                int off = 2;
 
@@ -671,7 +665,8 @@ public class WebLibraryServer implements Runnable {
                // 1-based (0 = desc)
                int chapter = 0;
                if (chapterStr != null && !"cover".equals(chapterStr)
-                               && !"metadata".equals(chapterStr)) {
+                               && !"metadata".equals(chapterStr)
+                               && !"json".equals(chapterStr)) {
                        try {
                                chapter = Integer.parseInt(chapterStr);
                                if (chapter < 0) {
@@ -701,17 +696,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, 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();
@@ -754,7 +756,7 @@ public class WebLibraryServer implements Runnable {
        }
 
        private Response getViewer(Map<String, String> cookies, String uri,
-                       List<String> whitelist) {
+                       LoginResult login) {
                String[] cover = uri.split("/");
                int off = 2;
 
@@ -769,7 +771,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);
@@ -797,7 +799,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");
@@ -806,123 +808,213 @@ public class WebLibraryServer implements Runnable {
                        StringBuilder builder = new StringBuilder();
                        appendPreHtml(builder, false);
 
-                       if (chapter < 0) {
-                               builder.append(story);
-                       } else {
-                               if (chapter == 0) {
-                                       // TODO: description
-                                       chapter = 1;
-                               }
+                       // For images documents, always go to the images if not chap 0 desc
+                       if (story.getMeta().isImageDocument()) {
+                               if (chapter > 0 && paragraph <= 0)
+                                       paragraph = 1;
+                       }
 
-                               Chapter chap = null;
+                       Chapter chap = null;
+                       if (chapter <= 0) {
+                               chap = story.getMeta().getResume();
+                       } else {
                                try {
                                        chap = story.getChapters().get(chapter - 1);
                                } catch (IndexOutOfBoundsException e) {
                                        return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
                                                        NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
                                }
+                       }
 
-                               if (story.getMeta().isImageDocument() && paragraph <= 0) {
-                                       paragraph = 1;
+                       String first, previous, next, last;
+
+                       StringBuilder content = new StringBuilder();
+
+                       String disabledLeft = "";
+                       String disabledRight = "";
+                       String disabledZoomReal = "";
+                       String disabledZoomWidth = "";
+                       String disabledZoomHeight = "";
+
+                       if (paragraph <= 0) {
+                               first = getViewUrl(luid, 0, null);
+                               previous = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
+                               next = getViewUrl(luid,
+                                               (Math.min(chapter + 1, story.getChapters().size())),
+                                               null);
+                               last = 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<a href='" + next + "' class='cover'>\n");
+                                       desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
+                                       desc.append("\t</a>\n");
+                                       desc.append("\t<table class='details'>\n");
+                                       Map<String, String> details = BasicLibrary
+                                                       .getMetaDesc(story.getMeta());
+                                       for (String key : details.keySet()) {
+                                               appendTableRow(desc, 2, key, details.get(key));
+                                       }
+                                       desc.append("\t</table>\n");
+                                       desc.append("</div>\n");
+                                       desc.append("<h1 class='title'>Description</h1>\n");
                                }
 
-                               String first, previous, next, last;
-                               String content;
+                               content.append("<div class='viewer text'>\n");
+                               content.append(desc);
+                               String description = new TextOutput(false).convert(chap,
+                                               chapter > 0);
+                               content.append(chap.getParagraphs().size() <= 0
+                                               ? "No content provided."
+                                               : description);
+                               content.append("</div>\n");
+
+                               if (chapter <= 0)
+                                       disabledLeft = " disabled='disbaled'";
+                               if (chapter >= story.getChapters().size())
+                                       disabledRight = " disabled='disbaled'";
+                       } else {
+                               first = getViewUrl(luid, chapter, 1);
+                               previous = getViewUrl(luid, chapter,
+                                               (Math.max(paragraph - 1, 1)));
+                               next = getViewUrl(luid, chapter,
+                                               (Math.min(paragraph + 1, chap.getParagraphs().size())));
+                               last = getViewUrl(luid, chapter, chap.getParagraphs().size());
+
+                               if (paragraph <= 1)
+                                       disabledLeft = " disabled='disbaled'";
+                               if (paragraph >= chap.getParagraphs().size())
+                                       disabledRight = " disabled='disbaled'";
+
+                               // First -> previous *chapter*
+                               if (chapter > 0)
+                                       disabledLeft = "";
+                               first = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
+                               if (paragraph <= 1) {
+                                       previous = first;
+                               }
 
-                               if (paragraph <= 0) {
-                                       first = getViewUrl(luid, 1, null);
-                                       previous = getViewUrl(luid, (Math.max(chapter - 1, 1)),
-                                                       null);
-                                       next = getViewUrl(luid,
-                                                       (Math.min(chapter + 1, story.getChapters().size())),
-                                                       null);
-                                       last = getViewUrl(luid, story.getChapters().size(), null);
-
-                                       content = "<div class='viewer text'>\n"
-                                                       + new TextOutput(false).convert(chap, true)
-                                                       + "</div>\n";
-                               } else {
-                                       first = getViewUrl(luid, chapter, 1);
-                                       previous = getViewUrl(luid, chapter,
-                                                       (Math.max(paragraph - 1, 1)));
-                                       next = getViewUrl(luid, chapter, (Math.min(paragraph + 1,
-                                                       chap.getParagraphs().size())));
-                                       last = getViewUrl(luid, chapter,
-                                                       chap.getParagraphs().size());
-
-                                       Paragraph para = null;
-                                       try {
-                                               para = chap.getParagraphs().get(paragraph - 1);
-                                       } catch (IndexOutOfBoundsException e) {
-                                               return NanoHTTPD.newFixedLengthResponse(
-                                                               Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
-                                                               "Paragraph not found");
-                                       }
+                               Paragraph para = null;
+                               try {
+                                       para = chap.getParagraphs().get(paragraph - 1);
+                               } catch (IndexOutOfBoundsException e) {
+                                       return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
+                                                       NanoHTTPD.MIME_PLAINTEXT,
+                                                       "Paragraph " + paragraph + " not found");
+                               }
 
-                                       if (para.getType() == ParagraphType.IMAGE) {
-                                               String zoomStyle = "max-width: 100%;";
-                                               String zoomOption = cookies.get("zoom");
-                                               if (zoomOption != null && !zoomOption.isEmpty()) {
-                                                       if (zoomOption.equals("real")) {
-                                                               zoomStyle = "";
-                                                       } else if (zoomOption.equals("width")) {
-                                                               zoomStyle = "max-width: 100%;";
-                                                       } else if (zoomOption.equals("height")) {
-                                                               // see height of navbar + optionbar
-                                                               zoomStyle = "max-height: calc(100% - 128px);";
-                                                       }
+                               if (para.getType() == ParagraphType.IMAGE) {
+                                       String zoomStyle = "max-width: 100%;";
+                                       disabledZoomWidth = " disabled='disabled'";
+                                       String zoomOption = cookies.get("zoom");
+                                       if (zoomOption != null && !zoomOption.isEmpty()) {
+                                               if (zoomOption.equals("real")) {
+                                                       zoomStyle = "";
+                                                       disabledZoomWidth = "";
+                                                       disabledZoomReal = " disabled='disabled'";
+                                               } else if (zoomOption.equals("width")) {
+                                                       zoomStyle = "max-width: 100%;";
+                                               } else if (zoomOption.equals("height")) {
+                                                       // see height of navbar + optionbar
+                                                       zoomStyle = "max-height: calc(100% - 128px);";
+                                                       disabledZoomWidth = "";
+                                                       disabledZoomHeight = " disabled='disabled'";
                                                }
-                                               content = String.format("" //
-                                                               + "<a class='viewer link' href='%s'>" //
-                                                               + "<img class='viewer img' style='%s' src='%s'/>"
-                                                               + "</a>", //
-                                                               next, //
-                                                               zoomStyle, //
-                                                               getStoryUrl(luid, chapter, paragraph));
-                                       } else {
-                                               content = para.getContent();
                                        }
 
+                                       String javascript = "document.getElementById(\"previous\").click(); return false;";
+                                       content.append(String.format("" //
+                                                       + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
+                                                       + "<img class='viewer img' style='%s' src='%s'/>"
+                                                       + "</a>", //
+                                                       javascript, //
+                                                       next, //
+                                                       zoomStyle, //
+                                                       getStoryUrl(luid, chapter, paragraph)));
+                               } else {
+                                       content.append(String.format("" //
+                                                       + "<div class='viewer text'>%s</div>", //
+                                                       para.getContent()));
                                }
+                       }
 
-                               builder.append(String.format("" //
-                                               + "<div class='bar navbar'>\n" //
-                                               + "\t<a class='button first' href='%s'>&lt;&lt;</a>\n"//
-                                               + "\t<a class='button previous' href='%s'>&lt;</a>\n"//
-                                               + "\t<a class='button next' href='%s'>&gt;</a>\n"//
-                                               + "\t<a class='button last' href='%s'>&gt;&gt;</a>\n"//
-                                               + "</div>\n" //
-                                               + "%s", //
-                                               first, //
-                                               previous, //
-                                               next, //
-                                               last, //
-                                               content //
-                               ));
-
-                               builder.append("<div class='bar optionbar ");
-                               if (paragraph > 0) {
-                                       builder.append("s4");
-                               } else {
-                                       builder.append("s1");
+                       builder.append(String.format("" //
+                                       + "<div class='bar navbar'>\n" //
+                                       + "\t<a%s class='button first' href='%s'>&lt;&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", //
+                                       disabledLeft, first, //
+                                       disabledLeft, previous, //
+                                       paragraph > 0 ? paragraph : chapter //
+                       ));
+
+                       // List of chap/para links
+
+                       appendItemA(builder, 3, getViewUrl(luid, 0, null), "Description",
+                                       paragraph == 0 && chapter == 0);
+                       if (paragraph > 0) {
+                               for (int i = 1; i <= chap.getParagraphs().size(); i++) {
+                                       appendItemA(builder, 3, getViewUrl(luid, chapter, i),
+                                                       "Image " + i, paragraph == i);
                                }
-                               builder.append("'>\n");
-                               builder.append(
-                                               "       <a class='button back' href='/'>BACK</a>\n");
-
-                               if (paragraph > 0) {
-                                       builder.append(String.format("" //
-                                                       + "\t<a class='button zoomreal'   href='%s'>REAL</a>\n"//
-                                                       + "\t<a class='button zoomwidth'  href='%s'>WIDTH</a>\n"//
-                                                       + "\t<a class='button zoomheight' href='%s'>HEIGHT</a>\n"//
-                                                       + "</div>\n", //
-                                                       uri + "?optionName=zoom&optionValue=real", //
-                                                       uri + "?optionName=zoom&optionValue=width", //
-                                                       uri + "?optionName=zoom&optionValue=height" //
-                                       ));
+                       } else {
+                               int i = 1;
+                               for (Chapter c : story.getChapters()) {
+                                       String chapName = "Chapter " + c.getNumber();
+                                       if (c.getName() != null && !c.getName().isEmpty()) {
+                                               chapName += ": " + c.getName();
+                                       }
+
+                                       appendItemA(builder, 3, getViewUrl(luid, i, null), chapName,
+                                                       chapter == i);
+
+                                       i++;
                                }
                        }
 
+                       builder.append(String.format("" //
+                                       + "\t\t</div>\n" //
+                                       + "\t</div>\n" //
+                                       + "\t<a%s class='button next' href='%s'>&gt;</a>\n" //
+                                       + "\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
+                                       + "</div>\n", //
+                                       disabledRight, next, //
+                                       disabledRight, last //
+                       ));
+
+                       builder.append(content);
+
+                       builder.append("<div class='bar optionbar ");
+                       if (paragraph > 0) {
+                               builder.append("s4");
+                       } else {
+                               builder.append("s1");
+                       }
+                       builder.append("'>\n");
+                       builder.append("        <a class='button back' href='/'>BACK</a>\n");
+
+                       if (paragraph > 0) {
+                               builder.append(String.format("" //
+                                               + "\t<a%s class='button zoomreal'   href='%s'>REAL</a>\n"//
+                                               + "\t<a%s class='button zoomwidth'  href='%s'>WIDTH</a>\n"//
+                                               + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
+                                               + "</div>\n", //
+                                               disabledZoomReal,
+                                               uri + "?optionName=zoom&optionValue=real", //
+                                               disabledZoomWidth,
+                                               uri + "?optionName=zoom&optionValue=width", //
+                                               disabledZoomHeight,
+                                               uri + "?optionName=zoom&optionValue=height" //
+                               ));
+                       }
+
                        appendPostHtml(builder);
                        return NanoHTTPD.newFixedLengthResponse(Status.OK,
                                        NanoHTTPD.MIME_HTML, builder.toString());
@@ -957,12 +1049,12 @@ public class WebLibraryServer implements Runnable {
                return "";
        }
 
-       private String getViewUrl(String luid, int chap, Integer para) {
+       private String getViewUrl(String luid, Integer chap, Integer para) {
                return VIEWER_URL //
                                .replace("{luid}", luid) //
-                               .replace("{chap}", Integer.toString(chap)) //
+                               .replace("/{chap}", chap == null ? "" : "/" + chap) //
                                .replace("/{para}",
-                                               para == null ? "" : "/" + Integer.toString(para));
+                                               (chap == null || para == null) ? "" : "/" + para);
        }
 
        private String getStoryUrl(String luid, int chap, Integer para) {
@@ -977,44 +1069,63 @@ public class WebLibraryServer implements Runnable {
                                .replace("{luid}", luid);
        }
 
-       private MetaData meta(String luid, List<String> whitelist)
-                       throws IOException {
+       private boolean isAllowed(MetaData meta, LoginResult login) {
+               if (login.isWl() && !whitelist.isEmpty()
+                               && !whitelist.contains(meta.getSource())) {
+                       return false;
+               }
+               if (login.isBl() && blacklist.contains(meta.getSource())) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       private List<MetaData> metas(LoginResult 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, LoginResult 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, LoginResult login) throws IOException {
+               MetaData meta = meta(luid, login);
                if (meta != null) {
-                       return meta.getCover();
+                       BasicLibrary lib = Instance.getInstance().getLibrary();
+                       return lib.getCover(meta.getLuid());
                }
 
                return null;
        }
 
        // NULL if not whitelist OK or if not found
-       private Story story(String luid, List<String> whitelist)
-                       throws IOException {
+       private Story story(String luid, LoginResult 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);
@@ -1067,7 +1178,7 @@ public class WebLibraryServer implements Runnable {
 
                if (banner) {
                        builder.append("<div class='banner'>\n");
-                       builder.append("\t<img class='ico' src='") //
+                       builder.append("\t<img class='ico' src='/") //
                                        .append(favicon) //
                                        .append("'/>\n");
                        builder.append("\t<h1>Fanfix</h1>\n");
@@ -1093,4 +1204,45 @@ public class WebLibraryServer implements Runnable {
                }
                builder.append(">").append(name).append("</option>\n");
        }
+
+       private void appendTableRow(StringBuilder builder, int depth,
+                       String... tds) {
+               for (int i = 0; i < depth; i++) {
+                       builder.append("\t");
+               }
+
+               int col = 1;
+               builder.append("<tr>");
+               for (String td : tds) {
+                       builder.append("<td class='col");
+                       builder.append(col++);
+                       builder.append("'>");
+                       builder.append(td);
+                       builder.append("</td>");
+               }
+               builder.append("</tr>\n");
+       }
+
+       private void appendItemA(StringBuilder builder, int depth, String link,
+                       String name, boolean selected) {
+               for (int i = 0; i < depth; i++) {
+                       builder.append("\t");
+               }
+
+               builder.append("<a href='");
+               builder.append(link);
+               builder.append("' class='item goto");
+               if (selected) {
+                       builder.append(" selected");
+               }
+               builder.append("'>");
+               builder.append(name);
+               builder.append("</a>\n");
+       }
+
+       public static void main(String[] args) throws IOException {
+               Instance.init();
+               WebLibraryServer web = new WebLibraryServer(false);
+               web.run();
+       }
 }