<|start_filename|>src/main/java/org/b3log/solo/processor/SearchProcessor.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Latkes;
import org.b3log.latke.http.Request;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
import org.b3log.latke.http.renderer.TextXmlRenderer;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.model.Pagination;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.util.Paginator;
import org.b3log.solo.model.Article;
import org.b3log.solo.model.Common;
import org.b3log.solo.model.Option;
import org.b3log.solo.service.ArticleQueryService;
import org.b3log.solo.service.DataModelService;
import org.b3log.solo.service.OptionQueryService;
import org.b3log.solo.service.UserQueryService;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import org.owasp.encoder.Encode;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Search processor.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @author <a href="http://vanessa.b3log.org"><NAME></a>
 * @version 2.0.0.0, Feb 9, 2020
 * @since 2.4.0
 */
@Singleton
public class SearchProcessor {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(SearchProcessor.class);

    /**
     * Article query service.
     */
    @Inject
    private ArticleQueryService articleQueryService;

    /**
     * User query service.
     */
    @Inject
    private UserQueryService userQueryService;

    /**
     * Option query service.
     */
    @Inject
    private OptionQueryService optionQueryService;

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * DataModelService.
     */
    @Inject
    private DataModelService dataModelService;

    /**
     * Shows opensearch.xml.
     *
     * @param context the specified context
     */
    public void showOpensearchXML(final RequestContext context) {
        final TextXmlRenderer renderer = new TextXmlRenderer();
        context.setRenderer(renderer);

        try {
            final InputStream resourceAsStream = SearchProcessor.class.getResourceAsStream("/opensearch.xml");
            String content = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8);
            final JSONObject preference = optionQueryService.getPreference();
            content = StringUtils.replace(content, "${blogTitle}", Jsoup.clean(preference.optString(Option.ID_C_BLOG_TITLE), Whitelist.none()));
            content = StringUtils.replace(content, "${blogSubtitle}", Jsoup.clean(preference.optString(Option.ID_C_BLOG_SUBTITLE), Whitelist.none()));
            content = StringUtils.replace(content, "${servePath}", Latkes.getServePath());

            renderer.setContent(content);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Shows opensearch.xml failed", e);
        }
    }

    /**
     * Searches articles.
     *
     * @param context the specified context
     */
    public void search(final RequestContext context) {
        final Request request = context.getRequest();
        final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "common-template/search.ftl");
        final Map<String, String> langs = langPropsService.getAll(Latkes.getLocale());
        final Map<String, Object> dataModel = renderer.getDataModel();
        dataModel.putAll(langs);

        final int pageNum = Paginator.getPage(request);
        String keyword = context.param(Common.KEYWORD);
        if (StringUtils.isBlank(keyword)) {
            keyword = "";
        }
        keyword = Encode.forHtml(keyword);

        dataModel.put(Common.KEYWORD, keyword);
        final JSONObject result = articleQueryService.searchKeyword(keyword, pageNum, 15);
        final List<JSONObject> articles = (List<JSONObject>) result.opt(Article.ARTICLES);

        try {
            final JSONObject preference = optionQueryService.getPreference();

            dataModelService.fillCommon(context, dataModel, preference);
            dataModelService.fillFaviconURL(dataModel, preference);
            dataModelService.fillUsite(dataModel);
            dataModelService.setArticlesExProperties(context, articles, preference);

            dataModel.put(Article.ARTICLES, articles);
            final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
            pagination.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
            dataModel.put(Pagination.PAGINATION, pagination);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Search articles failed");

            dataModel.put(Article.ARTICLES, Collections.emptyList());
        }
    }
}


<|start_filename|>src/main/java/org/b3log/solo/model/atom/Category.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.model.atom;


import org.apache.commons.lang3.StringEscapeUtils;

/**
 * Category.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.1.1.0, Jul 5, 2018
 * @since 0.3.1
 */
public final class Category {

    /**
     * Term variable.
     */
    private static final String TERM_VARIABLE = "${term}";

    /**
     * Category element.
     */
    private static final String CATEGORY_ELEMENT = "<category term=\"" + TERM_VARIABLE + "\" />";

    /**
     * Term.
     */
    private String term;

    /**
     * Gets the term.
     *
     * @return term
     */
    public String getTerm() {
        return term;
    }

    /**
     * Sets the term with the specified term.
     *
     * @param term the specified term
     */
    public void setTerm(final String term) {
        this.term = term;
    }

    @Override
    public String toString() {
        return CATEGORY_ELEMENT.replace(TERM_VARIABLE, StringEscapeUtils.escapeXml(term));
    }
}


<|start_filename|>src/main/java/org/b3log/solo/event/B3ArticleSender.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.event;

import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.event.AbstractEventListener;
import org.b3log.latke.event.Event;
import org.b3log.latke.ioc.BeanManager;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.model.User;
import org.b3log.solo.Server;
import org.b3log.solo.model.Article;
import org.b3log.solo.model.Common;
import org.b3log.solo.model.Option;
import org.b3log.solo.model.UserExt;
import org.b3log.solo.service.ArticleQueryService;
import org.b3log.solo.service.OptionQueryService;
import org.b3log.solo.util.Solos;
import org.json.JSONObject;

/**
 * This listener is responsible for sending article to B3log Rhythm. Sees <a href="https://ld246.com/article/1546941897596">B3log 构思 - 分布式社区网络</a> for more details.
 * <p>
 * API spec: https://ld246.com/article/1457158841475
 * </p>
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @author <a href="https://ld246.com/member/armstrong">ArmstrongCN</a>
 * @version 1.0.2.27, Sep 3, 2020
 * @since 0.3.1
 */
@Singleton
public class B3ArticleSender extends AbstractEventListener<JSONObject> {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(B3ArticleSender.class);

    @Override
    public void action(final Event<JSONObject> event) {
        final JSONObject data = event.getData();
        LOGGER.log(Level.DEBUG, "Processing an event [type={}, data={}] in listener [className={}]",
                event.getType(), data, B3ArticleSender.class.getName());

        pushArticleToRhy(data);
    }

    /**
     * Pushes the specified article data to B3log Rhythm.
     *
     * @param data the specified article data
     */
    public static void pushArticleToRhy(final JSONObject data) {
        try {
            final JSONObject originalArticle = data.getJSONObject(Article.ARTICLE);
            final String title = originalArticle.getString(Article.ARTICLE_TITLE);
            if (Article.ARTICLE_STATUS_C_PUBLISHED != originalArticle.optInt(Article.ARTICLE_STATUS)) {
                LOGGER.log(Level.INFO, "Ignored push a draft [title={}] to Rhy", title);
                return;
            }

            if (StringUtils.isNotBlank(originalArticle.optString(Article.ARTICLE_VIEW_PWD))) {
                LOGGER.log(Level.INFO, "Article [title={}] is a password article, ignored push to Rhy", title);
                return;
            }

            if (!originalArticle.optBoolean(Common.POST_TO_COMMUNITY)) {
                LOGGER.log(Level.INFO, "Article [title={}] push flag [postToCommunity] is [false], ignored push to Rhy", title);
                return;
            }

            final BeanManager beanManager = BeanManager.getInstance();
            final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
            final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class);
            final JSONObject preference = optionQueryService.getPreference();

            final JSONObject article = new JSONObject().
                    put("id", originalArticle.getString(Keys.OBJECT_ID)).
                    put("title", originalArticle.getString(Article.ARTICLE_TITLE)).
                    put("permalink", originalArticle.getString(Article.ARTICLE_PERMALINK)).
                    put("tags", originalArticle.getString(Article.ARTICLE_TAGS_REF)).
                    put("content", originalArticle.getString(Article.ARTICLE_CONTENT));
            final JSONObject author = articleQueryService.getAuthor(originalArticle);
            final JSONObject client = new JSONObject().
                    put("title", preference.getString(Option.ID_C_BLOG_TITLE)).
                    put("subTitle", preference.optString(Option.ID_C_BLOG_SUBTITLE)).
                    put("favicon", preference.optString(Option.ID_C_FAVICON_URL)).
                    put("host", Latkes.getServePath()).
                    put("name", "Solo").
                    put("ver", Server.VERSION).
                    put("userName", author.optString(User.USER_NAME)).
                    put("userB3Key", author.optString(UserExt.USER_B3_KEY));
            final JSONObject requestJSONObject = new JSONObject().
                    put("article", article).
                    put("client", client);
            final HttpResponse response = HttpRequest.post("https://rhythm.b3log.org/api/article").bodyText(requestJSONObject.toString()).
                    connectionTimeout(3000).timeout(7000).trustAllCerts(true).followRedirects(true).
                    contentTypeJson().header("User-Agent", Solos.USER_AGENT).send();
            response.charset("UTF-8");
            final JSONObject result = new JSONObject(response.bodyText());
            LOGGER.log(Level.INFO, "Pushed an article [title=" + title + "] to Rhy, result [" + result.optString(Keys.MSG) + "]");
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Pushes an article to Rhy failed: " + e.getMessage());
        }
    }

    /**
     * Gets the event type {@linkplain EventTypes#ADD_ARTICLE}.
     *
     * @return event type
     */
    @Override
    public String getEventType() {
        return EventTypes.ADD_ARTICLE;
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/SkinHandler.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import eu.bitwalker.useragentutils.BrowserType;
import eu.bitwalker.useragentutils.UserAgent;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.http.Request;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.http.function.Handler;
import org.b3log.latke.ioc.BeanManager;
import org.b3log.latke.util.Requests;
import org.b3log.solo.service.OptionQueryService;
import org.b3log.solo.service.StatisticMgmtService;
import org.b3log.solo.util.Skins;
import org.b3log.solo.util.Solos;
import org.json.JSONObject;

/**
 * Skin handler.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.1, Apr 30, 2020
 * @since 3.6.7
 */
public class SkinHandler implements Handler {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(SkinHandler.class);

    @Override
    public void handle(final RequestContext context) {
        final Request request = context.getRequest();
        Requests.log(request, Level.DEBUG, LOGGER);

        fillBotAttrs(request);
        if (!Solos.isBot(request)) {
            final StatisticMgmtService statisticMgmtService = BeanManager.getInstance().getReference(StatisticMgmtService.class);
            statisticMgmtService.onlineVisitorCount(request);
        }

        resolveSkinDir(request);

        context.handle();
    }

    /**
     * Resolve skin (template) for the specified HTTP request.
     * 前台皮肤切换 https://github.com/b3log/solo/issues/12060
     * 调整前台动态皮肤预览逻辑 https://github.com/88250/solo/issues/116
     *
     * @param request the specified HTTP request
     */
    private void resolveSkinDir(final Request request) {
        String skin = Skins.getQuerySkin(request);
        if (StringUtils.isBlank(skin)) {
            final BeanManager beanManager = BeanManager.getInstance();
            final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
            final JSONObject skinOpt = optionQueryService.getSkin();
            if (Solos.isMobile(request)) {
                if (null != skinOpt) {
                    skin = skinOpt.optString(org.b3log.solo.model.Option.ID_C_MOBILE_SKIN_DIR_NAME);
                } else {
                    skin = org.b3log.solo.model.Option.DefaultPreference.DEFAULT_MOBILE_SKIN_DIR_NAME;
                }
            } else {
                if (null != skinOpt) {
                    skin = skinOpt.optString(org.b3log.solo.model.Option.ID_C_SKIN_DIR_NAME);
                } else {
                    skin = org.b3log.solo.model.Option.DefaultPreference.DEFAULT_SKIN_DIR_NAME;
                }
            }
        }
        request.setAttribute(Keys.TEMPLATE_DIR_NAME, skin);
    }

    private static void fillBotAttrs(final Request request) {
        final String userAgentStr = request.getHeader("User-Agent");
        final UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
        BrowserType browserType = userAgent.getBrowser().getBrowserType();

        if (StringUtils.containsIgnoreCase(userAgentStr, "mobile")
                || StringUtils.containsIgnoreCase(userAgentStr, "MQQBrowser")
                || StringUtils.containsIgnoreCase(userAgentStr, "iphone")
                || StringUtils.containsIgnoreCase(userAgentStr, "MicroMessenger")
                || StringUtils.containsIgnoreCase(userAgentStr, "CFNetwork")
                || StringUtils.containsIgnoreCase(userAgentStr, "Android")) {
            browserType = BrowserType.MOBILE_BROWSER;
        } else if (StringUtils.containsIgnoreCase(userAgentStr, "Iframely")
                || StringUtils.containsIgnoreCase(userAgentStr, "Google")
                || StringUtils.containsIgnoreCase(userAgentStr, "BUbiNG")
                || StringUtils.containsIgnoreCase(userAgentStr, "ltx71")
                || StringUtils.containsIgnoreCase(userAgentStr, "py")) {
            browserType = BrowserType.ROBOT;
        }

        request.setAttribute(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT, BrowserType.ROBOT == browserType);
        request.setAttribute(Keys.HttpRequest.IS_MOBILE_BOT, BrowserType.MOBILE_BROWSER == browserType);
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/console/AdminConsole.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor.console;

import jodd.io.ZipUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.event.Event;
import org.b3log.latke.event.EventManager;
import org.b3log.latke.http.FileUpload;
import org.b3log.latke.http.Request;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.http.Response;
import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.model.Plugin;
import org.b3log.latke.model.User;
import org.b3log.latke.plugin.ViewLoadEventData;
import org.b3log.latke.service.LangPropsService;
import org.b3log.solo.Server;
import org.b3log.solo.model.Common;
import org.b3log.solo.model.Option;
import org.b3log.solo.model.UserExt;
import org.b3log.solo.service.*;
import org.b3log.solo.util.Markdowns;
import org.b3log.solo.util.Solos;
import org.b3log.solo.util.StatusCodes;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * Admin console render processing.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 2.0.0.1, May 21, 2020
 * @since 0.4.1
 */
@Singleton
public class AdminConsole {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(AdminConsole.class);

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * Option query service.
     */
    @Inject
    private OptionQueryService optionQueryService;

    /**
     * User query service.
     */
    @Inject
    private UserQueryService userQueryService;

    /**
     * Export service.
     */
    @Inject
    private ExportService exportService;

    /**
     * DataModelService.
     */
    @Inject
    private DataModelService dataModelService;

    /**
     * Event manager.
     */
    @Inject
    private EventManager eventManager;

    /**
     * Import service.
     */
    @Inject
    private ImportService importService;

    /**
     * Shows administrator index with the specified context.
     *
     * @param context the specified context
     */
    public void showAdminIndex(final RequestContext context) {
        final String templateName = "admin-index.ftl";
        final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, templateName);
        final Map<String, String> langs = langPropsService.getAll(Latkes.getLocale());
        final Map<String, Object> dataModel = renderer.getDataModel();
        dataModel.putAll(langs);
        final JSONObject currentUser = Solos.getCurrentUser(context);
        final String userName = currentUser.optString(User.USER_NAME);
        dataModel.put(User.USER_NAME, userName);
        final String roleName = currentUser.optString(User.USER_ROLE);
        dataModel.put(User.USER_ROLE, roleName);
        final String userAvatar = currentUser.optString(UserExt.USER_AVATAR);
        dataModel.put(Common.GRAVATAR, userAvatar);

        try {
            final JSONObject preference = optionQueryService.getPreference();
            // 支持配置编辑器模式 https://github.com/88250/solo/issues/95
            String editorMode = preference.optString(Option.ID_C_EDITOR_MODE);
            if (StringUtils.isBlank(editorMode)) {
                editorMode = "wysiwyg";
            }
            dataModel.put(Option.ID_C_EDITOR_MODE, editorMode);

            dataModel.put(Option.ID_C_LOCALE_STRING, preference.getString(Option.ID_C_LOCALE_STRING));
            dataModel.put(Option.ID_C_BLOG_TITLE, preference.getString(Option.ID_C_BLOG_TITLE));
            dataModel.put(Option.ID_C_BLOG_SUBTITLE, preference.getString(Option.ID_C_BLOG_SUBTITLE));
            dataModel.put(Common.VERSION, Server.VERSION);
            dataModel.put(Common.STATIC_RESOURCE_VERSION, Latkes.getStaticResourceVersion());
            dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR)));
            dataModel.put(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT, preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT));
            dataModel.put(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE, preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE));
            final JSONObject skin = optionQueryService.getSkin();
            dataModel.put(Option.CATEGORY_C_SKIN, skin.optString(Option.ID_C_SKIN_DIR_NAME));
            Keys.fillRuntime(dataModel);
            dataModel.put(Common.LUTE_AVAILABLE, Markdowns.LUTE_AVAILABLE);
            // 内置 HTTPS+CDN 文件存储 https://github.com/b3log/solo/issues/12556
            dataModel.put(Common.UPLOAD_TOKEN, "");
            dataModel.put(Common.UPLOAD_URL, "");
            dataModel.put(Common.UPLOAD_MSG, langPropsService.get("getUploadTokenErrLabel"));
            final JSONObject upload = Solos.getUploadToken(context);
            if (null != upload) {
                dataModel.put(Common.UPLOAD_TOKEN, upload.optString(Common.UPLOAD_TOKEN));
                dataModel.put(Common.UPLOAD_URL, upload.optString(Common.UPLOAD_URL));
                dataModel.put(Common.UPLOAD_MSG, upload.optString(Common.UPLOAD_MSG));
            }
            dataModelService.fillFaviconURL(dataModel, preference);
            dataModelService.fillUsite(dataModel);
            dataModelService.fillCommon(context, dataModel, preference);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Admin index render failed", e);
        }

        fireFreeMarkerActionEvent(templateName, dataModel);
    }

    /**
     * Shows administrator functions with the specified context.
     *
     * @param context the specified context
     */
    public void showAdminFunctions(final RequestContext context) {
        final String requestURI = context.requestURI();
        final String templateName = StringUtils.substringBetween(requestURI, Latkes.getContextPath() + '/', ".") + ".ftl";
        final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, templateName);

        final Locale locale = Latkes.getLocale();
        final Map<String, String> langs = langPropsService.getAll(locale);
        final Map<String, Object> dataModel = renderer.getDataModel();

        // 使用 MySQL 时不启用 SQL 导出功能 https://github.com/b3log/solo/issues/12806
        dataModel.put("supportExport", Latkes.RuntimeDatabase.H2 == Latkes.getRuntimeDatabase());
        dataModel.putAll(langs);
        Keys.fillRuntime(dataModel);
        dataModel.put(Option.ID_C_LOCALE_STRING, locale.toString());

        fireFreeMarkerActionEvent(templateName, dataModel);
    }

    /**
     * Shows administrator preference function with the specified context.
     *
     * @param context the specified context
     */
    public void showAdminPreferenceFunction(final RequestContext context) {
        final String templateName = "admin-preference.ftl";
        final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, templateName);

        final Locale locale = Latkes.getLocale();
        final Map<String, String> langs = langPropsService.getAll(locale);
        final Map<String, Object> dataModel = renderer.getDataModel();
        dataModel.putAll(langs);
        dataModel.put(Option.ID_C_LOCALE_STRING, locale.toString());

        final JSONObject preference = optionQueryService.getPreference();
        final StringBuilder timeZoneIdOptions = new StringBuilder();
        final String[] availableIDs = TimeZone.getAvailableIDs();
        for (int i = 0; i < availableIDs.length; i++) {
            final String id = availableIDs[i];
            String option;

            if (id.equals(preference.optString(Option.ID_C_TIME_ZONE_ID))) {
                option = "<option value=\"" + id + "\" selected=\"true\">" + id + "</option>";
            } else {
                option = "<option value=\"" + id + "\">" + id + "</option>";
            }

            timeZoneIdOptions.append(option);
        }

        dataModel.put(Common.LUTE_AVAILABLE, Markdowns.LUTE_AVAILABLE);
        dataModel.put("timeZoneIdOptions", timeZoneIdOptions.toString());
        fireFreeMarkerActionEvent(templateName, dataModel);
    }

    /**
     * Imports markdown zip.
     *
     * @param context the specified context
     */
    public void importMarkdownZip(final RequestContext context) {
        context.renderJSON(StatusCodes.ERR);
        final Request request = context.getRequest();
        final FileUpload file = request.getFileUpload("file");
        if (null == file) {
            context.renderMsg(langPropsService.get("allowZipOnlyLabel"));
            return;
        }
        final String fileName = file.getFilename();
        String suffix = StringUtils.substringAfterLast(fileName, ".");
        if (!StringUtils.equalsIgnoreCase(suffix, "zip")) {
            context.renderMsg(langPropsService.get("allowZipOnlyLabel"));
            return;
        }

        try {
            final byte[] bytes = file.getData();
            final String tmpDir = System.getProperty("java.io.tmpdir");
            final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss");
            final String zipPath = tmpDir + File.separator + "solo-import-" + date + ".zip";
            final File zipFile = new File(zipPath);
            FileUtils.writeByteArrayToFile(zipFile, bytes);
            final String unzipPath = tmpDir + File.separator + "solo-import-" + date;
            final File unzipDir = new File(unzipPath);
            ZipUtil.unzip(zipFile, unzipDir);
            final JSONObject result = importService.importMarkdownDir(unzipDir);
            final int succCount = result.optInt("succCount");
            final int failCount = result.optInt("failCount");
            FileUtils.deleteQuietly(zipFile);
            FileUtils.deleteQuietly(unzipDir);
            context.renderJSONValue(Keys.CODE, StatusCodes.SUCC);
            String msg = langPropsService.get("importSuccLabel");
            msg = msg.replace("${succCount}", succCount + "");
            if (0 < failCount) {
                msg = langPropsService.get("importFailLabel");
                msg = msg.replace("${failCount}", failCount + "");
            }
            context.renderMsg(msg);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Imports markdown file failed", e);
            context.renderMsg(langPropsService.get("importFailedSeeLogLabel"));
        }
    }

    /**
     * Exports data as SQL zip file.
     *
     * @param context the specified request context
     */
    public void exportSQL(final RequestContext context) {
        final Response response = context.getResponse();

        if (!Solos.isAdminLoggedIn(context)) {
            context.sendError(401);
            return;
        }

        try {
            Thread.sleep(550); // 前端会发两次请求，文件名又是按秒生成，所以两次请求需要错开至少 1 秒避免文件名冲突
        } catch (final Exception e) {
            // ignored
        }

        final byte[] zipData = exportService.exportSQL();
        if (null == zipData) {
            context.sendError(500);
            return;
        }

        final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss");
        response.setContentType("application/zip");
        final String fileName = "solo-sql-" + date + ".zip";
        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        response.sendBytes(zipData);
    }

    /**
     * Exports data as JSON zip file.
     *
     * @param context the specified request context
     */
    public void exportJSON(final RequestContext context) {
        final Response response = context.getResponse();
        if (!Solos.isAdminLoggedIn(context)) {
            context.sendError(401);
            return;
        }

        try {
            Thread.sleep(550);
        } catch (final Exception e) {
            // ignored
        }

        final String tmpDir = System.getProperty("java.io.tmpdir");
        final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss");
        String localFilePath = tmpDir + File.separator + "solo-" + date + ".json";
        LOGGER.trace(localFilePath);
        final File localFile = new File(localFilePath);

        try {
            final JSONObject json = exportService.getJSONs();
            final byte[] data = json.toString(4).getBytes(StandardCharsets.UTF_8);

            try (final OutputStream output = new FileOutputStream(localFile)) {
                IOUtils.write(data, output);
            }

            try (final FileInputStream inputStream = new FileInputStream(ZipUtil.zip(localFile))) {
                final byte[] zipData = IOUtils.toByteArray(inputStream);
                response.setContentType("application/zip");
                final String fileName = "solo-json-" + date + ".zip";
                response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
                response.sendBytes(zipData);
            }
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Export failed", e);
            context.renderJSON(StatusCodes.ERR).renderMsg("Export failed, please check log");
        }
    }

    /**
     * Exports data as Hexo markdown zip file.
     *
     * @param context the specified request context
     */
    public void exportHexo(final RequestContext context) {
        final Response response = context.getResponse();
        if (!Solos.isAdminLoggedIn(context)) {
            context.sendError(401);
            return;
        }

        try {
            Thread.sleep(550);
        } catch (final Exception e) {
            // ignored
        }

        final String tmpDir = System.getProperty("java.io.tmpdir");
        final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss");
        String localFilePath = tmpDir + File.separator + "solo-hexo-" + date;
        LOGGER.trace(localFilePath);
        final File localFile = new File(localFilePath);

        final File postDir = new File(localFilePath + File.separator + "posts");
        final File passwordDir = new File(localFilePath + File.separator + "passwords");
        final File draftDir = new File(localFilePath + File.separator + "drafts");

        try {
            if (!postDir.mkdirs()) {
                throw new Exception("Create dir [" + postDir.getPath() + "] failed");
            }
            if (!passwordDir.mkdirs()) {
                throw new Exception("Create dir [" + passwordDir.getPath() + "] failed");
            }
            if (!draftDir.mkdirs()) {
                throw new Exception("Create dir [" + draftDir.getPath() + "] failed");
            }

            final JSONObject result = exportService.exportHexoMDs();
            final List<JSONObject> posts = (List<JSONObject>) result.opt("posts");
            exportService.exportHexoMd(posts, postDir.getPath());
            final List<JSONObject> passwords = (List<JSONObject>) result.opt("passwords");
            exportService.exportHexoMd(passwords, passwordDir.getPath());
            final List<JSONObject> drafts = (List<JSONObject>) result.opt("drafts");
            exportService.exportHexoMd(drafts, draftDir.getPath());

            final File zipFile = ZipUtil.zip(localFile);
            byte[] zipData;
            try (final FileInputStream inputStream = new FileInputStream(zipFile)) {
                zipData = IOUtils.toByteArray(inputStream);
                response.setContentType("application/zip");
                final String fileName = "solo-hexo-" + date + ".zip";
                response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
            }

            response.sendBytes(zipData);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Export failed", e);
            context.renderJSON(StatusCodes.ERR).renderMsg("Export failed, please check log");
        }
    }

    /**
     * Fires FreeMarker action event with the host template name and data model.
     *
     * @param hostTemplateName the specified host template name
     * @param dataModel        the specified data model
     */
    private void fireFreeMarkerActionEvent(final String hostTemplateName, final Map<String, Object> dataModel) {
        final ViewLoadEventData data = new ViewLoadEventData();

        data.setViewName(hostTemplateName);
        data.setDataModel(dataModel);
        eventManager.fireEventSynchronously(new Event<>(Keys.FREEMARKER_ACTION, data));
        if (StringUtils.isBlank((String) dataModel.get(Plugin.PLUGINS))) {
            // There is no plugin for this template, fill ${plugins} with blank.
            dataModel.put(Plugin.PLUGINS, "");
        }
    }
}


<|start_filename|>src/main/java/org/b3log/solo/model/rss/Channel.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.model.rss;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.b3log.latke.util.XMLs;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * RSS 2.0 channel.
 * <p>
 * See <a href="http://cyber.law.harvard.edu/rss/rss.html">RSS 2.0 at Harvard Law</a> for more details.
 * </p>
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.1.1.2, Aug 2, 2018
 * @see Item
 * @see Category
 * @since 0.3.1
 */
public final class Channel {

    /**
     * Start.
     */
    private static final String START = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" "
            + "xmlns:atom=\"http://www.w3.org/2005/Atom\"><channel>";

    /**
     * End.
     */
    private static final String END = "</channel></rss>";

    /**
     * Start title element.
     */
    private static final String START_TITLE_ELEMENT = "<title>";

    /**
     * End title element.
     */
    private static final String END_TITLE_ELEMENT = "</title>";

    /**
     * Start link element.
     */
    private static final String START_LINK_ELEMENT = "<link>";

    /**
     * Atom link variable.
     */
    private static final String ATOM_LINK_VARIABLE = "${atomLink}";

    /**
     * End link element.
     */
    private static final String END_LINK_ELEMENT = "</link>";

    /**
     * Atom link element.
     */
    private static final String ATOM_LINK_ELEMENT = "<atom:link href=\"" + ATOM_LINK_VARIABLE
            + "\" rel=\"self\" type=\"application/rss+xml\" />";

    /**
     * Start description element.
     */
    private static final String START_DESCRIPTION_ELEMENT = "<description>";

    /**
     * End description element.
     */
    private static final String END_DESCRIPTION_ELEMENT = "</description>";

    /**
     * Start generator element.
     */
    private static final String START_GENERATOR_ELEMENT = "<generator>";

    /**
     * End generator element.
     */
    private static final String END_GENERATOR_ELEMENT = "</generator>";

    /**
     * Start language element.
     */
    private static final String START_LANGUAGE_ELEMENT = "<language>";

    /**
     * End language element.
     */
    private static final String END_LANGUAGE_ELEMENT = "</language>";

    /**
     * Start last build date element.
     */
    private static final String START_LAST_BUILD_DATE_ELEMENT = "<lastBuildDate>";

    /**
     * End last build date element.
     */
    private static final String END_LAST_BUILD_DATE_ELEMENT = "</lastBuildDate>";

    /**
     * Title.
     */
    private String title;

    /**
     * Link.
     */
    private String link;

    /**
     * Atom link.
     */
    private String atomLink;

    /**
     * Description.
     */
    private String description;

    /**
     * Generator.
     */
    private String generator;

    /**
     * Last build date.
     */
    private Date lastBuildDate;

    /**
     * Language.
     */
    private String language;

    /**
     * Items.
     */
    private final List<Item> items = new ArrayList<>();

    /**
     * Gets the atom link.
     *
     * @return atom link
     */
    public String getAtomLink() {
        return atomLink;
    }

    /**
     * Sets the atom link with the specified atom link.
     *
     * @param atomLink the specified atom link
     */
    public void setAtomLink(final String atomLink) {
        this.atomLink = atomLink;
    }

    /**
     * Gets the last build date.
     *
     * @return last build date
     */
    public Date getLastBuildDate() {
        return lastBuildDate;
    }

    /**
     * Sets the last build date with the specified last build date.
     *
     * @param lastBuildDate the specified last build date
     */
    public void setLastBuildDate(final Date lastBuildDate) {
        this.lastBuildDate = lastBuildDate;
    }

    /**
     * Gets generator.
     *
     * @return generator
     */
    public String getGenerator() {
        return generator;
    }

    /**
     * Sets the generator with the specified generator.
     *
     * @param generator the specified generator
     */
    public void setGenerator(final String generator) {
        this.generator = generator;
    }

    /**
     * Gets the link.
     *
     * @return link
     */
    public String getLink() {
        return link;
    }

    /**
     * Sets the link with the specified link.
     *
     * @param link the specified link
     */
    public void setLink(final String link) {
        this.link = link;
    }

    /**
     * Gets the title.
     *
     * @return title
     */
    public String getTitle() {
        return title;
    }

    /**
     * Sets the title with the specified title.
     *
     * @param title the specified title
     */
    public void setTitle(final String title) {
        this.title = title;
    }

    /**
     * Adds the specified item.
     *
     * @param item the specified item
     */
    public void addItem(final Item item) {
        items.add(item);
    }

    /**
     * Gets the description.
     *
     * @return description
     */
    public String getDescription() {
        return description;
    }

    /**
     * Sets the description with the specified description.
     *
     * @param description the specified description
     */
    public void setDescription(final String description) {
        this.description = description;
    }

    /**
     * Gets the language.
     *
     * @return language
     */
    public String getLanguage() {
        return language;
    }

    /**
     * Sets the language with the specified language.
     *
     * @param language the specified language
     */
    public void setLanguage(final String language) {
        this.language = language;
    }

    @Override
    public String toString() {
        final StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(START);
        stringBuilder.append(START_TITLE_ELEMENT).append(StringEscapeUtils.escapeXml(title)).append(END_TITLE_ELEMENT);
        stringBuilder.append(START_LINK_ELEMENT).append(StringEscapeUtils.escapeXml(link)).append(END_LINK_ELEMENT);
        stringBuilder.append(ATOM_LINK_ELEMENT.replace(ATOM_LINK_VARIABLE, atomLink));
        stringBuilder.append(START_DESCRIPTION_ELEMENT).append(StringEscapeUtils.escapeXml(description)).append(END_DESCRIPTION_ELEMENT);
        stringBuilder.append(START_GENERATOR_ELEMENT).append(StringEscapeUtils.escapeXml(generator)).append(END_GENERATOR_ELEMENT);
        stringBuilder.append(START_LAST_BUILD_DATE_ELEMENT).append(DateFormatUtils.SMTP_DATETIME_FORMAT.format(lastBuildDate)).append(END_LAST_BUILD_DATE_ELEMENT);
        stringBuilder.append(START_LANGUAGE_ELEMENT).append(StringEscapeUtils.escapeXml(language)).append(END_LANGUAGE_ELEMENT);
        for (final Item item : items) {
            stringBuilder.append(item.toString());
        }
        stringBuilder.append(END);
        return XMLs.format(stringBuilder.toString());
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/B3Receiver.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.model.User;
import org.b3log.solo.model.Article;
import org.b3log.solo.model.Common;
import org.b3log.solo.model.UserExt;
import org.b3log.solo.repository.ArticleRepository;
import org.b3log.solo.repository.UserRepository;
import org.b3log.solo.service.ArticleMgmtService;
import org.b3log.solo.service.ArticleQueryService;
import org.b3log.solo.service.OptionQueryService;
import org.b3log.solo.service.UserMgmtService;
import org.json.JSONObject;

/**
 * Receiving articles from B3log community. Visits <a href="https://ld246.com/article/1546941897596">B3log 构思 - 分布式社区网络</a> for more details.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 3.0.0.5, Jul 8, 2020
 * @since 0.5.5
 */
@Singleton
public class B3Receiver {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(B3Receiver.class);

    /**
     * User repository.
     */
    @Inject
    private UserRepository userRepository;

    /**
     * Article repository.
     */
    @Inject
    private ArticleRepository articleRepository;

    /**
     * Article management service.
     */
    @Inject
    private ArticleMgmtService articleMgmtService;

    /**
     * Article query service.
     */
    @Inject
    private ArticleQueryService articleQueryService;

    /**
     * Option query service.
     */
    @Inject
    private OptionQueryService optionQueryService;

    /**
     * User management service.
     */
    @Inject
    private UserMgmtService userMgmtService;

    /**
     * Adds or updates an article with the specified request.
     * <p>
     * Request json:
     * <pre>
     * {
     *     "article": {
     *         "id": "",
     *         "title": "",
     *         "content": "",
     *         "contentHTML": "",
     *         "tags": "tag1,tag2,tag3"
     *     },
     *     "client": {
     *         "userName": "",
     *         "userB3Key": ""
     *     }
     * }
     * </pre>
     * </p>
     * <p>
     * Renders the response with a json object, for example,
     * <pre>
     * {
     *     "code": int,
     *     "oId": "", // Generated article id
     *     "msg": ""
     * }
     * </pre>
     * </p>
     *
     * @param context the specified request context
     */
    public void receiveArticle(final RequestContext context) {
        final JSONObject ret = new JSONObject().put(Keys.CODE, 0);
        context.renderJSON(ret);

        final JSONObject requestJSONObject = context.requestJSON();
        LOGGER.log(Level.DEBUG, "Adds an article from Sym [" + requestJSONObject.toString() + "]");

        try {
            final JSONObject client = requestJSONObject.optJSONObject("client");
            if (null == client) {
                ret.put(Keys.CODE, 1);
                final String msg = "Not found client";
                ret.put(Keys.MSG, msg);
                LOGGER.log(Level.WARN, msg);
                return;
            }


            final String articleAuthorName = client.optString(User.USER_NAME);
            final JSONObject articleAuthor = userRepository.getByUserName(articleAuthorName);
            if (null == articleAuthor) {
                ret.put(Keys.CODE, 1);
                final String msg = "Not found user [" + articleAuthorName + "]";
                ret.put(Keys.MSG, msg);
                LOGGER.log(Level.WARN, msg);
                return;
            }

            final String b3Key = client.optString(UserExt.USER_B3_KEY);
            final String key = articleAuthor.optString(UserExt.USER_B3_KEY);
            if (!StringUtils.equals(key, b3Key)) {
                ret.put(Keys.CODE, 1);
                final String msg = "Wrong key";
                ret.put(Keys.MSG, msg);
                LOGGER.log(Level.WARN, msg);
                return;
            }

            final JSONObject symArticle = requestJSONObject.optJSONObject(Article.ARTICLE);
            if (null == symArticle) {
                ret.put(Keys.CODE, 1);
                final String msg = "Not found article";
                ret.put(Keys.MSG, msg);
                LOGGER.log(Level.WARN, msg);
                return;
            }

            final String title = symArticle.optString("title");
            final String articleId = symArticle.optString("id");
            final JSONObject oldArticle = articleQueryService.getArticleById(articleId);
            if (null == oldArticle) {
                final JSONObject article = new JSONObject().
                        put(Keys.OBJECT_ID, symArticle.optString("id")).
                        put(Article.ARTICLE_TITLE, title).
                        put(Article.ARTICLE_CONTENT, symArticle.optString("content")).
                        put(Article.ARTICLE_TAGS_REF, symArticle.optString("tags"));
                article.put(Article.ARTICLE_AUTHOR_ID, articleAuthor.getString(Keys.OBJECT_ID));
                final String articleContent = article.optString(Article.ARTICLE_CONTENT);
                article.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(articleContent));
                article.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED);
                article.put(Common.POST_TO_COMMUNITY, false); // Do not send to rhythm
                article.put(Article.ARTICLE_VIEW_PWD, "");
                final String content = article.getString(Article.ARTICLE_CONTENT);
                article.put(Article.ARTICLE_CONTENT, content);
                final JSONObject addRequest = new JSONObject().put(Article.ARTICLE, article);
                articleMgmtService.addArticle(addRequest);
                LOGGER.log(Level.INFO, "Added an article [" + title + "] via Sym");
                return;
            }

            final String articleContent = symArticle.optString("content");
            oldArticle.put(Article.ARTICLE_ABSTRACT, Article.getAbstractText(articleContent));
            oldArticle.put(Article.ARTICLE_CONTENT, articleContent);
            oldArticle.put(Article.ARTICLE_TITLE, symArticle.optString("title"));
            oldArticle.put(Article.ARTICLE_TAGS_REF, symArticle.optString("tags"));
            oldArticle.put(Common.POST_TO_COMMUNITY, false); // Do not send to rhythm
            final JSONObject updateRequest = new JSONObject().put(Article.ARTICLE, oldArticle);
            articleMgmtService.updateArticle(updateRequest);
            LOGGER.log(Level.INFO, "Updated an article [" + title + "] via Sym");
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            ret.put(Keys.CODE, 1).put(Keys.MSG, e.getMessage());
        }
    }
}


<|start_filename|>src/test/java/org/b3log/solo/processor/console/PageConsoleTestCase.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor.console;

import org.apache.commons.lang3.StringUtils;
import org.b3log.latke.Keys;
import org.b3log.latke.repository.Query;
import org.b3log.solo.AbstractTestCase;
import org.b3log.solo.MockRequest;
import org.b3log.solo.MockResponse;
import org.b3log.solo.model.Common;
import org.b3log.solo.model.Page;
import org.json.JSONObject;
import org.testng.Assert;
import org.testng.annotations.Test;

/**
 * {@link PageConsole} test case.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.1, Apr 19, 2019
 * @since 2.9.8
 */
@Test(suiteName = "processor")
public class PageConsoleTestCase extends AbstractTestCase {

    /**
     * addPage.
     */
    public void addPage() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/page/");
        request.setMethod("POST");
        final JSONObject requestJSON = new JSONObject();
        final JSONObject page = new JSONObject();
        requestJSON.put(Page.PAGE, page);
        page.put(Page.PAGE_TITLE, "链滴");
        page.put(Page.PAGE_PERMALINK, "https://ld246.com");
        page.put(Page.PAGE_OPEN_TARGET, "");
        page.put(Page.PAGE_ICON, "");
        request.setJSON(requestJSON);

        mockAdminLogin(request);

        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "code\":0"));
    }

    /**
     * updatePage.
     *
     * @throws Exception exception
     */
    @Test(dependsOnMethods = "addPage")
    public void updatePage() throws Exception {
        final JSONObject p = getPageRepository().getList(new Query()).get(0);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/page/");
        request.setMethod("PUT");
        final JSONObject requestJSON = new JSONObject();
        requestJSON.put(Page.PAGE, p);
        request.setJSON(requestJSON);

        mockAdminLogin(request);

        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "code\":0"));
    }

    /**
     * changeOrder.
     *
     * @throws Exception exception
     */
    @Test(dependsOnMethods = "updatePage")
    public void changeOrder() throws Exception {
        final JSONObject p = getPageRepository().getList(new Query()).get(0);
        final String pageId = p.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/page/order/");
        request.setMethod("PUT");
        final JSONObject requestJSON = new JSONObject();
        requestJSON.put(Keys.OBJECT_ID, pageId);
        requestJSON.put(Common.DIRECTION, "up");
        request.setJSON(requestJSON);

        mockAdminLogin(request);

        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "code\":0"));
    }

    /**
     * getPage.
     *
     * @throws Exception exception
     */
    @Test(dependsOnMethods = "changeOrder")
    public void getPage() throws Exception {
        final JSONObject p = getPageRepository().getList(new Query()).get(0);
        final String pageId = p.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/page/" + pageId);

        mockAdminLogin(request);

        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "code\":0"));
    }

    /**
     * getPages.
     *
     * @throws Exception exception
     */
    @Test(dependsOnMethods = "getPage")
    public void getPages() throws Exception {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/pages/1/10/20");

        mockAdminLogin(request);

        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "code\":0"));
    }

    /**
     * removePage.
     *
     * @throws Exception exception
     */
    @Test(dependsOnMethods = "getPages")
    public void removeLink() throws Exception {
        final JSONObject p = getPageRepository().getList(new Query()).get(0);
        final String pageId = p.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/page/" + pageId);
        request.setMethod("DELETE");

        mockAdminLogin(request);

        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "code\":0"));
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/ArticleProcessor.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.event.Event;
import org.b3log.latke.event.EventManager;
import org.b3log.latke.http.Request;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.http.Session;
import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
import org.b3log.latke.http.renderer.JsonRenderer;
import org.b3log.latke.http.renderer.TextHtmlRenderer;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.model.Pagination;
import org.b3log.latke.model.User;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.service.ServiceException;
import org.b3log.latke.util.Dates;
import org.b3log.latke.util.Locales;
import org.b3log.latke.util.Paginator;
import org.b3log.latke.util.Stopwatchs;
import org.b3log.solo.Server;
import org.b3log.solo.event.EventTypes;
import org.b3log.solo.model.*;
import org.b3log.solo.processor.console.ConsoleRenderer;
import org.b3log.solo.service.*;
import org.b3log.solo.util.Markdowns;
import org.b3log.solo.util.Skins;
import org.b3log.solo.util.Solos;
import org.b3log.solo.util.StatusCodes;
import org.json.JSONObject;
import org.jsoup.Jsoup;

import java.util.*;

/**
 * Article processor.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @author <a href="https://ld246.com/member/ZephyrJung">Zephyr</a>
 * @version 2.0.0.3, Jul 8, 2020
 * @since 0.3.1
 */
@Singleton
public class ArticleProcessor {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(ArticleProcessor.class);

    /**
     * Article query service.
     */
    @Inject
    private ArticleQueryService articleQueryService;

    /**
     * Tag query service.
     */
    @Inject
    private TagQueryService tagQueryService;

    /**
     * DataModelService.
     */
    @Inject
    private DataModelService dataModelService;

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * Option query service.
     */
    @Inject
    private OptionQueryService optionQueryService;

    /**
     * Archive date query service.
     */
    @Inject
    private ArchiveDateQueryService archiveDateQueryService;

    /**
     * User query service.
     */
    @Inject
    private UserQueryService userQueryService;

    /**
     * Article management service.
     */
    @Inject
    private ArticleMgmtService articleMgmtService;

    /**
     * Event manager.
     */
    @Inject
    private EventManager eventManager;

    /**
     * Markdowns.
     * <p>
     * Renders the response with a json object, for example,
     * <pre>
     * {
     *     "html": ""
     * }
     * </pre>
     * </p>
     *
     * @param context the specified request context
     */
    public void markdown2HTML(final RequestContext context) {
        final JSONObject result = Solos.newSucc();
        context.renderJSON(result);

        final String markdownText = context.requestJSON().optString("markdownText");
        if (StringUtils.isBlank(markdownText)) {
            result.put(Common.DATA, "");
            return;
        }

        if (!Solos.isLoggedIn(context)) {
            result.put(Keys.CODE, -1);
            result.put(Keys.MSG, langPropsService.get("getFailLabel"));
            return;
        }

        try {
            final String html = Markdowns.toHTML(markdownText);
            result.put(Common.DATA, html);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            result.put(Keys.CODE, -1);
            result.put(Keys.MSG, langPropsService.get("getFailLabel"));
        }
    }

    /**
     * Shows the article view password form.
     *
     * @param context the specified context
     */
    public void showArticlePwdForm(final RequestContext context) {
        final String articleId = context.param("articleId");
        if (StringUtils.isBlank(articleId)) {
            context.sendError(404);
            return;
        }

        final JSONObject article = articleQueryService.getArticleById(articleId);
        if (null == article) {
            context.sendError(404);
            return;
        }

        final AbstractFreeMarkerRenderer renderer = new ConsoleRenderer(context, "article-pwd.ftl");
        final Map<String, Object> dataModel = renderer.getDataModel();
        dataModel.put("articleId", articleId);
        dataModel.put("articlePermalink", article.optString(Article.ARTICLE_PERMALINK));
        dataModel.put("articleTitle", article.optString(Article.ARTICLE_TITLE));
        dataModel.put("articleAbstract", article.optString(Article.ARTICLE_ABSTRACT));
        final String msg = context.param(Keys.MSG);

        if (StringUtils.isNotBlank(msg)) {
            dataModel.put(Keys.MSG, langPropsService.get("passwordNotMatchLabel"));
        }

        final Map<String, String> langs = langPropsService.getAll(Latkes.getLocale());
        dataModel.putAll(langs);

        final JSONObject preference = optionQueryService.getPreference();
        dataModel.put(Option.ID_C_BLOG_TITLE, preference.getString(Option.ID_C_BLOG_TITLE));
        dataModel.put(Common.VERSION, Server.VERSION);
        dataModel.put(Common.STATIC_RESOURCE_VERSION, Latkes.getStaticResourceVersion());
        dataModel.put(Common.YEAR, String.valueOf(Calendar.getInstance().get(Calendar.YEAR)));

        Keys.fillRuntime(dataModel);
        dataModelService.fillFaviconURL(dataModel, preference);
        dataModelService.fillUsite(dataModel);
    }

    /**
     * Processes the article view password form submits.
     *
     * @param context the specified context
     */
    public void onArticlePwdForm(final RequestContext context) {
        try {
            final Request request = context.getRequest();
            final String articleId = context.param("articleId");
            final String pwdTyped = context.param("pwdTyped");

            final JSONObject article = articleQueryService.getArticleById(articleId);
            if (article.getString(Article.ARTICLE_VIEW_PWD).equals(pwdTyped)) {
                final Session session = request.getSession();
                if (null != session) {
                    JSONObject viewPwds;
                    final String viewPwdsStr = session.getAttribute(Common.ARTICLES_VIEW_PWD);
                    if (null == viewPwdsStr) {
                        viewPwds = new JSONObject();
                    } else {
                        viewPwds = new JSONObject(viewPwdsStr);
                    }

                    viewPwds.put(articleId, pwdTyped);
                    session.setAttribute(Common.ARTICLES_VIEW_PWD, viewPwds.toString());
                }

                context.sendRedirect(Latkes.getServePath() + article.getString(Article.ARTICLE_PERMALINK));
                return;
            }

            context.sendRedirect(Latkes.getServePath() + "/console/article-pwd?articleId=" + article.optString(Keys.OBJECT_ID) + "&msg=1");
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Processes article view password form submits failed", e);
            context.sendError(404);
        }
    }

    /**
     * Gets random articles with the specified context.
     *
     * @param context the specified context
     */
    public void getRandomArticles(final RequestContext context) {
        final JSONObject jsonObject = new JSONObject();

        final JSONObject preference = optionQueryService.getPreference();
        final int displayCnt = preference.getInt(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT);
        if (0 == displayCnt) {
            jsonObject.put(Common.RANDOM_ARTICLES, new ArrayList<JSONObject>());
            final JsonRenderer renderer = new JsonRenderer();
            context.setRenderer(renderer);
            renderer.setJSONObject(jsonObject);
            return;
        }

        Stopwatchs.start("Get Random Articles");
        final List<JSONObject> randomArticles = getRandomArticles(preference);
        jsonObject.put(Common.RANDOM_ARTICLES, randomArticles);
        final JsonRenderer renderer = new JsonRenderer();
        context.setRenderer(renderer);
        renderer.setJSONObject(jsonObject);
        Stopwatchs.end();
    }

    /**
     * Gets relevant articles with the specified context.
     *
     * @param context the specified context
     */
    public void getRelevantArticles(final RequestContext context) {
        final JSONObject jsonObject = new JSONObject();

        final JSONObject preference = optionQueryService.getPreference();
        final int displayCnt = preference.getInt(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT);
        if (0 == displayCnt) {
            jsonObject.put(Common.RANDOM_ARTICLES, new ArrayList<JSONObject>());

            final JsonRenderer renderer = new JsonRenderer();
            context.setRenderer(renderer);
            renderer.setJSONObject(jsonObject);
            return;
        }

        final Request request = context.getRequest();
        Stopwatchs.start("Get Relevant Articles");
        final String articleId = context.pathVar("id");
        if (StringUtils.isBlank(articleId)) {
            context.sendError(404);
            return;
        }

        final JSONObject article = articleQueryService.getArticleById(articleId);
        if (null == article) {
            context.sendError(404);
            return;
        }

        final List<JSONObject> relevantArticles = articleQueryService.getRelevantArticles(article, preference);
        jsonObject.put(Common.RELEVANT_ARTICLES, relevantArticles);

        final JsonRenderer renderer = new JsonRenderer();
        context.setRenderer(renderer);
        renderer.setJSONObject(jsonObject);

        Stopwatchs.end();
    }

    /**
     * Gets article content with the specified context.
     *
     * @param context the specified context
     */
    public void getArticleContent(final RequestContext context) {
        final String articleId = context.param("id");
        if (StringUtils.isBlank(articleId)) {
            return;
        }

        final TextHtmlRenderer renderer = new TextHtmlRenderer();
        context.setRenderer(renderer);

        String content;
        try {
            content = articleQueryService.getArticleContent(context, articleId);
        } catch (final ServiceException e) {
            LOGGER.log(Level.ERROR, "Can not get article content", e);
            return;
        }

        if (null == content) {
            return;
        }

        renderer.setContent(content);
    }

    /**
     * Gets articles paged with the specified context.
     *
     * @param context the specified context
     */
    public void getArticlesByPage(final RequestContext context) {
        final JSONObject jsonObject = new JSONObject();
        final Request request = context.getRequest();
        final int currentPageNum = Paginator.getPage(request);

        Stopwatchs.start("Get Articles Paged [pageNum=" + currentPageNum + ']');
        try {
            jsonObject.put(Keys.CODE, StatusCodes.SUCC);

            final JSONObject preference = optionQueryService.getPreference();
            final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);
            final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE);

            final StringBuilder pathBuilder = new StringBuilder();
            pathBuilder.append(currentPageNum).append('/').append(pageSize).append('/').append(windowSize);

            final JSONObject requestJSONObject = Solos.buildPaginationRequest(pathBuilder.toString());
            requestJSONObject.put(Article.ARTICLE_STATUS, Article.ARTICLE_STATUS_C_PUBLISHED);
            requestJSONObject.put(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT, preference.optBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT));
            final JSONObject result = articleQueryService.getArticles(requestJSONObject);
            final List<JSONObject> articles = (List<JSONObject>) result.opt(Article.ARTICLES);
            dataModelService.setArticlesExProperties(context, articles, preference);

            jsonObject.put(Keys.RESULTS, result);
        } catch (final Exception e) {
            jsonObject.put(Keys.CODE, StatusCodes.ERR);
            LOGGER.log(Level.ERROR, "Gets article paged failed", e);
        } finally {
            Stopwatchs.end();
        }

        final JsonRenderer renderer = new JsonRenderer();
        context.setRenderer(renderer);
        renderer.setJSONObject(jsonObject);
    }

    /**
     * Gets tag articles paged with the specified context.
     *
     * @param context the specified context
     */
    public void getTagArticlesByPage(final RequestContext context) {
        final JSONObject jsonObject = new JSONObject();

        final Request request = context.getRequest();
        final String tagTitle = context.pathVar("tagTitle");
        final int currentPageNum = Paginator.getPage(request);
        Stopwatchs.start("Get Tag-Articles Paged [tagTitle=" + tagTitle + ", pageNum=" + currentPageNum + ']');
        try {
            jsonObject.put(Keys.CODE, StatusCodes.SUCC);

            final JSONObject preference = optionQueryService.getPreference();
            final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

            final JSONObject tagQueryResult = tagQueryService.getTagByTitle(tagTitle);
            if (null == tagQueryResult) {
                throw new Exception("Can not found tag [title=" + tagTitle + "]");
            }

            final JSONObject tag = tagQueryResult.getJSONObject(Tag.TAG);
            final String tagId = tag.getString(Keys.OBJECT_ID);
            final JSONObject tagArticleResult = articleQueryService.getArticlesByTag(tagId, currentPageNum, pageSize);
            if (null == tagArticleResult) {
                throw new Exception("Can not found tag [title=" + tagTitle + "]'s articles");
            }

            final List<JSONObject> articles = (List<JSONObject>) tagArticleResult.opt(Keys.RESULTS);
            final int pageCount = tagArticleResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);
            dataModelService.setArticlesExProperties(context, articles, preference);

            final JSONObject result = new JSONObject();
            final JSONObject pagination = new JSONObject();
            pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
            result.put(Pagination.PAGINATION, pagination);
            result.put(Article.ARTICLES, articles);
            jsonObject.put(Keys.RESULTS, result);
        } catch (final Exception e) {
            jsonObject.put(Keys.CODE, StatusCodes.ERR);
            LOGGER.log(Level.ERROR, "Gets article paged failed", e);
        } finally {
            Stopwatchs.end();
        }

        final JsonRenderer renderer = new JsonRenderer();
        context.setRenderer(renderer);
        renderer.setJSONObject(jsonObject);
    }

    /**
     * Gets tag articles paged with the specified context.
     *
     * @param context the specified context
     */
    public void getArchivesArticlesByPage(final RequestContext context) {
        final JSONObject jsonObject = new JSONObject();

        final Request request = context.getRequest();
        final String archiveDateString = context.pathVar("yyyy") + "/" + context.pathVar("MM");
        final int currentPageNum = Paginator.getPage(request);

        Stopwatchs.start("Get Archive-Articles Paged [archive=" + archiveDateString + ", pageNum=" + currentPageNum + ']');
        try {
            jsonObject.put(Keys.CODE, StatusCodes.SUCC);

            final JSONObject preference = optionQueryService.getPreference();
            final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

            final JSONObject archiveQueryResult = archiveDateQueryService.getByArchiveDateString(archiveDateString);
            if (null == archiveQueryResult) {
                throw new Exception("Can not found archive [archiveDate=" + archiveDateString + "]");
            }

            final JSONObject archiveDate = archiveQueryResult.getJSONObject(ArchiveDate.ARCHIVE_DATE);
            final String archiveDateId = archiveDate.getString(Keys.OBJECT_ID);

            final int articleCount = archiveDateQueryService.getArchiveDatePublishedArticleCount(archiveDateId);
            final int pageCount = (int) Math.ceil((double) articleCount / (double) pageSize);

            final List<JSONObject> articles = articleQueryService.getArticlesByArchiveDate(archiveDateId, currentPageNum, pageSize);
            dataModelService.setArticlesExProperties(context, articles, preference);

            final JSONObject result = new JSONObject();
            final JSONObject pagination = new JSONObject();
            pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
            result.put(Pagination.PAGINATION, pagination);
            result.put(Article.ARTICLES, articles);
            jsonObject.put(Keys.RESULTS, result);
        } catch (final Exception e) {
            jsonObject.put(Keys.CODE, StatusCodes.ERR);
            LOGGER.log(Level.ERROR, "Gets article paged failed", e);
        } finally {
            Stopwatchs.end();
        }

        final JsonRenderer renderer = new JsonRenderer();
        context.setRenderer(renderer);
        renderer.setJSONObject(jsonObject);
    }

    /**
     * Gets author articles paged with the specified context.
     *
     * @param context the specified context
     */
    public void getAuthorsArticlesByPage(final RequestContext context) {
        final JSONObject jsonObject = new JSONObject();

        final Request request = context.getRequest();
        final String authorId = context.pathVar("author");
        final int currentPageNum = Paginator.getPage(request);

        Stopwatchs.start("Get Author-Articles Paged [authorId=" + authorId + ", pageNum=" + currentPageNum + ']');
        try {
            jsonObject.put(Keys.CODE, StatusCodes.SUCC);

            final JSONObject preference = optionQueryService.getPreference();
            final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

            final JSONObject authorRet = userQueryService.getUser(authorId);
            if (null == authorRet) {
                context.sendError(404);
                return;
            }

            final JSONObject articlesResult = articleQueryService.getArticlesByAuthorId(authorId, currentPageNum, pageSize);
            final List<JSONObject> articles = (List<JSONObject>) articlesResult.opt(Keys.RESULTS);
            dataModelService.setArticlesExProperties(context, articles, preference);
            final int pageCount = articlesResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);

            final JSONObject result = new JSONObject();
            final JSONObject pagination = new JSONObject();
            pagination.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
            result.put(Pagination.PAGINATION, pagination);
            result.put(Article.ARTICLES, articles);
            jsonObject.put(Keys.RESULTS, result);
        } catch (final Exception e) {
            jsonObject.put(Keys.CODE, StatusCodes.ERR);
            LOGGER.log(Level.ERROR, "Gets article paged failed", e);
        } finally {
            Stopwatchs.end();
        }

        final JsonRenderer renderer = new JsonRenderer();
        context.setRenderer(renderer);
        renderer.setJSONObject(jsonObject);
    }

    /**
     * Shows author articles with the specified context.
     *
     * @param context the specified context
     */
    public void showAuthorArticles(final RequestContext context) {
        final Request request = context.getRequest();
        final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "author-articles.ftl");

        try {
            final String authorId = context.pathVar("author");
            final int currentPageNum = Paginator.getPage(request);
            LOGGER.log(Level.DEBUG, "Request author articles [authorId={}, currentPageNum={}]", authorId, currentPageNum);

            final JSONObject preference = optionQueryService.getPreference();
            if (null == preference) {
                context.sendError(404);
                return;
            }

            final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);
            final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE);

            final JSONObject result = userQueryService.getUser(authorId);
            if (null == result) {
                context.sendError(404);
                return;
            }

            final JSONObject articlesResult = articleQueryService.getArticlesByAuthorId(authorId, currentPageNum, pageSize);
            if (null == articlesResult) {
                context.sendError(404);
                return;
            }

            final List<JSONObject> articles = (List<JSONObject>) articlesResult.opt(Keys.RESULTS);
            dataModelService.setArticlesExProperties(context, articles, preference);
            final int pageCount = articlesResult.optJSONObject(Pagination.PAGINATION).optInt(Pagination.PAGINATION_PAGE_COUNT);
            final List<Integer> pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);

            final Map<String, Object> dataModel = renderer.getDataModel();
            final JSONObject author = result.getJSONObject(User.USER);
            prepareShowAuthorArticles(pageNums, dataModel, pageCount, currentPageNum, articles, author);
            dataModelService.fillCommon(context, dataModel, preference);
            dataModelService.fillFaviconURL(dataModel, preference);
            dataModelService.fillUsite(dataModel);
            Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMPLATE_DIR_NAME), dataModel);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            context.sendError(404);
        }
    }

    /**
     * Shows archive articles with the specified context.
     *
     * @param context the specified context
     */
    public void showArchiveArticles(final RequestContext context) {
        final Request request = context.getRequest();
        final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "archive-articles.ftl");

        try {
            final int currentPageNum = Paginator.getPage(request);
            final String archiveDateString = context.pathVar("yyyy") + "/" + context.pathVar("MM");
            LOGGER.log(Level.DEBUG, "Request archive date [string={}, currentPageNum={}]", archiveDateString, currentPageNum);
            final JSONObject result = archiveDateQueryService.getByArchiveDateString(archiveDateString);
            if (null == result) {
                LOGGER.log(Level.DEBUG, "Can not find articles for the specified archive date[string={}]", archiveDateString);
                context.sendError(404);
                return;
            }

            final JSONObject archiveDate = result.getJSONObject(ArchiveDate.ARCHIVE_DATE);
            final String archiveDateId = archiveDate.getString(Keys.OBJECT_ID);

            final JSONObject preference = optionQueryService.getPreference();
            final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

            final int articleCount = archiveDateQueryService.getArchiveDatePublishedArticleCount(archiveDateId);
            final int pageCount = (int) Math.ceil((double) articleCount / (double) pageSize);

            final List<JSONObject> articles = articleQueryService.getArticlesByArchiveDate(archiveDateId, currentPageNum, pageSize);
            if (articles.isEmpty()) {
                context.sendError(404);
                return;
            }

            dataModelService.setArticlesExProperties(context, articles, preference);

            final Map<String, Object> dataModel = renderer.getDataModel();
            Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMPLATE_DIR_NAME), dataModel);
            prepareShowArchiveArticles(preference, dataModel, articles, currentPageNum, pageCount, archiveDateString, archiveDate);
            dataModelService.fillCommon(context, dataModel, preference);
            dataModelService.fillFaviconURL(dataModel, preference);
            dataModelService.fillUsite(dataModel);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            context.sendError(404);
        }
    }

    /**
     * Shows an article with the specified context.
     *
     * @param context the specified context
     */
    public void showArticle(final RequestContext context) {
        // See PermalinkHandler#dispatchToArticleProcessor()
        final JSONObject article = (JSONObject) context.attr(Article.ARTICLE);
        if (null == article) {
            context.sendError(404);
            return;
        }

        final String articleId = article.optString(Keys.OBJECT_ID);
        LOGGER.log(Level.DEBUG, "Article [id={}]", articleId);

        final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "article.ftl");

        try {
            LOGGER.log(Level.TRACE, "Article [title={}]", article.getString(Article.ARTICLE_TITLE));
            articleQueryService.markdown(article);

            article.put(Article.ARTICLE_T_CREATE_DATE, new Date(article.optLong(Article.ARTICLE_CREATED)));
            article.put(Article.ARTICLE_T_UPDATE_DATE, new Date(article.optLong(Article.ARTICLE_UPDATED)));
            // For <meta name="description" content="${article.articleAbstract}"/>
            final String metaDescription = Jsoup.parse(article.optString(Article.ARTICLE_ABSTRACT)).text();
            article.put(Article.ARTICLE_ABSTRACT, metaDescription);
            final JSONObject preference = optionQueryService.getPreference();
            if (preference.getBoolean(Option.ID_C_ENABLE_ARTICLE_UPDATE_HINT)) {
                article.put(Common.HAS_UPDATED, articleQueryService.hasUpdated(article));
            } else {
                article.put(Common.HAS_UPDATED, false);
            }

            final JSONObject author = articleQueryService.getAuthor(article);
            final String authorName = author.getString(User.USER_NAME);
            article.put(Common.AUTHOR_NAME, authorName);
            final String authorId = author.getString(Keys.OBJECT_ID);
            article.put(Common.AUTHOR_ID, authorId);
            article.put(Common.AUTHOR_ROLE, author.getString(User.USER_ROLE));
            final String userAvatar = author.optString(UserExt.USER_AVATAR);
            article.put(Common.AUTHOR_THUMBNAIL_URL, userAvatar);
            dataModelService.fillCategory(article);
            final Map<String, Object> dataModel = renderer.getDataModel();

            prepareShowArticle(preference, dataModel, article);

            dataModelService.fillCommon(context, dataModel, preference);
            dataModelService.fillFaviconURL(dataModel, preference);
            dataModelService.fillUsite(dataModel);
            Skins.fillLangs(preference.optString(Option.ID_C_LOCALE_STRING), (String) context.attr(Keys.TEMPLATE_DIR_NAME), dataModel);

            // Fire [Before Render Article] event
            final JSONObject eventData = new JSONObject();
            eventData.put(Article.ARTICLE, article);
            eventManager.fireEventSynchronously(new Event<>(EventTypes.BEFORE_RENDER_ARTICLE, eventData));
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            context.sendError(404);
        }
    }

    /**
     * Gets the random articles.
     *
     * @param preference the specified preference
     * @return a list of articles, returns an empty list if not found
     */
    private List<JSONObject> getRandomArticles(final JSONObject preference) {
        try {
            final int displayCnt = preference.getInt(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT);
            final List<JSONObject> ret = articleQueryService.getArticlesRandomly(displayCnt);
            return ret;
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            return Collections.emptyList();
        }
    }

    /**
     * Prepares the specified data model for rendering author articles.
     *
     * @param pageNums       the specified page numbers
     * @param dataModel      the specified data model
     * @param pageCount      the specified page count
     * @param currentPageNum the specified current page number
     * @param articles       the specified articles
     * @param author         the specified author
     */
    private void prepareShowAuthorArticles(final List<Integer> pageNums,
                                           final Map<String, Object> dataModel,
                                           final int pageCount,
                                           final int currentPageNum,
                                           final List<JSONObject> articles,
                                           final JSONObject author) {
        if (0 != pageNums.size()) {
            dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
            dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
        }
        dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
        dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);

        dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum);
        final String previousPageNum = Integer.toString(currentPageNum > 1 ? currentPageNum - 1 : 0);
        dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, "0".equals(previousPageNum) ? "" : previousPageNum);
        if (pageCount == currentPageNum + 1) { // The next page is the last page
            dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, "");
        } else {
            dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, currentPageNum + 1);
        }

        dataModel.put(Article.ARTICLES, articles);
        final String authorId = author.optString(Keys.OBJECT_ID);
        dataModel.put(Common.PATH, "/authors/" + authorId);
        dataModel.put(Keys.OBJECT_ID, authorId);
        dataModel.put(Common.AUTHOR_NAME, author.optString(User.USER_NAME));
        final String userAvatar = author.optString(UserExt.USER_AVATAR);
        dataModel.put(Common.AUTHOR_THUMBNAIL_URL, userAvatar);
        dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum);
    }

    /**
     * Prepares the specified data model for rendering archive articles.
     *
     * @param preference        the specified preference
     * @param dataModel         the specified data model
     * @param articles          the specified articles
     * @param currentPageNum    the specified current page number
     * @param pageCount         the specified page count
     * @param archiveDateString the specified archive data string
     * @param archiveDate       the specified archive date
     * @return page title for caching
     */
    private String prepareShowArchiveArticles(final JSONObject preference,
                                              final Map<String, Object> dataModel,
                                              final List<JSONObject> articles,
                                              final int currentPageNum,
                                              final int pageCount,
                                              final String archiveDateString,
                                              final JSONObject archiveDate) {
        final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);
        final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE);

        final List<Integer> pageNums = Paginator.paginate(currentPageNum, pageSize, pageCount, windowSize);

        dataModel.put(Article.ARTICLES, articles);
        final String previousPageNum = Integer.toString(currentPageNum > 1 ? currentPageNum - 1 : 0);

        dataModel.put(Pagination.PAGINATION_PREVIOUS_PAGE_NUM, "0".equals(previousPageNum) ? "" : previousPageNum);
        if (pageCount == currentPageNum + 1) { // The next page is the last page
            dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, "");
        } else {
            dataModel.put(Pagination.PAGINATION_NEXT_PAGE_NUM, currentPageNum + 1);
        }
        dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, currentPageNum);
        dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
        dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
        dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
        dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
        dataModel.put(Common.PATH, "/archives/" + archiveDateString);
        dataModel.put(Keys.OBJECT_ID, archiveDate.getString(Keys.OBJECT_ID));

        final long time = archiveDate.getLong(ArchiveDate.ARCHIVE_TIME);
        final String dateString = DateFormatUtils.format(time, "yyyy/MM");
        final String[] dateStrings = dateString.split("/");
        final String year = dateStrings[0];
        final String month = dateStrings[1];

        archiveDate.put(ArchiveDate.ARCHIVE_DATE_YEAR, year);
        final String language = Locales.getLanguage(preference.getString(Option.ID_C_LOCALE_STRING));
        String ret;

        if ("en".equals(language)) {
            archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH, Dates.EN_MONTHS.get(month));
            ret = Dates.EN_MONTHS.get(month) + " " + year;
        } else {
            archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH, month);
            ret = year + " " + dataModel.get("yearLabel") + " " + month + " " + dataModel.get("monthLabel");
        }
        dataModel.put(ArchiveDate.ARCHIVE_DATE, archiveDate);
        return ret;
    }

    /**
     * Prepares the specified data model for rendering article.
     *
     * @param preference the specified preference
     * @param dataModel  the specified data model
     * @param article    the specified article
     * @throws Exception exception
     */
    private void prepareShowArticle(final JSONObject preference, final Map<String, Object> dataModel, final JSONObject article) throws Exception {
        article.put(Common.PERMALINK, article.getString(Article.ARTICLE_PERMALINK));
        dataModel.put(Article.ARTICLE, article);
        final String articleId = article.getString(Keys.OBJECT_ID);

        Stopwatchs.start("Get Article Sign");
        LOGGER.debug("Getting article sign....");
        final JSONObject sign = articleQueryService.getSign(article.getString(Article.ARTICLE_SIGN_ID), preference);
        final String articleTitle = article.optString(Article.ARTICLE_TITLE);
        final String author = article.optString(Common.AUTHOR_NAME);
        final String url = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK);
        String signHtml = sign.optString(Sign.SIGN_HTML);
        // 签名档内置模板变量 https://github.com/b3log/solo/issues/12758
        signHtml = StringUtils.replace(signHtml, "{title}", articleTitle);
        signHtml = StringUtils.replace(signHtml, "{author}", author);
        signHtml = StringUtils.replace(signHtml, "{url}", url);
        signHtml = StringUtils.replace(signHtml, "{blog}", Latkes.getServePath());
        sign.put(Sign.SIGN_HTML, signHtml);
        article.put(Common.ARTICLE_SIGN, sign);
        LOGGER.debug("Got article sign");
        Stopwatchs.end();

        Stopwatchs.start("Get Next Article");
        LOGGER.debug("Getting the next article....");
        final JSONObject nextArticle = articleQueryService.getNextArticle(articleId);

        if (null != nextArticle) {
            dataModel.put(Common.NEXT_ARTICLE_PERMALINK, nextArticle.getString(Article.ARTICLE_PERMALINK));
            dataModel.put(Common.NEXT_ARTICLE_TITLE, nextArticle.getString(Article.ARTICLE_TITLE));
            dataModel.put(Common.NEXT_ARTICLE_ABSTRACT, nextArticle.getString(Article.ARTICLE_ABSTRACT));
            LOGGER.debug("Got the next article");
        }
        Stopwatchs.end();

        Stopwatchs.start("Get Previous Article");
        LOGGER.debug("Getting the previous article....");
        final JSONObject previousArticle = articleQueryService.getPreviousArticle(articleId);
        if (null != previousArticle) {
            dataModel.put(Common.PREVIOUS_ARTICLE_PERMALINK, previousArticle.getString(Article.ARTICLE_PERMALINK));
            dataModel.put(Common.PREVIOUS_ARTICLE_TITLE, previousArticle.getString(Article.ARTICLE_TITLE));
            dataModel.put(Common.PREVIOUS_ARTICLE_ABSTRACT, previousArticle.getString(Article.ARTICLE_ABSTRACT));
            LOGGER.debug("Got the previous article");
        }
        Stopwatchs.end();

        dataModel.put(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT, preference.getInt(Option.ID_C_EXTERNAL_RELEVANT_ARTICLES_DISPLAY_CNT));
        dataModel.put(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT, preference.getInt(Option.ID_C_RANDOM_ARTICLES_DISPLAY_CNT));
        dataModel.put(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT, preference.getInt(Option.ID_C_RELEVANT_ARTICLES_DISPLAY_CNT));
    }
}


<|start_filename|>src/main/java/org/b3log/solo/service/PluginMgmtService.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.service;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.model.Plugin;
import org.b3log.latke.plugin.AbstractPlugin;
import org.b3log.latke.plugin.PluginManager;
import org.b3log.latke.plugin.PluginStatus;
import org.b3log.latke.repository.Query;
import org.b3log.latke.repository.Transaction;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.service.annotation.Service;
import org.b3log.solo.repository.PluginRepository;
import org.b3log.solo.util.StatusCodes;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.List;
import java.util.Map;

/**
 * Plugin management service.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.2, Jun 19, 2020
 * @since 0.4.0
 */
@Service
public class PluginMgmtService {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(PluginMgmtService.class);

    /**
     * Plugin repository.
     */
    @Inject
    private PluginRepository pluginRepository;

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * Initialization service.
     */
    @Inject
    private InitService initService;

    /**
     * Plugin manager.
     */
    @Inject
    private PluginManager pluginManager;

    /**
     * Updates datastore plugin descriptions with the specified plugins.
     *
     * @param plugins the specified plugins
     * @throws Exception exception
     */
    public void refresh(final List<AbstractPlugin> plugins) throws Exception {
        if (!initService.isInited()) {
            return;
        }

        final List<JSONObject> persistedPlugins = pluginRepository.getList(new Query());
        try {
            // Reads plugin status from datastore and clear plugin datastore
            for (final JSONObject oldPluginDesc : persistedPlugins) {
                final String descId = oldPluginDesc.getString(Keys.OBJECT_ID);
                final AbstractPlugin plugin = get(plugins, descId);

                pluginRepository.remove(descId);

                if (null != plugin) {
                    final String status = oldPluginDesc.getString(Plugin.PLUGIN_STATUS);
                    final String setting = oldPluginDesc.optString(Plugin.PLUGIN_SETTING);

                    plugin.setStatus(PluginStatus.valueOf(status));
                    try {
                        if (StringUtils.isNotBlank(setting)) {
                            plugin.setSetting(new JSONObject(setting));
                        }
                    } catch (final JSONException e) {
                        LOGGER.log(Level.WARN, "the formatter of the old config failed to convert to json", e);
                    }
                }
            }

            // Adds these plugins into datastore
            for (final AbstractPlugin plugin : plugins) {
                final JSONObject pluginDesc = plugin.toJSONObject();

                pluginRepository.add(pluginDesc);

                LOGGER.log(Level.TRACE, "Refreshed plugin[{}]", pluginDesc);
            }

        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Refresh plugins failed", e);
        }
    }

    /**
     * Gets a plugin in the specified plugins with the specified id.
     *
     * @param plugins the specified plugins
     * @param id      the specified id, must NOT be {@code null}
     * @return a plugin, returns {@code null} if not found
     */
    private AbstractPlugin get(final List<AbstractPlugin> plugins, final String id) {
        if (null == id) {
            throw new IllegalArgumentException("id must not be null");
        }

        for (final AbstractPlugin plugin : plugins) {
            if (id.equals(plugin.getId())) {
                return plugin;
            }
        }

        return null;
    }

    /**
     * Sets a plugin's status with the specified plugin id, status.
     *
     * @param pluginId the specified plugin id
     * @param status   the specified status, see {@link PluginStatus}
     * @return for example,
     * <pre>
     * {
     *     "code": int,
     *     "msg": ""
     * }
     * </pre>
     */
    public JSONObject setPluginStatus(final String pluginId, final String status) {
        final Map<String, String> langs = langPropsService.getAll(Latkes.getLocale());
        final List<AbstractPlugin> plugins = pluginManager.getPlugins();
        final JSONObject ret = new JSONObject();
        for (final AbstractPlugin plugin : plugins) {
            if (plugin.getId().equals(pluginId)) {
                final Transaction transaction = pluginRepository.beginTransaction();
                try {
                    plugin.setStatus(PluginStatus.valueOf(status));
                    pluginRepository.update(pluginId, plugin.toJSONObject());
                    transaction.commit();
                    plugin.changeStatus();
                    ret.put(Keys.CODE, StatusCodes.SUCC);
                    ret.put(Keys.MSG, langs.get("setSuccLabel"));
                    return ret;
                } catch (final Exception e) {
                    if (transaction.isActive()) {
                        transaction.rollback();
                    }

                    LOGGER.log(Level.ERROR, "Set plugin status error", e);
                    ret.put(Keys.CODE, StatusCodes.ERR);
                    ret.put(Keys.MSG, langs.get("setFailLabel"));
                    return ret;
                }
            }
        }

        ret.put(Keys.CODE, StatusCodes.ERR);
        ret.put(Keys.MSG, langs.get("refreshAndRetryLabel"));
        return ret;
    }

    /**
     * updatePluginSetting.
     *
     * @param pluginId the specified pluginoId
     * @param setting  the specified setting
     * @return the ret json
     */
    public JSONObject updatePluginSetting(final String pluginId, final String setting) {
        final JSONObject ret = new JSONObject();
        final Map<String, String> langs = langPropsService.getAll(Latkes.getLocale());
        final List<AbstractPlugin> plugins = pluginManager.getPlugins();
        for (final AbstractPlugin plugin : plugins) {
            if (plugin.getId().equals(pluginId)) {
                final Transaction transaction = pluginRepository.beginTransaction();

                try {
                    final JSONObject pluginJson = plugin.toJSONObject();
                    pluginJson.put(Plugin.PLUGIN_SETTING, setting);
                    pluginRepository.update(pluginId, pluginJson);
                    transaction.commit();
                    ret.put(Keys.CODE, StatusCodes.SUCC);
                    ret.put(Keys.MSG, langs.get("setSuccLabel"));
                    return ret;
                } catch (final Exception e) {
                    if (transaction.isActive()) {
                        transaction.rollback();
                    }
                    LOGGER.log(Level.ERROR, "Set plugin status error", e);
                    ret.put(Keys.CODE, StatusCodes.ERR);
                    ret.put(Keys.MSG, langs.get("setFailLabel"));
                    return ret;
                }
            }
        }

        ret.put(Keys.CODE, StatusCodes.ERR);
        ret.put(Keys.MSG, langs.get("refreshAndRetryLabel"));
        return ret;

    }
}


<|start_filename|>src/test/java/org/b3log/solo/processor/ErrorProcessorTestCase.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.lang3.StringUtils;
import org.b3log.solo.AbstractTestCase;
import org.b3log.solo.MockRequest;
import org.b3log.solo.MockResponse;
import org.testng.Assert;
import org.testng.annotations.Test;

/**
 * {@link ErrorProcessor} test case.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.1.4, Apr 30, 2020
 * @since 1.7.0
 */
@Test(suiteName = "processor")
public class ErrorProcessorTestCase extends AbstractTestCase {

    /**
     * showErrorPage.
     */
    public void showErrorPage() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/notfound");
        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "<title>404 Not Found! - Solo 的个人博客</title>"));
    }
}


<|start_filename|>src/main/java/org/b3log/solo/plugin/ToCPlugin.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.plugin;

import org.apache.commons.lang3.StringUtils;
import org.b3log.latke.event.AbstractEventListener;
import org.b3log.latke.event.Event;
import org.b3log.latke.event.EventManager;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.ioc.BeanManager;
import org.b3log.latke.plugin.NotInteractivePlugin;
import org.b3log.latke.plugin.PluginStatus;
import org.b3log.solo.event.EventTypes;
import org.b3log.solo.model.Article;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * ToC event handler.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 2.0.1.0, Jan 24, 2020
 * @since 0.6.7
 */
public class ToCPlugin extends NotInteractivePlugin {

    private final ToCEventHandler handler = new ToCEventHandler();

    @Override
    public void changeStatus() {
        super.changeStatus();

        final EventManager eventManager = BeanManager.getInstance().getReference(EventManager.class);
        final PluginStatus status = getStatus();
        if (PluginStatus.DISABLED == status) {
            eventManager.unregisterListener(handler);
        } else {
            eventManager.registerListener(handler);
        }
    }

    @Override
    public void prePlug(RequestContext context) {
    }

    @Override
    public void postPlug(Map<String, Object> dataModel, RequestContext context) {
    }
}

/**
 * ToC event handler.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @author <a href="http://www.annpeter.cn"><NAME></a>
 * @author <a href="http://vanessa.b3log.org">Vanessa</a>
 * @version 2.0.0.3, Mar 8, 2020
 * @since 0.6.7
 */
class ToCEventHandler extends AbstractEventListener<JSONObject> {

    @Override
    public String getEventType() {
        return EventTypes.BEFORE_RENDER_ARTICLE;
    }

    @Override
    public void action(final Event<JSONObject> event) {
        final JSONObject data = event.getData();
        final JSONObject article = data.optJSONObject(Article.ARTICLE);
        final String content = article.optString(Article.ARTICLE_CONTENT);
        final Document doc = Jsoup.parse(content, StringUtils.EMPTY, Parser.htmlParser());
        doc.outputSettings().prettyPrint(false);

        final List<JSONObject> toc = new ArrayList<>();
        final Elements hs = doc.select("body>h1, body>h2, body>h3, body>h4, body>h5, body>h6");
        for (int i = 0; i < hs.size(); i++) {
            final Element element = hs.get(i);
            final String tagName = element.tagName().toLowerCase();
            final String text = element.text();
            String id = element.attr("id");
            if (StringUtils.isBlank(id)) {
                id = "toc_" + tagName + "_" + i;
            } else if (StringUtils.startsWith(id, "#")) {
                id = StringUtils.substringAfter(id, "#");
            }
            element.attr("id", id);
            final JSONObject li = new JSONObject().
                    put("className", "toc__" + tagName).
                    put("id", id).
                    put("text", text);
            toc.add(li);
        }
        final Element body = doc.getElementsByTag("body").get(0);
        article.put(Article.ARTICLE_CONTENT, body.html());
        article.put(Article.ARTICLE_T_TOC, (Object) toc);
    }
}


<|start_filename|>src/main/java/org/b3log/solo/service/PageMgmtService.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.service;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.repository.RepositoryException;
import org.b3log.latke.repository.Transaction;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.service.ServiceException;
import org.b3log.latke.service.annotation.Service;
import org.b3log.solo.model.Page;
import org.b3log.solo.repository.PageRepository;
import org.b3log.solo.util.Statics;
import org.json.JSONObject;

/**
 * Page management service.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @author <a href="http://vanessa.b3log.org">Vanessa</a>
 * @version 1.1.2.1, Jul 8, 2020
 * @since 0.4.0
 */
@Service
public class PageMgmtService {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(PageMgmtService.class);

    /**
     * Page repository.
     */
    @Inject
    private PageRepository pageRepository;

    /**
     * User query service.
     */
    @Inject
    private UserQueryService userQueryService;

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * Permalink query service.
     */
    @Inject
    private PermalinkQueryService permalinkQueryService;

    /**
     * Statistic management service.
     */
    @Inject
    private StatisticMgmtService statisticMgmtService;

    /**
     * Statistic query service.
     */
    @Inject
    private StatisticQueryService statisticQueryService;

    /**
     * Option query service.
     */
    @Inject
    private OptionQueryService optionQueryService;

    /**
     * Option management service.
     */
    @Inject
    private OptionMgmtService optionMgmtService;

    /**
     * Updates a page by the specified request json object.
     *
     * @param requestJSONObject the specified request json object, for example,
     *                          {
     *                          "page": {
     *                          "oId": "",
     *                          "pageTitle": "",
     *                          "pageOrder": int,
     *                          "pagePermalink": "",
     *                          "pageOpenTarget": "",
     *                          "pageIcon": "" // optional
     *                          }
     *                          }, see {@link Page} for more details
     * @throws ServiceException service exception
     */
    public void updatePage(final JSONObject requestJSONObject) throws ServiceException {
        final Transaction transaction = pageRepository.beginTransaction();
        try {
            final JSONObject page = requestJSONObject.getJSONObject(Page.PAGE);
            final String pageId = page.getString(Keys.OBJECT_ID);
            final JSONObject oldPage = pageRepository.get(pageId);
            final JSONObject newPage = new JSONObject(page, JSONObject.getNames(page));
            newPage.put(Page.PAGE_ORDER, oldPage.getInt(Page.PAGE_ORDER));
            String permalink = page.optString(Page.PAGE_PERMALINK).trim();
            permalink = StringUtils.replace(permalink, " ", "-");
            newPage.put(Page.PAGE_PERMALINK, permalink);
            page.put(Page.PAGE_ICON, page.optString(Page.PAGE_ICON));

            pageRepository.update(pageId, newPage);
            transaction.commit();

            Statics.clear();

            LOGGER.log(Level.DEBUG, "Updated a page[id={}]", pageId);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            if (transaction.isActive()) {
                transaction.rollback();
            }

            throw new ServiceException(e);
        }
    }

    /**
     * Removes a page specified by the given page id.
     *
     * @param pageId the given page id
     * @throws ServiceException service exception
     */
    public void removePage(final String pageId) throws ServiceException {
        final Transaction transaction = pageRepository.beginTransaction();
        try {
            pageRepository.remove(pageId);
            transaction.commit();

            Statics.clear();
        } catch (final Exception e) {
            if (transaction.isActive()) {
                transaction.rollback();
            }

            LOGGER.log(Level.ERROR, "Removes a page[id=" + pageId + "] failed", e);
            throw new ServiceException(e);
        }
    }

    /**
     * Adds a page with the specified request json object.
     *
     * @param requestJSONObject the specified request json object, for example,
     *                          {
     *                          "page": {
     *                          "pageTitle": "",
     *                          "pageOpenTarget": "",
     *                          "pagePermalink": "",
     *                          "pageIcon": "" // optional
     *                          }
     *                          }, see {@link Page} for more details
     * @return generated page id
     * @throws ServiceException if permalink format checks failed or persists failed
     */
    public String addPage(final JSONObject requestJSONObject) throws ServiceException {
        final Transaction transaction = pageRepository.beginTransaction();

        try {
            final JSONObject page = requestJSONObject.getJSONObject(Page.PAGE);
            final int maxOrder = pageRepository.getMaxOrder();
            page.put(Page.PAGE_ORDER, maxOrder + 1);

            String permalink = page.optString(Page.PAGE_PERMALINK);
            permalink = StringUtils.replace(permalink, " ", "-");
            page.put(Page.PAGE_PERMALINK, permalink);
            page.put(Page.PAGE_ICON, page.optString(Page.PAGE_ICON));
            final String ret = pageRepository.add(page);
            transaction.commit();

            Statics.clear();

            return ret;
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, e.getMessage(), e);
            if (transaction.isActive()) {
                transaction.rollback();
            }
            throw new ServiceException(e);
        }
    }

    /**
     * Changes the order of a page specified by the given page id with the specified direction.
     *
     * @param pageId    the given page id
     * @param direction the specified direction, "up"/"down"
     * @throws ServiceException service exception
     */
    public void changeOrder(final String pageId, final String direction) throws ServiceException {
        final Transaction transaction = pageRepository.beginTransaction();
        try {
            final JSONObject srcPage = pageRepository.get(pageId);
            final int srcPageOrder = srcPage.getInt(Page.PAGE_ORDER);

            JSONObject targetPage;
            if ("up".equals(direction)) {
                targetPage = pageRepository.getUpper(pageId);
            } else { // Down
                targetPage = pageRepository.getUnder(pageId);
            }

            if (null == targetPage) {
                if (transaction.isActive()) {
                    transaction.rollback();
                }

                LOGGER.log(Level.WARN, "Cant not find the target page of source page[order={}]", srcPageOrder);
                return;
            }

            // Swaps
            srcPage.put(Page.PAGE_ORDER, targetPage.getInt(Page.PAGE_ORDER));
            targetPage.put(Page.PAGE_ORDER, srcPageOrder);
            pageRepository.update(srcPage.getString(Keys.OBJECT_ID), srcPage, Page.PAGE_ORDER);
            pageRepository.update(targetPage.getString(Keys.OBJECT_ID), targetPage, Page.PAGE_ORDER);
            transaction.commit();

            Statics.clear();
        } catch (final Exception e) {
            if (transaction.isActive()) {
                transaction.rollback();
            }

            LOGGER.log(Level.ERROR, "Changes page's order failed", e);
            throw new ServiceException(e);
        }
    }
}


<|start_filename|>src/main/java/org/b3log/solo/model/UserExt.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.model;

import org.apache.commons.lang3.StringUtils;

/**
 * This class defines ext of user model relevant keys.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.3.0.0, Feb 8, 2019
 * @see org.b3log.latke.model.User
 * @since 0.4.1
 */
public final class UserExt {

    /**
     * Key of user avatar.
     */
    public static final String USER_AVATAR = "userAvatar";

    /**
     * Max user name length.
     */
    public static final int MAX_USER_NAME_LENGTH = 64;

    /**
     * Min user name length.
     */
    public static final int MIN_USER_NAME_LENGTH = 1;

    /**
     * Key of user B3 key.
     */
    public static final String USER_B3_KEY = "userB3Key";

    /**
     * Key of GitHub open id.
     */
    public static final String USER_GITHUB_ID = "userGitHubId";

    /**
     * Checks whether the specified name is invalid.
     * <p>
     * A valid user name:
     * <ul>
     * <li>length [1, 64]</li>
     * <li>content {a-z, A-Z, 0-9, -}</li>
     * <li>Not contains "admin"/"Admin"</li>
     * </ul>
     * </p>
     *
     * @param name the specified name
     * @return {@code true} if it is invalid, returns {@code false} otherwise
     */
    public static boolean invalidUserName(final String name) {
        final int length = name.length();
        if (length < MIN_USER_NAME_LENGTH || length > MAX_USER_NAME_LENGTH) {
            return true;
        }

        char c;
        for (int i = 0; i < length; i++) {
            c = name.charAt(i);
            if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || '-' == c) {
                continue;
            }

            return true;
        }

        return StringUtils.containsIgnoreCase(name, "admin");
    }

    /**
     * Private constructor.
     */
    private UserExt() {
    }
}


<|start_filename|>src/test/java/org/b3log/solo/processor/console/ArticleConsoleTestCase.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor.console;

import org.apache.commons.lang3.StringUtils;
import org.b3log.latke.Keys;
import org.b3log.latke.repository.Query;
import org.b3log.solo.AbstractTestCase;
import org.b3log.solo.MockRequest;
import org.b3log.solo.MockResponse;
import org.b3log.solo.model.Article;
import org.json.JSONObject;
import org.testng.Assert;
import org.testng.annotations.Test;

/**
 * {@link ArticleConsole} test case.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.1, Feb 6, 2019
 * @since 2.9.7
 */
@Test(suiteName = "processor")
public class ArticleConsoleTestCase extends AbstractTestCase {

    /**
     * getArticleThumbs.
     */
    public void getArticleThumbs() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/thumbs");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * markdown2HTML.
     *
     * @throws Exception exception
     */
    public void markdown2HTML() throws Exception {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/markdown/2html");
        request.setMethod("POST");
        final JSONObject requestJSON = new JSONObject();
        requestJSON.put("markdownText", "**Solo**");
        request.setJSON(requestJSON);

        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "<p><strong>Solo<\\/strong><\\/p>"));
    }

    /**
     * getArticle.
     *
     * @throws Exception exception
     */
    public void getArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());
        final String articleId = article.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/" + articleId);
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * getArticles.
     *
     * @throws Exception exception
     */
    public void getArticles() throws Exception {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/articles/status/published/1/10/20");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * removeArticle.
     *
     * @throws Exception exception
     */
    @Test(dependsOnMethods = "updateArticle")
    public void removeArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());
        final String articleId = article.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/" + articleId);
        request.setMethod("DELETE");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * cancelPublishArticle.
     *
     * @throws Exception exception
     */
    public void cancelPublishArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());
        final String articleId = article.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/unpublish/" + articleId);
        request.setMethod("PUT");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * cancelTopArticle.
     *
     * @throws Exception exception
     */
    public void cancelTopArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());
        final String articleId = article.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/canceltop/" + articleId);
        request.setMethod("PUT");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * putTopArticle.
     *
     * @throws Exception exception
     */
    public void putTopArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());
        final String articleId = article.optString(Keys.OBJECT_ID);

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/puttop/" + articleId);
        request.setMethod("PUT");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * updateArticle.
     *
     * @throws Exception exception
     */
    public void updateArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/");
        request.setMethod("PUT");
        final JSONObject requestJSON = new JSONObject();
        requestJSON.put(Article.ARTICLE, article);
        request.setJSON(requestJSON);

        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }

    /**
     * addArticle.
     *
     * @throws Exception exception
     */
    public void addArticle() throws Exception {
        final JSONObject article = getArticleRepository().getFirst(new Query());
        article.put(Keys.OBJECT_ID, "");
        article.put(Article.ARTICLE_PERMALINK, "");

        final MockRequest request = mockRequest();
        request.setRequestURI("/console/article/");
        request.setMethod("POST");
        final JSONObject requestJSON = new JSONObject();
        requestJSON.put(Article.ARTICLE, article);
        request.setJSON(requestJSON);

        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "\"code\":0"));
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/FetchUploadProcessor.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.net.MimeTypes;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.util.Strings;
import org.b3log.solo.model.Common;
import org.b3log.solo.util.Images;
import org.b3log.solo.util.Solos;
import org.json.JSONObject;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 * File fetch upload processor. 第三方图床自动替换为社区图床 https://github.com/88250/solo/issues/114
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.0, Apr 30, 2020
 * @since 4.1.0
 */
@Singleton
public class FetchUploadProcessor {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(FetchUploadProcessor.class);

    /**
     * Fetches the remote file and upload it.
     *
     * @param context the specified context
     */
    public void fetchUpload(final RequestContext context) {
        final JSONObject result = Solos.newFail();
        context.renderJSONPretty(result);
        final JSONObject data = new JSONObject();

        final JSONObject requestJSONObject = context.requestJSON();
        final String originalURL = requestJSONObject.optString(Common.URL);
        if (!Strings.isURL(originalURL) || !StringUtils.startsWithIgnoreCase(originalURL, "http")) {
            return;
        }

        if (Images.uploaded(originalURL)) {
            return;
        }

        final JSONObject upload = Solos.getUploadToken(context);
        if (null == upload) {
            final String msg = "Gets upload token failed";
            LOGGER.log(Level.ERROR, msg);
            result.put(Keys.MSG, msg);
            return;
        }

        String url;
        byte[] bytes;
        String contentType;
        try {
            final HttpRequest req = HttpRequest.get(originalURL).header("User-Agent", Solos.USER_AGENT);
            final HttpResponse res = req.connectionTimeout(3000).timeout(5000).send();
            res.close();
            if (200 != res.statusCode()) {
                result.put(Keys.MSG, "Fetch upload return status code is [" + res.statusCode() + "]");
                return;
            }

            bytes = res.bodyBytes();
            contentType = res.contentType();
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Fetch file [url=" + originalURL + "] failed", e);
            result.put(Keys.MSG, "Fetch file [url=" + originalURL + "] failed");
            return;
        }

        final String uploadURL = upload.optString(Common.UPLOAD_URL);
        final String uploadToken = upload.optString(Common.UPLOAD_TOKEN);

        try {
            final String suffix = "." + getSuffix(contentType);
            final Path imgFilePath = Files.createTempFile("solo-fetchupload-", suffix);
            final File file = imgFilePath.toFile();
            FileUtils.writeByteArrayToFile(file, bytes);
            final HttpRequest req = HttpRequest.post(uploadURL).
                    header("User-Agent", Solos.USER_AGENT).header("X-Upload-Token", uploadToken).
                    connectionTimeout(3000).timeout(5000).form(
                    "file[]", file);
            final HttpResponse res = req.send();
            res.close();
            if (200 != res.statusCode()) {
                result.put(Keys.MSG, "Upload file to community OSS return status code is [" + res.statusCode() + "]");
                return;
            }
            res.charset("UTF-8");
            final JSONObject uploadResult = new JSONObject(res.bodyText());
            final JSONObject succMap = uploadResult.optJSONObject("data").optJSONObject("succMap");
            final String key = succMap.keys().next();
            url = succMap.optString(key);
        } catch (final Exception e) {
            final String msg = "Upload file to community OSS [url=" + originalURL + "] failed";
            LOGGER.log(Level.ERROR, msg, e);
            result.put(Keys.MSG, msg);
            return;
        }


        data.put(Common.URL, url);
        data.put("originalURL", originalURL);

        result.put(Common.DATA, data);
        result.put(Keys.CODE, 0);
        result.put(Keys.MSG, "");
    }

    /**
     * Gets suffix (for example jpg) with the specified content type.
     *
     * @param contentType the specified content type
     * @return suffix
     */
    public static String getSuffix(final String contentType) {
        String ret;
        final String[] exts = MimeTypes.findExtensionsByMimeTypes(contentType, false);
        if (null != exts && 0 < exts.length) {
            ret = exts[0];
        } else {
            ret = StringUtils.substringAfter(contentType, "/");
            ret = StringUtils.substringBefore(ret, ";");
        }

        return ret;
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/console/StaticSiteConsole.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor.console;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.ioc.BeanManager;
import org.b3log.latke.ioc.Inject;
import org.b3log.latke.ioc.Singleton;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.util.Strings;
import org.b3log.solo.model.*;
import org.b3log.solo.service.*;
import org.b3log.solo.util.Mocks;
import org.b3log.solo.util.Solos;
import org.b3log.solo.util.StatusCodes;
import org.json.JSONObject;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * Static site console request processing. HTML 静态站点生成 https://github.com/88250/solo/issues/19
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 2.0.1.2, Mar 29, 2020
 * @since 3.9.0
 */
@Singleton
public class StaticSiteConsole {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(StaticSiteConsole.class);

    /**
     * Language service.
     */
    @Inject
    private LangPropsService langPropsService;

    /**
     * Mock request scheme.
     */
    private static String scheme;

    /**
     * Mock request host.
     */
    private static String host;

    /**
     * Generates static site.
     *
     * @param context the specified request context
     */
    public synchronized void genSite(final RequestContext context) {
        try {
            final JSONObject requestJSONObject = context.requestJSON();
            final String url = requestJSONObject.optString(Common.URL);
            if (!Strings.isURL(url)) {
                context.renderJSON(StatusCodes.ERR);
                context.renderMsg("Invalid site URL");
                return;
            }

            if (Latkes.isInJar()) {
                context.renderJSON(StatusCodes.ERR);
                context.renderMsg("Do not support this feature while running in Jar");
                return;
            }

            FileUtils.deleteDirectory(new File(staticSitePath));
            FileUtils.forceMkdir(new File(staticSitePath));

            final URL u = new URL(url);
            scheme = u.getProtocol();
            host = u.getHost();
            if (-1 != u.getPort()) {
                host += ":" + u.getPort();
            }
            Solos.GEN_STATIC_SITE = true;

            genURI("/tags.html");
            genURI("/archives.html");
            genURI("/links.html");
            genURI("/categories.html");
            genURI("/index.html");
            genURI("/blog/info");
            genURI("/manifest.json");
            genURI("/rss.xml");
            genURI("/articles/random.json");

            genArticles();
            genTags();
            genArchives();
            genCategories();
            genSkins();
            genJS();
            genImages();
            genPlugins();
            genFile("sw.js");
            genFile("robots.txt");
            genFile("CHANGE_LOGS.md");

            Solos.GEN_STATIC_SITE = false;

            LOGGER.log(Level.INFO, "Static site generated [dir=" + staticSitePath + "]");

            String siteGenedLabel = langPropsService.get("siteGenedLabel");
            siteGenedLabel = siteGenedLabel.replace("{dir}", staticSitePath);
            context.renderJSON(StatusCodes.SUCC);
            context.renderMsg(siteGenedLabel);
        } catch (final Exception e) {
            LOGGER.log(Level.ERROR, "Generates static site failed", e);

            context.renderJSON(StatusCodes.ERR);
            context.renderMsg(langPropsService.get("updateFailLabel"));
        }
    }

    /**
     * Source root directory path.
     */
    private static String rootPath;

    /**
     * Path of generate directory.
     */
    private static String staticSitePath;


    static {
        if (!Latkes.isInJar()) {
            rootPath = StaticSiteConsole.class.getResource("/repository.json").getPath();
            rootPath = StringUtils.substringBeforeLast(rootPath, "/repository.json");
            staticSitePath = rootPath + "/static-site";
        } else {
            LOGGER.log(Level.INFO, "Do not support export static site when running in jar");
        }
    }

    private static void genCategories() {
        final BeanManager beanManager = BeanManager.getInstance();
        final CategoryQueryService categoryQueryService = beanManager.getReference(CategoryQueryService.class);
        final List<JSONObject> categories = categoryQueryService.getMostTagCategory(Integer.MAX_VALUE);
        final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
        final JSONObject preference = optionQueryService.getPreference();
        final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

        categories.parallelStream().forEach(category -> {
            final String categoryURI = category.optString(Category.CATEGORY_URI);
            try {
                final int articleCount = categoryQueryService.getPublishedArticleCount(category.optString(Keys.OBJECT_ID));
                final int pageCount = (int) Math.ceil((double) articleCount / pageSize);
                int count = 0;
                while (count++ < pageCount) {
                    genPage("/category/" + categoryURI);
                    genPage("/category/" + categoryURI + "?p=" + count);
                }
            } catch (final Exception e) {
                LOGGER.log(Level.ERROR, "Generates a category [uri=" + categoryURI + "] failed", e);
            }
        });
    }

    private static void genArchives() throws Exception {
        final BeanManager beanManager = BeanManager.getInstance();
        final ArchiveDateQueryService archiveDateQueryService = beanManager.getReference(ArchiveDateQueryService.class);
        final List<JSONObject> archiveDates = archiveDateQueryService.getArchiveDates();
        final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
        final JSONObject preference = optionQueryService.getPreference();
        final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

        archiveDates.parallelStream().forEach(archiveDate -> {
            final long time = archiveDate.getLong(ArchiveDate.ARCHIVE_TIME);
            final String dateString = DateFormatUtils.format(time, "yyyy/MM");
            try {
                final int articleCount = archiveDateQueryService.getArchiveDatePublishedArticleCount(archiveDate.optString(Keys.OBJECT_ID));
                final int pageCount = (int) Math.ceil((double) articleCount / pageSize);
                int count = 0;
                while (count++ < pageCount) {
                    genPage("/archives/" + dateString);
                    genPage("/archives/" + dateString + "?p=" + count);
                }
            } catch (final Exception e) {
                LOGGER.log(Level.ERROR, "Generates an archive [date=" + archiveDate + "] failed", e);
            }
        });
    }

    private static void genTags() throws Exception {
        final BeanManager beanManager = BeanManager.getInstance();
        final TagQueryService tagQueryService = beanManager.getReference(TagQueryService.class);
        final List<JSONObject> tags = tagQueryService.getTags();
        final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
        final JSONObject preference = optionQueryService.getPreference();
        final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);

        tags.parallelStream().forEach(tag -> {
            final String tagTitle = tag.optString(Tag.TAG_TITLE);
            try {
                final int articleCount = tagQueryService.getArticleCount(tag.optString(Keys.OBJECT_ID));
                final int pageCount = (int) Math.ceil((double) articleCount / pageSize);
                int count = 0;
                while (count++ < pageCount) {
                    genPage("/tags/" + tagTitle);
                    genPage("/tags/" + tagTitle + "?p=" + count);
                }
            } catch (final Exception e) {
                LOGGER.log(Level.ERROR, "Generates a tag [title=" + tagTitle + "] failed", e);
            }
        });
    }

    private static void genArticles() throws Exception {
        final BeanManager beanManager = BeanManager.getInstance();
        final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class);
        final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
        final JSONObject preference = optionQueryService.getPreference();
        final int pageSize = preference.getInt(Option.ID_C_ARTICLE_LIST_DISPLAY_COUNT);
        final int windowSize = preference.getInt(Option.ID_C_ARTICLE_LIST_PAGINATION_WINDOW_SIZE);
        int currentPageNum = 1;
        int count = 0;
        final int maxPage = 10240;
        while (count++ < maxPage) {
            final JSONObject requestJSONObject = Solos.buildPaginationRequest(String.valueOf(currentPageNum) + '/' + pageSize + '/' + windowSize);
            final JSONObject result = articleQueryService.getArticles(requestJSONObject);
            final List<JSONObject> articles = (List<JSONObject>) result.opt(Article.ARTICLES);
            if (articles.isEmpty()) {
                break;
            }

            genPage("?p=" + currentPageNum);

            articles.parallelStream().forEach(article -> {
                final String permalink = article.optString(Article.ARTICLE_PERMALINK);
                try {
                    genArticle(permalink);
                    genURI("/article/relevant/" + article.optString(Keys.OBJECT_ID) + ".json");
                } catch (final Exception e) {
                    LOGGER.log(Level.ERROR, "Generates an article [uri=" + permalink + "] failed", e);
                }
            });

            currentPageNum++;
        }
    }

    private static void genPage(final String uri) throws Exception {
        String filePath = uri;
        filePath = StringUtils.replace(filePath, "?", "/");
        filePath = StringUtils.replace(filePath, "=", "/");
        FileUtils.forceMkdir(new File(staticSitePath + filePath));
        final OutputStream outputStream = new FileOutputStream(staticSitePath + filePath + "/index.html");
        String html = Mocks.mockRequest(uri, scheme, host);
        IOUtils.write(html, outputStream, StandardCharsets.UTF_8);
        outputStream.close();
        LOGGER.log(Level.INFO, "Generated a page [" + uri + "]");
    }

    private static void genURI(final String uri) throws Exception {
        FileUtils.forceMkdirParent(new File(staticSitePath + uri));
        final OutputStream outputStream = new FileOutputStream(staticSitePath + uri);
        String html = Mocks.mockRequest(uri, scheme, host);
        IOUtils.write(html, outputStream, StandardCharsets.UTF_8);
        outputStream.close();
        LOGGER.log(Level.INFO, "Generated a file [" + uri + "]");
    }

    private static void genArticle(final String permalink) throws Exception {
        if (!StringUtils.endsWithIgnoreCase(permalink, ".html") && !StringUtils.endsWithIgnoreCase(permalink, ".htm")) {
            FileUtils.forceMkdir(new File(staticSitePath + permalink));
            final String html = Mocks.mockRequest(permalink, scheme, host);
            final OutputStream outputStream = new FileOutputStream(staticSitePath + permalink + "/index.html");
            IOUtils.write(html, outputStream, StandardCharsets.UTF_8);
            outputStream.close();
        } else {
            FileUtils.forceMkdirParent(new File(staticSitePath + permalink));
            final String html = Mocks.mockRequest(permalink, scheme, host);
            final OutputStream outputStream = new FileOutputStream(staticSitePath + permalink);
            IOUtils.write(html, outputStream, StandardCharsets.UTF_8);
            outputStream.close();
        }
        LOGGER.log(Level.INFO, "Generated an article [" + permalink + "]");
    }

    private static void genSkins() throws Exception {
        FileUtils.deleteDirectory(new File(staticSitePath + "/skins"));
        FileUtils.forceMkdir(new File(staticSitePath + "/skins"));
        FileUtils.copyDirectory(new File(rootPath + "/skins"), new File(staticSitePath + "/skins"));
        LOGGER.log(Level.INFO, "Generated skins");
    }

    private static void genJS() throws Exception {
        FileUtils.deleteDirectory(new File(staticSitePath + "/js"));
        FileUtils.forceMkdir(new File(staticSitePath + "/js"));
        FileUtils.copyDirectory(new File(rootPath + "/js"), new File(staticSitePath + "/js"));
        LOGGER.log(Level.INFO, "Generated js");
    }

    private static void genImages() throws Exception {
        FileUtils.deleteDirectory(new File(staticSitePath + "/images"));
        FileUtils.forceMkdir(new File(staticSitePath + "/images"));
        FileUtils.copyDirectory(new File(rootPath + "/images"), new File(staticSitePath + "/images"));
        LOGGER.log(Level.INFO, "Generated images");
    }

    private static void genPlugins() throws Exception {
        FileUtils.deleteDirectory(new File(staticSitePath + "/plugins"));
        FileUtils.forceMkdir(new File(staticSitePath + "/plugins"));
        FileUtils.copyDirectory(new File(rootPath + "/plugins"), new File(staticSitePath + "/plugins"));
        genURI("/plugins/kanbanniang/assets/model.json");
        LOGGER.log(Level.INFO, "Generated plugins");

    }

    private static void genFile(final String file) throws Exception {
        FileUtils.forceMkdirParent(new File(staticSitePath + "/" + file));
        FileUtils.copyFile(new File(rootPath + "/" + file), new File(staticSitePath + "/" + file));
        LOGGER.log(Level.INFO, "Generated a file [" + file + "]");
    }

}


<|start_filename|>src/main/java/org/b3log/solo/processor/InitCheckHandler.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.http.function.Handler;
import org.b3log.latke.ioc.BeanManager;
import org.b3log.solo.service.InitService;

/**
 * Checks initialization handler.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.2, Dec 14, 2019
 * @since 3.2.0
 */
public class InitCheckHandler implements Handler {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(InitCheckHandler.class);

    /**
     * Whether initialization info reported.
     */
    private static boolean initReported;

    @Override
    public void handle(final RequestContext context) {
        final String requestURI = context.requestURI();
        final boolean isSpiderBot = (boolean) context.attr(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT);
        LOGGER.log(Level.TRACE, "Request [URI={}]", requestURI);

        // 禁止直接获取 robots.txt https://github.com/b3log/solo/issues/12543
        if (requestURI.startsWith("/robots.txt") && !isSpiderBot) {
            context.sendError(403);
            return;
        }

        final BeanManager beanManager = BeanManager.getInstance();
        final InitService initService = beanManager.getReference(InitService.class);
        if (initService.isInited()) {
            context.handle();
            return;
        }

        if (StringUtils.startsWith(requestURI, Latkes.getContextPath() + "/login/")) {
            // Do initialization
            context.handle();
            return;
        }

        if (!initReported) {
            LOGGER.log(Level.DEBUG, "Solo has not been initialized, so redirects to /start");
            initReported = true;
        }

        context.attr(Keys.HttpRequest.REQUEST_URI, Latkes.getContextPath() + "/start");
        context.handle();
    }
}


<|start_filename|>src/main/java/org/b3log/solo/processor/PermalinkHandler.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.b3log.latke.Keys;
import org.b3log.latke.Latkes;
import org.b3log.latke.http.HttpMethod;
import org.b3log.latke.http.RequestContext;
import org.b3log.latke.http.function.Handler;
import org.b3log.latke.ioc.BeanManager;
import org.b3log.latke.repository.RepositoryException;
import org.b3log.solo.model.Article;
import org.b3log.solo.model.Option;
import org.b3log.solo.repository.ArticleRepository;
import org.b3log.solo.service.InitService;
import org.b3log.solo.service.OptionQueryService;
import org.b3log.solo.service.PermalinkQueryService;
import org.b3log.solo.util.Solos;
import org.b3log.solo.util.Statics;
import org.json.JSONObject;

/**
 * Article permalink  handler.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.5, May 23, 2020
 * @since 3.2.0
 */
public class PermalinkHandler implements Handler {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LogManager.getLogger(PermalinkHandler.class);

    /**
     * Whether initialization info reported.
     */
    private static boolean initReported;

    @Override
    public void handle(final RequestContext context) {
        final BeanManager beanManager = BeanManager.getInstance();

        JSONObject article;
        try {
            final InitService initService = beanManager.getReference(InitService.class);
            if (!initService.isInited()) {
                context.handle();
                return;
            }

            final String requestURI = context.requestURI();
            final String contextPath = Latkes.getContextPath();
            final String permalink = StringUtils.substringAfter(requestURI, contextPath);
            if (PermalinkQueryService.invalidPermalinkFormat(permalink)) {
                LOGGER.log(Level.DEBUG, "Skip permalink handling request [URI={}]", permalink);
                context.handle();
                return;
            }

            // 尝试走静态化缓存
            final String html = Statics.get(context);
            if (StringUtils.isNotBlank(html)) {
                context.getResponse().setContentType("text/html; charset=utf-8");
                context.sendString(html);
                context.abort();
                return;
            }

            final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class);
            article = articleRepository.getByPermalink(permalink);
            if (null == article) {
                LOGGER.log(Level.DEBUG, "Not found article with permalink [{}]", permalink);
                context.handle();
                return;
            }
        } catch (final RepositoryException e) {
            LOGGER.log(Level.ERROR, "Processes article permalink handler failed", e);
            context.sendError(404);
            return;
        }

        // If requests an article and the article need view password, sends redirect to the password form
        if (Solos.needViewPwd(context, article)) {
            try {
                context.sendRedirect(Latkes.getServePath() + "/console/article-pwd?articleId=" + article.optString(Keys.OBJECT_ID));
                return;
            } catch (final Exception e) {
                context.sendError(404);
                return;
            }
        }

        final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
        final JSONObject preference = optionQueryService.getPreference();
        final boolean allowVisitDraftViaPermalink = preference.getBoolean(Option.ID_C_ALLOW_VISIT_DRAFT_VIA_PERMALINK);
        if (Article.ARTICLE_STATUS_C_PUBLISHED != article.optInt(Article.ARTICLE_STATUS) && !allowVisitDraftViaPermalink) {
            context.sendError(404);
            return;
        }

        dispatchToArticleProcessor(context, article);
        context.handle();
    }

    /**
     * Dispatches the specified request to the specified article processor with the specified response.
     *
     * @param context the specified request context
     * @param article the specified article
     */
    private void dispatchToArticleProcessor(final RequestContext context, final JSONObject article) {
        context.attr(Article.ARTICLE, article);
        context.attr(Keys.HttpRequest.REQUEST_URI, Latkes.getContextPath() + "/article");
        context.attr(Keys.HttpRequest.REQUEST_METHOD, HttpMethod.GET.name());
    }
}


<|start_filename|>src/test/java/org/b3log/solo/processor/IndexProcessorTestCase.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor;

import org.apache.commons.lang3.StringUtils;
import org.b3log.latke.http.Cookie;
import org.b3log.solo.AbstractTestCase;
import org.b3log.solo.MockRequest;
import org.b3log.solo.MockResponse;
import org.b3log.solo.util.Solos;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.Set;

/**
 * {@link IndexProcessor} test case.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 2.0.0.0, Feb 9, 2020
 * @since 1.7.0
 */
@Test(suiteName = "processor")
public class IndexProcessorTestCase extends AbstractTestCase {

    /**
     * showIndex.
     */
    public void showIndex() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/");
        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "<title>Solo 的个人博客</title>"));
    }

    /**
     * showKillBrowser.
     */
    public void showKillBrowser() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/kill-browser");
        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "<title>Kill IE! - Solo 的个人博客</title>"));
    }

    /**
     * logout.
     */
    public void logout() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/logout");
        final MockResponse response = mockResponse();
        mockDispatcher(request, response);

        final Set<Cookie> cookies = response.getCookies();
        Assert.assertEquals(cookies.size(), 1);
        final Cookie first = cookies.iterator().next();
        Assert.assertEquals(first.getName(), Solos.COOKIE_NAME);
        Assert.assertEquals(first.getValue(), "");
    }
}


<|start_filename|>src/test/java/org/b3log/solo/processor/console/AdminConsoleTestCase.java<|end_filename|>
/*
 * Solo - A small and beautiful blogging system written in Java.
 * Copyright (c) 2010-present, b3log.org
 *
 * Solo is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.b3log.solo.processor.console;

import org.apache.commons.lang3.StringUtils;
import org.b3log.solo.AbstractTestCase;
import org.b3log.solo.MockRequest;
import org.b3log.solo.MockResponse;
import org.testng.Assert;
import org.testng.annotations.Test;

/**
 * {@link AdminConsole} test case.
 *
 * @author <a href="http://88250.b3log.org"><NAME></a>
 * @version 1.0.0.2, Feb 22, 2019
 * @since 2.9.7
 */
@Test(suiteName = "processor")
public class AdminConsoleTestCase extends AbstractTestCase {

    /**
     * showAdminIndex.
     */
    public void showAdminIndex() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/admin-index.do");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "后台管理 - Solo 的个人博客"));
    }

    /**
     * showAdminFunctions.
     */
    public void showAdminFunctions() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/admin-article.do");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "<div class=\"form\">"));
    }

    /**
     * showAdminPreferenceFunction.
     */
    public void showAdminPreferenceFunction() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/admin-preference.do");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final String content = response.getString();
        Assert.assertTrue(StringUtils.contains(content, "信息配置"));
    }

    /**
     * exportSQL.
     */
    public void exportSQL() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/export/sql");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final long outputBytes = response.getBytes().length;
        Assert.assertTrue(0 < outputBytes);
    }

    /**
     * exportJSON.
     */
    public void exportJSON() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/export/json");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final long outputBytes = response.getBytes().length;
        Assert.assertTrue(0 < outputBytes);
    }

    /**
     * exportHexo.
     */
    public void exportHexo() {
        final MockRequest request = mockRequest();
        request.setRequestURI("/console/export/hexo");
        mockAdminLogin(request);
        final MockResponse response = mockResponse();

        mockDispatcher(request, response);

        final long outputBytes = response.getBytes().length;
        Assert.assertTrue(0 < outputBytes);
    }
}
