/*
* Symphony - A modern community (forum/SNS/blog) platform written in Java.
* Copyright (C) 2012-2017, b3log.org & hacpai.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.b3log.symphony.processor;
import com.qiniu.util.Auth;
import jodd.util.Base64;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.b3log.latke.Keys;
import org.b3log.latke.ioc.inject.Inject;
import org.b3log.latke.logging.Level;
import org.b3log.latke.logging.Logger;
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.servlet.HTTPRequestContext;
import org.b3log.latke.servlet.HTTPRequestMethod;
import org.b3log.latke.servlet.annotation.After;
import org.b3log.latke.servlet.annotation.Before;
import org.b3log.latke.servlet.annotation.RequestProcessing;
import org.b3log.latke.servlet.annotation.RequestProcessor;
import org.b3log.latke.servlet.renderer.freemarker.AbstractFreeMarkerRenderer;
import org.b3log.latke.util.Paginator;
import org.b3log.latke.util.Requests;
import org.b3log.latke.util.Stopwatchs;
import org.b3log.latke.util.Strings;
import org.b3log.symphony.cache.DomainCache;
import org.b3log.symphony.model.*;
import org.b3log.symphony.processor.advice.*;
import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
import org.b3log.symphony.processor.advice.validate.ArticleAddValidation;
import org.b3log.symphony.processor.advice.validate.ArticleUpdateValidation;
import org.b3log.symphony.processor.advice.validate.UserRegisterValidation;
import org.b3log.symphony.service.*;
import org.b3log.symphony.util.*;
import org.json.JSONObject;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
/**
* Article processor.
* <p>
* <ul>
* <li>Shows an article (/article/{articleId}), GET</li>
* <li>Shows article pre adding form page (/pre-post), GET</li>
* <li>Shows article adding form page (/post), GET</li>
* <li>Adds an article (/post) <em>locally</em>, POST</li>
* <li>Shows an article updating form page (/update) <em>locally</em>, GET</li>
* <li>Updates an article (/article/{id}) <em>locally</em>, PUT</li>
* <li>Adds an article (/rhythm/article) <em>remotely</em>, POST</li>
* <li>Updates an article (/rhythm/article) <em>remotely</em>, PUT</li>
* <li>Markdowns text (/markdown), POST</li>
* <li>Rewards an article (/article/reward), POST</li>
* <li>Gets an article preview content (/article/{articleId}/preview), GET</li>
* <li>Sticks an article (/article/stick), POST</li>
* <li>Gets an article's revisions (/article/{id}/revisions), GET</li>
* <li>Gets article image (/article/{articleId}/image), GET</li>
* <li>Checks article title (/article/check-title), POST</li>
* <li>Removes an article (/article/{id}/remove), POST</li>
* </ul>
* </p>
* <p>
* The '<em>locally</em>' means user post an article on Symphony directly rather than receiving an article from
* externally (for example Rhythm).
* </p>
*
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @author <a href="http://zephyr.b3log.org">Zephyr</a>
* @version 1.26.28.45, May 9, 2017
* @since 0.2.0
*/
@RequestProcessor
public class ArticleProcessor {
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(ArticleProcessor.class);
/**
* Revision query service.
*/
@Inject
private RevisionQueryService revisionQueryService;
/**
* Short link query service.
*/
@Inject
private ShortLinkQueryService shortLinkQueryService;
/**
* Article management service.
*/
@Inject
private ArticleMgmtService articleMgmtService;
/**
* Article query service.
*/
@Inject
private ArticleQueryService articleQueryService;
/**
* Comment query service.
*/
@Inject
private CommentQueryService commentQueryService;
/**
* User query service.
*/
@Inject
private UserQueryService userQueryService;
/**
* User management service.
*/
@Inject
private UserMgmtService userMgmtService;
/**
* Client management service.
*/
@Inject
private ClientMgmtService clientMgmtService;
/**
* Client query service.
*/
@Inject
private ClientQueryService clientQueryService;
/**
* Language service.
*/
@Inject
private LangPropsService langPropsService;
/**
* Follow query service.
*/
@Inject
private FollowQueryService followQueryService;
/**
* Reward query service.
*/
@Inject
private RewardQueryService rewardQueryService;
/**
* Vote query service.
*/
@Inject
private VoteQueryService voteQueryService;
/**
* Liveness management service.
*/
@Inject
private LivenessMgmtService livenessMgmtService;
/**
* Referral management service.
*/
@Inject
private ReferralMgmtService referralMgmtService;
/**
* Character query service.
*/
@Inject
private CharacterQueryService characterQueryService;
/**
* Domain query service.
*/
@Inject
private DomainQueryService domainQueryService;
/**
* Domain cache.
*/
@Inject
private DomainCache domainCache;
/**
* Data model service.
*/
@Inject
private DataModelService dataModelService;
/**
* Removes an article.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/article/{id}/remove", method = HTTPRequestMethod.POST)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class, PermissionCheck.class})
@After(adviceClass = {StopwatchEndAdvice.class})
public void removeArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response,
final String id) throws Exception {
if (StringUtils.isBlank(id)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
final JSONObject currentUser = (JSONObject) request.getAttribute(User.USER);
final String currentUserId = currentUser.optString(Keys.OBJECT_ID);
final JSONObject article = articleQueryService.getArticle(id);
if (null == article) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
if (!authorId.equals(currentUserId)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
context.renderJSON();
try {
articleMgmtService.removeArticle(id);
context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
context.renderJSONValue(Article.ARTICLE_T_ID, id);
} catch (final ServiceException e) {
final String msg = e.getMessage();
context.renderMsg(msg);
context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
}
}
/**
* Checks article title.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/article/check-title", method = HTTPRequestMethod.POST)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class})
@After(adviceClass = {StopwatchEndAdvice.class})
public void checkArticleTitle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
final JSONObject currentUser = (JSONObject) request.getAttribute(User.USER);
final String currentUserId = currentUser.optString(Keys.OBJECT_ID);
final JSONObject requestJSONObject = Requests.parseRequestJSONObject(request, context.getResponse());
String title = requestJSONObject.optString(Article.ARTICLE_TITLE);
title = StringUtils.trim(title);
String id = requestJSONObject.optString(Article.ARTICLE_T_ID);
final JSONObject article = articleQueryService.getArticleByTitle(title);
if (null == article) {
context.renderJSON(true);
return;
}
if (StringUtils.isBlank(id)) { // Add
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
String msg;
if (authorId.equals(currentUserId)) {
msg = langPropsService.get("duplicatedArticleTitleSelfLabel");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + article.optString(Keys.OBJECT_ID)
+ "'>" + title + "</a>");
} else {
final JSONObject author = userQueryService.getUser(authorId);
final String userName = author.optString(User.USER_NAME);
msg = langPropsService.get("duplicatedArticleTitleLabel");
msg = msg.replace("{user}", "<a target='_blank' href='/member/" + userName + "'>" + userName + "</a>");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + article.optString(Keys.OBJECT_ID)
+ "'>" + title + "</a>");
}
final JSONObject ret = new JSONObject();
ret.put(Keys.STATUS_CODE, false);
ret.put(Keys.MSG, msg);
context.renderJSON(ret);
} else { // Update
final JSONObject oldArticle = articleQueryService.getArticle(id);
if (oldArticle.optString(Article.ARTICLE_TITLE).equals(title)) {
context.renderJSON(true);
return;
}
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
String msg;
if (authorId.equals(currentUserId)) {
msg = langPropsService.get("duplicatedArticleTitleSelfLabel");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + article.optString(Keys.OBJECT_ID)
+ "'>" + title + "</a>");
} else {
final JSONObject author = userQueryService.getUser(authorId);
final String userName = author.optString(User.USER_NAME);
msg = langPropsService.get("duplicatedArticleTitleLabel");
msg = msg.replace("{user}", "<a target='_blank' href='/member/" + userName + "'>" + userName + "</a>");
msg = msg.replace("{article}", "<a target='_blank' href='/article/" + article.optString(Keys.OBJECT_ID)
+ "'>" + title + "</a>");
}
final JSONObject ret = new JSONObject();
ret.put(Keys.STATUS_CODE, false);
ret.put(Keys.MSG, msg);
context.renderJSON(ret);
}
}
/**
* Gets article image.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @param articleId the specified article id
* @throws Exception exception
*/
@RequestProcessing(value = "/article/{articleId}/image", method = HTTPRequestMethod.GET)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class})
@After(adviceClass = {StopwatchEndAdvice.class})
public void getArticleImage(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response,
final String articleId) throws Exception {
final JSONObject article = articleQueryService.getArticle(articleId);
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
final Set<JSONObject> characters = characterQueryService.getWrittenCharacters();
final String articleContent = article.optString(Article.ARTICLE_CONTENT);
final List<BufferedImage> images = new ArrayList<>();
for (int i = 0; i < articleContent.length(); i++) {
final String ch = articleContent.substring(i, i + 1);
final JSONObject chRecord = org.b3log.symphony.model.Character.getCharacter(ch, characters);
if (null == chRecord) {
images.add(org.b3log.symphony.model.Character.createImage(ch));
continue;
}
final String imgData = chRecord.optString(org.b3log.symphony.model.Character.CHARACTER_IMG);
final byte[] data = Base64.decode(imgData.getBytes());
final BufferedImage img = ImageIO.read(new ByteArrayInputStream(data));
final BufferedImage newImage = new BufferedImage(50, 50, img.getType());
final Graphics g = newImage.getGraphics();
g.setClip(0, 0, 50, 50);
g.fillRect(0, 0, 50, 50);
g.drawImage(img, 0, 0, 50, 50, null);
g.dispose();
images.add(newImage);
}
final int rowCharacterCount = 30;
final int rows = (int) Math.ceil((double) images.size() / (double) rowCharacterCount);
final BufferedImage combined = new BufferedImage(30 * 50, rows * 50, Transparency.TRANSLUCENT);
int row = 0;
for (int i = 0; i < images.size(); i++) {
final BufferedImage image = images.get(i);
final Graphics g = combined.getGraphics();
g.drawImage(image, (i % rowCharacterCount) * 50, row * 50, null);
if (0 == (i + 1) % rowCharacterCount) {
row++;
}
}
ImageIO.write(combined, "PNG", new File("./hp.png"));
String url = "";
final JSONObject ret = new JSONObject();
ret.put(Keys.STATUS_CODE, true);
ret.put(Common.URL, (Object) url);
context.renderJSON(ret);
}
/**
* Gets an article's revisions.
*
* @param context the specified context
* @param id the specified article id
*/
@RequestProcessing(value = "/article/{id}/revisions", method = HTTPRequestMethod.GET)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class, PermissionCheck.class})
@After(adviceClass = {StopwatchEndAdvice.class})
public void getArticleRevisions(final HTTPRequestContext context, final String id) {
final List<JSONObject> revisions = revisionQueryService.getArticleRevisions(id);
final JSONObject ret = new JSONObject();
ret.put(Keys.STATUS_CODE, true);
ret.put(Revision.REVISIONS, (Object) revisions);
context.renderJSON(ret);
}
/**
* Shows pre-add article.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/pre-post", method = HTTPRequestMethod.GET)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class})
@After(adviceClass = {CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
public void showPreAddArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
final AbstractFreeMarkerRenderer renderer = new SkinRenderer(request);
context.setRenderer(renderer);
renderer.setTemplateName("/home/pre-post.ftl");
final Map<String, Object> dataModel = renderer.getDataModel();
dataModel.put(Common.BROADCAST_POINT, Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST);
dataModelService.fillHeaderAndFooter(request, response, dataModel);
}
/**
* Fills the domains with tags.
*
* @param dataModel the specified data model
*/
private void fillDomainsWithTags(final Map<String, Object> dataModel) {
final List<JSONObject> domains = domainCache.getDomains(Integer.MAX_VALUE);
dataModel.put(Common.ADD_ARTICLE_DOMAINS, domains);
for (final JSONObject domain : domains) {
final List<JSONObject> tags = domainQueryService.getTags(domain.optString(Keys.OBJECT_ID));
domain.put(Domain.DOMAIN_T_TAGS, (Object) tags);
}
final JSONObject user = (JSONObject) dataModel.get(Common.CURRENT_USER);
if (null == user) {
return;
}
try {
final JSONObject followingTagsResult = followQueryService.getFollowingTags(
user.optString(Keys.OBJECT_ID), 1, 28);
final List<JSONObject> followingTags = (List<JSONObject>) followingTagsResult.opt(Keys.RESULTS);
if (!followingTags.isEmpty()) {
final JSONObject userWatched = new JSONObject();
userWatched.put(Keys.OBJECT_ID, String.valueOf(System.currentTimeMillis()));
userWatched.put(Domain.DOMAIN_TITLE, langPropsService.get("notificationFollowingLabel"));
userWatched.put(Domain.DOMAIN_T_TAGS, (Object) followingTags);
domains.add(0, userWatched);
}
} catch (final Exception e) {
LOGGER.log(Level.ERROR, "Get user [name=" + user.optString(User.USER_NAME) + "] following tags failed", e);
}
}
/**
* Shows add article.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/post", method = HTTPRequestMethod.GET)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class})
@After(adviceClass = {CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
public void showAddArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
final AbstractFreeMarkerRenderer renderer = new SkinRenderer(request);
context.setRenderer(renderer);
renderer.setTemplateName("/home/post.ftl");
final Map<String, Object> dataModel = renderer.getDataModel();
// Qiniu file upload authenticate
final Auth auth = Auth.create(Symphonys.get("qiniu.accessKey"), Symphonys.get("qiniu.secretKey"));
final String uploadToken = auth.uploadToken(Symphonys.get("qiniu.bucket"));
dataModel.put("qiniuUploadToken", uploadToken);
dataModel.put("qiniuDomain", Symphonys.get("qiniu.domain"));
if (!Symphonys.getBoolean("qiniu.enabled")) {
dataModel.put("qiniuUploadToken", "");
}
final long imgMaxSize = Symphonys.getLong("upload.img.maxSize");
dataModel.put("imgMaxSize", imgMaxSize);
final long fileMaxSize = Symphonys.getLong("upload.file.maxSize");
dataModel.put("fileMaxSize", fileMaxSize);
String tags = request.getParameter(Tag.TAGS);
final JSONObject currentUser = (JSONObject) request.getAttribute(User.USER);
if (StringUtils.isBlank(tags)) {
tags = "";
dataModel.put(Tag.TAGS, tags);
} else {
tags = Tag.formatTags(tags);
final String[] tagTitles = tags.split(",");
final StringBuilder tagBuilder = new StringBuilder();
for (final String title : tagTitles) {
final String tagTitle = title.trim();
if (Strings.isEmptyOrNull(tagTitle)) {
continue;
}
if (Tag.containsWhiteListTags(tagTitle)) {
tagBuilder.append(tagTitle).append(",");
continue;
}
if (!Tag.TAG_TITLE_PATTERN.matcher(tagTitle).matches()) {
continue;
}
if (tagTitle.length() > Tag.MAX_TAG_TITLE_LENGTH) {
continue;
}
if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
&& ArrayUtils.contains(Symphonys.RESERVED_TAGS, tagTitle)) {
continue;
}
tagBuilder.append(tagTitle).append(",");
}
if (tagBuilder.length() > 0) {
tagBuilder.deleteCharAt(tagBuilder.length() - 1);
}
dataModel.put(Tag.TAGS, tagBuilder.toString());
}
final String type = request.getParameter(Common.TYPE);
if (StringUtils.isBlank(type)) {
dataModel.put(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
} else {
int articleType = Article.ARTICLE_TYPE_C_NORMAL;
try {
articleType = Integer.valueOf(type);
} catch (final Exception e) {
LOGGER.log(Level.WARN, "Gets article type error [" + type + "]", e);
}
if (Article.isInvalidArticleType(articleType)) {
articleType = Article.ARTICLE_TYPE_C_NORMAL;
}
dataModel.put(Article.ARTICLE_TYPE, articleType);
}
String at = request.getParameter(Common.AT);
at = StringUtils.trim(at);
if (StringUtils.isNotBlank(at)) {
dataModel.put(Common.AT, at + " ");
}
dataModelService.fillHeaderAndFooter(request, response, dataModel);
String rewardEditorPlaceholderLabel = langPropsService.get("rewardEditorPlaceholderLabel");
rewardEditorPlaceholderLabel = rewardEditorPlaceholderLabel.replace("{point}",
String.valueOf(Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_REWARD));
dataModel.put("rewardEditorPlaceholderLabel", rewardEditorPlaceholderLabel);
dataModel.put(Common.BROADCAST_POINT, Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST);
String articleContentErrorLabel = langPropsService.get("articleContentErrorLabel");
articleContentErrorLabel = articleContentErrorLabel.replace("{maxArticleContentLength}",
String.valueOf(ArticleAddValidation.MAX_ARTICLE_CONTENT_LENGTH));
dataModel.put("articleContentErrorLabel", articleContentErrorLabel);
final String b3logKey = currentUser.optString(UserExt.USER_B3_KEY);
dataModel.put("hasB3Key", !Strings.isEmptyOrNull(b3logKey));
fillPostArticleRequisite(dataModel, currentUser);
fillDomainsWithTags(dataModel);
}
private void fillPostArticleRequisite(final Map<String, Object> dataModel, final JSONObject currentUser) {
boolean requisite = false;
String requisiteMsg = "";
if (!UserExt.updatedAvatar(currentUser)) {
requisite = true;
requisiteMsg = langPropsService.get("uploadAvatarThenPostLabel");
}
dataModel.put(Common.REQUISITE, requisite);
dataModel.put(Common.REQUISITE_MSG, requisiteMsg);
}
/**
* Shows article with the specified article id.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @param articleId the specified article id
* @throws Exception exception
*/
@RequestProcessing(value = "/article/{articleId}", method = HTTPRequestMethod.GET)
@Before(adviceClass = {StopwatchStartAdvice.class, AnonymousViewCheck.class})
@After(adviceClass = {CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
public void showArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response,
final String articleId) throws Exception {
final AbstractFreeMarkerRenderer renderer = new SkinRenderer(request);
context.setRenderer(renderer);
renderer.setTemplateName("/article.ftl");
final Map<String, Object> dataModel = renderer.getDataModel();
final int avatarViewMode = (int) request.getAttribute(UserExt.USER_AVATAR_VIEW_MODE);
final JSONObject article = articleQueryService.getArticleById(avatarViewMode, articleId);
if (null == article) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
final HttpSession session = request.getSession(false);
if (null != session) {
session.setAttribute(Article.ARTICLE_T_ID, articleId);
}
dataModelService.fillHeaderAndFooter(request, response, dataModel);
final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
final JSONObject author = userQueryService.getUser(authorId);
if (Article.ARTICLE_ANONYMOUS_C_PUBLIC == article.optInt(Article.ARTICLE_ANONYMOUS)) {
article.put(Article.ARTICLE_T_AUTHOR_NAME, author.optString(User.USER_NAME));
article.put(Article.ARTICLE_T_AUTHOR_URL, author.optString(User.USER_URL));
article.put(Article.ARTICLE_T_AUTHOR_INTRO, author.optString(UserExt.USER_INTRO));
} else {
article.put(Article.ARTICLE_T_AUTHOR_NAME, UserExt.ANONYMOUS_USER_NAME);
article.put(Article.ARTICLE_T_AUTHOR_URL, "");
article.put(Article.ARTICLE_T_AUTHOR_INTRO, "");
}
dataModel.put(Article.ARTICLE, article);
article.put(Common.IS_MY_ARTICLE, false);
article.put(Article.ARTICLE_T_AUTHOR, author);
article.put(Common.REWARDED, false);
article.put(Common.REWARED_COUNT, rewardQueryService.rewardedCount(articleId, Reward.TYPE_C_ARTICLE));
if (!article.has(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK)) { // TODO: for legacy data
article.put(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK, "");
}
article.put(Article.ARTICLE_REVISION_COUNT, revisionQueryService.count(articleId, Revision.DATA_TYPE_C_ARTICLE));
articleQueryService.processArticleContent(article, request);
String cmtViewModeStr = request.getParameter("m");
JSONObject currentUser;
String currentUserId = null;
final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
if (isLoggedIn) {
currentUser = (JSONObject) dataModel.get(Common.CURRENT_USER);
currentUserId = currentUser.optString(Keys.OBJECT_ID);
article.put(Common.IS_MY_ARTICLE, currentUserId.equals(article.optString(Article.ARTICLE_AUTHOR_ID)));
final boolean isFollowing = followQueryService.isFollowing(currentUserId, articleId, Follow.FOLLOWING_TYPE_C_ARTICLE);
dataModel.put(Common.IS_FOLLOWING, isFollowing);
final boolean isWatching = followQueryService.isFollowing(currentUserId, articleId, Follow.FOLLOWING_TYPE_C_ARTICLE_WATCH);
dataModel.put(Common.IS_WATCHING, isWatching);
final int articleVote = voteQueryService.isVoted(currentUserId, articleId);
article.put(Article.ARTICLE_T_VOTE, articleVote);
if (currentUserId.equals(author.optString(Keys.OBJECT_ID))) {
article.put(Common.REWARDED, true);
} else {
article.put(Common.REWARDED, rewardQueryService.isRewarded(currentUserId, articleId, Reward.TYPE_C_ARTICLE));
}
if (Strings.isEmptyOrNull(cmtViewModeStr) || !Strings.isNumeric(cmtViewModeStr)) {
cmtViewModeStr = currentUser.optString(UserExt.USER_COMMENT_VIEW_MODE);
}
} else if (Strings.isEmptyOrNull(cmtViewModeStr) || !Strings.isNumeric(cmtViewModeStr)) {
cmtViewModeStr = "0";
}
final int cmtViewMode = Integer.valueOf(cmtViewModeStr);
dataModel.put(UserExt.USER_COMMENT_VIEW_MODE, cmtViewMode);
if (!(Boolean) request.getAttribute(Keys.HttpRequest.IS_SEARCH_ENGINE_BOT)) {
articleMgmtService.incArticleViewCount(articleId);
}
final JSONObject viewer = (JSONObject) request.getAttribute(User.USER);
if (null != viewer) {
livenessMgmtService.incLiveness(viewer.optString(Keys.OBJECT_ID), Liveness.LIVENESS_PV);
}
dataModelService.fillRelevantArticles(avatarViewMode, dataModel, article);
dataModelService.fillRandomArticles(avatarViewMode, dataModel);
dataModelService.fillSideHotArticles(avatarViewMode, dataModel);
// Qiniu file upload authenticate
final Auth auth = Auth.create(Symphonys.get("qiniu.accessKey"), Symphonys.get("qiniu.secretKey"));
final String uploadToken = auth.uploadToken(Symphonys.get("qiniu.bucket"));
dataModel.put("qiniuUploadToken", uploadToken);
dataModel.put("qiniuDomain", Symphonys.get("qiniu.domain"));
if (!Symphonys.getBoolean("qiniu.enabled")) {
dataModel.put("qiniuUploadToken", "");
}
final long imgMaxSize = Symphonys.getLong("upload.img.maxSize");
dataModel.put("imgMaxSize", imgMaxSize);
final long fileMaxSize = Symphonys.getLong("upload.file.maxSize");
dataModel.put("fileMaxSize", fileMaxSize);
// Fill article thank
Stopwatchs.start("Fills article thank");
try {
article.put(Common.THANKED, rewardQueryService.isRewarded(currentUserId, articleId, Reward.TYPE_C_THANK_ARTICLE));
article.put(Common.THANKED_COUNT, rewardQueryService.rewardedCount(articleId, Reward.TYPE_C_THANK_ARTICLE));
} finally {
Stopwatchs.end();
}
// Fill previous/next article
final JSONObject previous = articleQueryService.getPreviousPermalink(articleId);
final JSONObject next = articleQueryService.getNextPermalink(articleId);
dataModel.put(Article.ARTICLE_T_PREVIOUS, previous);
dataModel.put(Article.ARTICLE_T_NEXT, next);
String stickConfirmLabel = langPropsService.get("stickConfirmLabel");
stickConfirmLabel = stickConfirmLabel.replace("{point}", Symphonys.get("pointStickArticle"));
dataModel.put("stickConfirmLabel", stickConfirmLabel);
dataModel.put("pointThankArticle", Symphonys.get("pointThankArticle"));
String pageNumStr = request.getParameter("p");
if (Strings.isEmptyOrNull(pageNumStr) || !Strings.isNumeric(pageNumStr)) {
pageNumStr = "1";
}
final int pageNum = Integer.valueOf(pageNumStr);
final int pageSize = Symphonys.getInt("articleCommentsPageSize");
final int windowSize = Symphonys.getInt("articleCommentsWindowSize");
final int commentCnt = article.getInt(Article.ARTICLE_COMMENT_CNT);
final int pageCount = (int) Math.ceil((double) commentCnt / (double) pageSize);
final List<Integer> pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
if (!pageNums.isEmpty()) {
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_CURRENT_PAGE_NUM, pageNum);
dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
dataModel.put(Common.ARTICLE_COMMENTS_PAGE_SIZE, pageSize);
dataModel.put(Common.DISCUSSION_VIEWABLE, article.optBoolean(Common.DISCUSSION_VIEWABLE));
if (!article.optBoolean(Common.DISCUSSION_VIEWABLE)) {
article.put(Article.ARTICLE_T_COMMENTS, (Object) Collections.emptyList());
article.put(Article.ARTICLE_T_NICE_COMMENTS, (Object) Collections.emptyList());
return;
}
final List<JSONObject> niceComments
= commentQueryService.getNiceComments(avatarViewMode, cmtViewMode, articleId, 3);
article.put(Article.ARTICLE_T_NICE_COMMENTS, (Object) niceComments);
double niceCmtScore = Double.MAX_VALUE;
if (!niceComments.isEmpty()) {
niceCmtScore = niceComments.get(niceComments.size() - 1).optDouble(Comment.COMMENT_SCORE, 0D);
for (final JSONObject comment : niceComments) {
String thankTemplate = langPropsService.get("thankConfirmLabel");
thankTemplate = thankTemplate.replace("{point}", String.valueOf(Symphonys.getInt("pointThankComment")))
.replace("{user}", comment.optJSONObject(Comment.COMMENT_T_COMMENTER).optString(User.USER_NAME));
comment.put(Comment.COMMENT_T_THANK_LABEL, thankTemplate);
final String commentId = comment.optString(Keys.OBJECT_ID);
if (isLoggedIn) {
comment.put(Common.REWARDED,
rewardQueryService.isRewarded(currentUserId, commentId, Reward.TYPE_C_COMMENT));
final int commentVote = voteQueryService.isVoted(currentUserId, commentId);
comment.put(Comment.COMMENT_T_VOTE, commentVote);
}
comment.put(Common.REWARED_COUNT, rewardQueryService.rewardedCount(commentId, Reward.TYPE_C_COMMENT));
}
}
// Load comments
final List<JSONObject> articleComments =
commentQueryService.getArticleComments(avatarViewMode, articleId, pageNum, pageSize, cmtViewMode);
article.put(Article.ARTICLE_T_COMMENTS, (Object) articleComments);
// Fill comment thank
Stopwatchs.start("Fills comment thank");
try {
final String thankTemplate = langPropsService.get("thankConfirmLabel");
for (final JSONObject comment : articleComments) {
comment.put(Comment.COMMENT_T_NICE, comment.optDouble(Comment.COMMENT_SCORE, 0D) >= niceCmtScore);
final String thankStr = thankTemplate.replace("{point}", String.valueOf(Symphonys.getInt("pointThankComment")))
.replace("{user}", comment.optJSONObject(Comment.COMMENT_T_COMMENTER).optString(User.USER_NAME));
comment.put(Comment.COMMENT_T_THANK_LABEL, thankStr);
final String commentId = comment.optString(Keys.OBJECT_ID);
if (isLoggedIn) {
comment.put(Common.REWARDED,
rewardQueryService.isRewarded(currentUserId, commentId, Reward.TYPE_C_COMMENT));
final int commentVote = voteQueryService.isVoted(currentUserId, commentId);
comment.put(Comment.COMMENT_T_VOTE, commentVote);
}
comment.put(Common.REWARED_COUNT, rewardQueryService.rewardedCount(commentId, Reward.TYPE_C_COMMENT));
}
} finally {
Stopwatchs.end();
}
// Referral statistic
final String referralUserName = request.getParameter("r");
if (!UserRegisterValidation.invalidUserName(referralUserName)) {
final JSONObject referralUser = userQueryService.getUserByName(referralUserName);
if (null == referralUser) {
return;
}
final String viewerIP = Requests.getRemoteAddr(request);
final JSONObject referral = new JSONObject();
referral.put(Referral.REFERRAL_CLICK, 1);
referral.put(Referral.REFERRAL_DATA_ID, articleId);
referral.put(Referral.REFERRAL_IP, viewerIP);
referral.put(Referral.REFERRAL_TYPE, Referral.REFERRAL_TYPE_C_ARTICLE);
referral.put(Referral.REFERRAL_USER, referralUserName);
referralMgmtService.updateReferral(referral);
}
if (StringUtils.isBlank(article.optString(Article.ARTICLE_AUDIO_URL))) {
final String uid = StringUtils.isBlank(currentUserId) ? "visitor" : currentUserId;
articleMgmtService.genArticleAudio(article, uid);
}
}
/**
* Adds an article locally.
* <p>
* The request json object (an article):
* <pre>
* {
* "articleTitle": "",
* "articleTags": "", // Tags spliting by ','
* "articleContent": "",
* "articleCommentable": boolean,
* "articleType": int,
* "articleRewardContent": "",
* "articleRewardPoint": int,
* "articleAnonymous": boolean
* }
* </pre>
* </p>
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws IOException io exception
* @throws ServletException servlet exception
*/
@RequestProcessing(value = "/article", method = HTTPRequestMethod.POST)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, ArticleAddValidation.class,
PermissionCheck.class})
@After(adviceClass = StopwatchEndAdvice.class)
public void addArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
context.renderJSON();
final JSONObject requestJSONObject = (JSONObject) request.getAttribute(Keys.REQUEST);
final String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
String articleTags = requestJSONObject.optString(Article.ARTICLE_TAGS);
final String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
//final boolean articleCommentable = requestJSONObject.optBoolean(Article.ARTICLE_COMMENTABLE);
final boolean articleCommentable = true;
final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
final String articleRewardContent = requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT);
final int articleRewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT);
final String ip = Requests.getRemoteAddr(request);
final String ua = request.getHeader(Common.USER_AGENT);
final boolean isAnonymous = requestJSONObject.optBoolean(Article.ARTICLE_ANONYMOUS, false);
final int articleAnonymous = isAnonymous
? Article.ARTICLE_ANONYMOUS_C_ANONYMOUS : Article.ARTICLE_ANONYMOUS_C_PUBLIC;
final boolean syncWithSymphonyClient = requestJSONObject.optBoolean(Article.ARTICLE_SYNC_TO_CLIENT, false);
final JSONObject article = new JSONObject();
article.put(Article.ARTICLE_TITLE, articleTitle);
article.put(Article.ARTICLE_CONTENT, articleContent);
article.put(Article.ARTICLE_EDITOR_TYPE, 0);
article.put(Article.ARTICLE_COMMENTABLE, articleCommentable);
article.put(Article.ARTICLE_TYPE, articleType);
article.put(Article.ARTICLE_REWARD_CONTENT, articleRewardContent);
article.put(Article.ARTICLE_REWARD_POINT, articleRewardPoint);
article.put(Article.ARTICLE_IP, "");
if (StringUtils.isNotBlank(ip)) {
article.put(Article.ARTICLE_IP, ip);
}
article.put(Article.ARTICLE_UA, "");
if (StringUtils.isNotBlank(ua)) {
article.put(Article.ARTICLE_UA, ua);
}
article.put(Article.ARTICLE_ANONYMOUS, articleAnonymous);
article.put(Article.ARTICLE_SYNC_TO_CLIENT, syncWithSymphonyClient);
try {
final JSONObject currentUser = (JSONObject) request.getAttribute(User.USER);
article.put(Article.ARTICLE_AUTHOR_ID, currentUser.optString(Keys.OBJECT_ID));
if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
articleTags = articleMgmtService.filterReservedTags(articleTags);
}
if (Article.ARTICLE_TYPE_C_DISCUSSION == articleType && StringUtils.isBlank(articleTags)) {
articleTags = "小黑屋";
}
if (Article.ARTICLE_TYPE_C_THOUGHT == articleType && StringUtils.isBlank(articleTags)) {
articleTags = "思绪";
}
article.put(Article.ARTICLE_TAGS, articleTags);
article.put(Article.ARTICLE_T_IS_BROADCAST, false);
final String articleId = articleMgmtService.addArticle(article);
context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
context.renderJSONValue(Article.ARTICLE_T_ID, articleId);
} catch (final ServiceException e) {
final String msg = e.getMessage();
LOGGER.log(Level.ERROR, "Adds article[title=" + articleTitle + "] failed: {0}", e.getMessage());
context.renderMsg(msg);
context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
}
}
/**
* Shows update article.
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/update", method = HTTPRequestMethod.GET)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class})
@After(adviceClass = {CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
public void showUpdateArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
final String articleId = request.getParameter("id");
if (Strings.isEmptyOrNull(articleId)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
final JSONObject article = articleQueryService.getArticle(articleId);
if (null == article) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
final JSONObject currentUser = (JSONObject) request.getAttribute(User.USER);
if (null == currentUser
|| !currentUser.optString(Keys.OBJECT_ID).equals(article.optString(Article.ARTICLE_AUTHOR_ID))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
final AbstractFreeMarkerRenderer renderer = new SkinRenderer(request);
context.setRenderer(renderer);
renderer.setTemplateName("/home/post.ftl");
final Map<String, Object> dataModel = renderer.getDataModel();
dataModel.put(Article.ARTICLE, article);
dataModelService.fillHeaderAndFooter(request, response, dataModel);
// Qiniu file upload authenticate
final Auth auth = Auth.create(Symphonys.get("qiniu.accessKey"), Symphonys.get("qiniu.secretKey"));
final String uploadToken = auth.uploadToken(Symphonys.get("qiniu.bucket"));
dataModel.put("qiniuUploadToken", uploadToken);
dataModel.put("qiniuDomain", Symphonys.get("qiniu.domain"));
if (!Symphonys.getBoolean("qiniu.enabled")) {
dataModel.put("qiniuUploadToken", "");
}
final long imgMaxSize = Symphonys.getLong("upload.img.maxSize");
dataModel.put("imgMaxSize", imgMaxSize);
final long fileMaxSize = Symphonys.getLong("upload.file.maxSize");
dataModel.put("fileMaxSize", fileMaxSize);
fillDomainsWithTags(dataModel);
String rewardEditorPlaceholderLabel = langPropsService.get("rewardEditorPlaceholderLabel");
rewardEditorPlaceholderLabel = rewardEditorPlaceholderLabel.replace("{point}",
String.valueOf(Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_REWARD));
dataModel.put("rewardEditorPlaceholderLabel", rewardEditorPlaceholderLabel);
dataModel.put(Common.BROADCAST_POINT, Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST);
final String b3logKey = currentUser.optString(UserExt.USER_B3_KEY);
dataModel.put("hasB3Key", !Strings.isEmptyOrNull(b3logKey));
fillPostArticleRequisite(dataModel, currentUser);
}
/**
* Updates an article locally.
* <p>
* The request json object (an article):
* <pre>
* {
* "articleTitle": "",
* "articleTags": "", // Tags spliting by ','
* "articleContent": "",
* "articleCommentable": boolean,
* "articleType": int,
* "articleRewardContent": "",
* "articleRewardPoint": int
* }
* </pre>
* </p>
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @param id the specified article id
* @throws Exception exception
*/
@RequestProcessing(value = "/article/{id}", method = HTTPRequestMethod.PUT)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, ArticleUpdateValidation.class,
PermissionCheck.class})
@After(adviceClass = StopwatchEndAdvice.class)
public void updateArticle(final HTTPRequestContext context, final HttpServletRequest request, final HttpServletResponse response,
final String id) throws Exception {
if (Strings.isEmptyOrNull(id)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
final int avatarViewMode = (int) request.getAttribute(UserExt.USER_AVATAR_VIEW_MODE);
final JSONObject oldArticle = articleQueryService.getArticleById(avatarViewMode, id);
if (null == oldArticle) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
context.renderJSON();
final JSONObject requestJSONObject = (JSONObject) request.getAttribute(Keys.REQUEST);
final String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
String articleTags = requestJSONObject.optString(Article.ARTICLE_TAGS);
final String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
//final boolean articleCommentable = requestJSONObject.optBoolean(Article.ARTICLE_COMMENTABLE);
final boolean articleCommentable = true;
final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
final String articleRewardContent = requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT);
final int articleRewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT);
final String ip = Requests.getRemoteAddr(request);
final String ua = request.getHeader(Common.USER_AGENT);
final JSONObject article = new JSONObject();
article.put(Keys.OBJECT_ID, id);
article.put(Article.ARTICLE_TITLE, articleTitle);
article.put(Article.ARTICLE_CONTENT, articleContent);
article.put(Article.ARTICLE_EDITOR_TYPE, 0);
article.put(Article.ARTICLE_COMMENTABLE, articleCommentable);
article.put(Article.ARTICLE_TYPE, articleType);
article.put(Article.ARTICLE_REWARD_CONTENT, articleRewardContent);
article.put(Article.ARTICLE_REWARD_POINT, articleRewardPoint);
article.put(Article.ARTICLE_IP, "");
if (StringUtils.isNotBlank(ip)) {
article.put(Article.ARTICLE_IP, ip);
}
article.put(Article.ARTICLE_UA, "");
if (StringUtils.isNotBlank(ua)) {
article.put(Article.ARTICLE_UA, ua);
}
final JSONObject currentUser = (JSONObject) request.getAttribute(User.USER);
if (null == currentUser
|| !currentUser.optString(Keys.OBJECT_ID).equals(oldArticle.optString(Article.ARTICLE_AUTHOR_ID))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
article.put(Article.ARTICLE_AUTHOR_ID, currentUser.optString(Keys.OBJECT_ID));
if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
articleTags = articleMgmtService.filterReservedTags(articleTags);
}
if (Article.ARTICLE_TYPE_C_DISCUSSION == articleType && StringUtils.isBlank(articleTags)) {
articleTags = "小黑屋";
}
if (Article.ARTICLE_TYPE_C_THOUGHT == articleType && StringUtils.isBlank(articleTags)) {
articleTags = "思绪";
}
article.put(Article.ARTICLE_TAGS, articleTags);
try {
articleMgmtService.updateArticle(article);
context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
context.renderJSONValue(Article.ARTICLE_T_ID, id);
} catch (final ServiceException e) {
final String msg = e.getMessage();
LOGGER.log(Level.ERROR, "Adds article[title=" + articleTitle + "] failed: {0}", e.getMessage());
context.renderMsg(msg);
context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
}
}
/**
* Adds an article remotely.
* <p>
* This interface will be called by Rhythm, so here is no article data validation, just only validate the B3
* key.
* </p>
* <p>
* The request json object, for example,
* <pre>
* {
* "article": {
* "articleAuthorEmail": "DL88250@gmail.com",
* "articleContent": "<p>test<\/p>",
* "articleCreateDate": 1350635469922,
* "articlePermalink": "/articles/2012/10/19/1350635469866.html",
* "articleTags": "test",
* "articleTitle": "test",
* "clientArticleId": "1350635469866",
* "oId": "1350635469866"
* },
* "clientAdminEmail": "DL88250@gmail.com",
* "clientHost": "http://localhost:11099",
* "clientName": "B3log Solo",
* "clientTitle": "简约设计の艺术",
* "clientRuntimeEnv": "LOCAL",
* "clientVersion": "0.5.0",
* "symphonyKey": "....",
* "userB3Key": "Your key"
* }
* </pre>
* </p>
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/rhythm/article", method = HTTPRequestMethod.POST)
@Before(adviceClass = StopwatchStartAdvice.class)
@After(adviceClass = StopwatchEndAdvice.class)
public void addArticleFromRhythm(final HTTPRequestContext context,
final HttpServletRequest request, final HttpServletResponse response) throws Exception {
context.renderJSON();
final JSONObject requestJSONObject = Requests.parseRequestJSONObject(request, response);
final String clientB3Key = requestJSONObject.getString(UserExt.USER_B3_KEY);
final String symphonyKey = requestJSONObject.getString(Common.SYMPHONY_KEY);
final String clientAdminEmail = requestJSONObject.getString(Client.CLIENT_ADMIN_EMAIL);
final String clientName = requestJSONObject.getString(Client.CLIENT_NAME);
final String clientTitle = requestJSONObject.getString(Client.CLIENT_T_TITLE);
final String clientVersion = requestJSONObject.getString(Client.CLIENT_VERSION);
final String clientHost = requestJSONObject.getString(Client.CLIENT_HOST);
final String clientRuntimeEnv = requestJSONObject.getString(Client.CLIENT_RUNTIME_ENV);
// final String maybeIP = StringUtils.substringBetween(clientHost, "://", ":");
// if (Networks.isIPv4(maybeIP)) {
// LOGGER.log(Level.WARN, "Sync add article error, caused by the client host [{0}] is invalid", clientHost);
//
// return;
// }
final JSONObject user = userQueryService.getUserByEmail(clientAdminEmail);
if (null == user) {
LOGGER.log(Level.WARN, "The user[email={0}, host={1}] not found in community", clientAdminEmail, clientHost);
return;
}
final String userName = user.optString(User.USER_NAME);
String userKey = user.optString(UserExt.USER_B3_KEY);
if (StringUtils.isBlank(userKey) || (Strings.isNumeric(userKey) && userKey.length() == clientB3Key.length())) {
userKey = clientB3Key;
user.put(UserExt.USER_B3_KEY, userKey);
userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
}
if (!Symphonys.get("keyOfSymphony").equals(symphonyKey) || !userKey.equals(clientB3Key)) {
LOGGER.log(Level.WARN, "B3 key not match, ignored add article [name={0}, email={1}, host={2}, userSymKey={3}, userClientKey={4}]",
userName, clientAdminEmail, clientHost, user.optString(UserExt.USER_B3_KEY), clientB3Key);
return;
}
if (UserExt.USER_STATUS_C_VALID != user.optInt(UserExt.USER_STATUS)) {
LOGGER.log(Level.WARN, "The user[name={0}, email={1}, host={2}] has been forbidden", userName, clientAdminEmail, clientHost);
return;
}
final JSONObject originalArticle = requestJSONObject.getJSONObject(Article.ARTICLE);
final String authorId = user.optString(Keys.OBJECT_ID);
final String clientArticleId = originalArticle.optString(Keys.OBJECT_ID);
final String articleTitle = originalArticle.optString(Article.ARTICLE_TITLE);
String articleTags = Tag.formatTags(originalArticle.optString(Article.ARTICLE_TAGS));
String articleContent = originalArticle.optString(Article.ARTICLE_CONTENT);
final JSONObject article = new JSONObject();
article.put(Article.ARTICLE_TITLE, articleTitle);
article.put(Article.ARTICLE_EDITOR_TYPE, 0);
article.put(Article.ARTICLE_SYNC_TO_CLIENT, true);
article.put(Article.ARTICLE_CLIENT_ARTICLE_ID, clientArticleId);
article.put(Article.ARTICLE_AUTHOR_ID, authorId);
final String permalink = originalArticle.optString(Article.ARTICLE_PERMALINK);
final JSONObject articleExisted = articleQueryService.getArticleByClientArticleId(authorId, clientArticleId);
final boolean toAdd = null == articleExisted;
if (!toAdd) { // Client requests to add an article, but the article already exist in server
article.put(Keys.OBJECT_ID, articleExisted.optString(Keys.OBJECT_ID));
article.put(Article.ARTICLE_T_IS_BROADCAST, false);
// articleContent += "\n\n<p class='fn-clear'><span class='fn-right'><span class='ft-small'>该文章同步自</span> "
// + "<i style='margin-right:5px;'><a target='_blank' href='"
// + clientHost + permalink + "'>" + clientTitle + "</a></i></span></p>";
} else { // Add
final boolean isBroadcast = "aBroadcast".equals(permalink);
if (isBroadcast) {
articleContent += "\n\n<p class='fn-clear'><span class='fn-right'><span class='ft-small'>该广播来自</span> "
+ "<i style='margin-right:5px;'><a target='_blank' href='"
+ clientHost + "'>" + clientTitle + "</a></i></span></p>";
} else {
// articleContent += "\n\n<p class='fn-clear'><span class='fn-right'><span class='ft-small'>该文章同步自</span> "
// + "<i style='margin-right:5px;'><a target='_blank' href='"
// + clientHost + permalink + "'>" + clientTitle + "</a></i></span></p>";
}
article.put(Article.ARTICLE_T_IS_BROADCAST, isBroadcast);
}
article.put(Article.ARTICLE_CONTENT, articleContent);
article.put(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK, clientHost + permalink);
if (!Role.ROLE_ID_C_ADMIN.equals(user.optString(User.USER_ROLE))) {
articleTags = articleMgmtService.filterReservedTags(articleTags);
}
try {
articleTags = "B3log," + articleTags;
articleTags = Tag.formatTags(articleTags);
article.put(Article.ARTICLE_TAGS, articleTags);
if (toAdd) {
articleMgmtService.addArticle(article);
} else {
articleMgmtService.updateArticle(article);
}
context.renderTrueResult();
} catch (final ServiceException e) {
final String msg = langPropsService.get("updateFailLabel") + " - " + e.getMessage();
LOGGER.log(Level.ERROR, msg, e);
context.renderMsg(msg);
}
// Updates client record
JSONObject client = clientQueryService.getClientByAdminEmail(clientAdminEmail);
if (null == client) {
client = new JSONObject();
client.put(Client.CLIENT_ADMIN_EMAIL, clientAdminEmail);
client.put(Client.CLIENT_HOST, clientHost);
client.put(Client.CLIENT_NAME, clientName);
client.put(Client.CLIENT_RUNTIME_ENV, clientRuntimeEnv);
client.put(Client.CLIENT_VERSION, clientVersion);
client.put(Client.CLIENT_LATEST_ADD_COMMENT_TIME, 0L);
client.put(Client.CLIENT_LATEST_ADD_ARTICLE_TIME, System.currentTimeMillis());
clientMgmtService.addClient(client);
} else {
client.put(Client.CLIENT_ADMIN_EMAIL, clientAdminEmail);
client.put(Client.CLIENT_HOST, clientHost);
client.put(Client.CLIENT_NAME, clientName);
client.put(Client.CLIENT_RUNTIME_ENV, clientRuntimeEnv);
client.put(Client.CLIENT_VERSION, clientVersion);
client.put(Client.CLIENT_LATEST_ADD_ARTICLE_TIME, System.currentTimeMillis());
clientMgmtService.updateClient(client);
}
}
/**
* Updates an article remotely.
* <p>
* This interface will be called by Rhythm, so here is no article data validation, just only validate the B3
* key.
* </p>
* <p>
* The request json object, for example,
* <pre>
* {
* "article": {
* "articleAuthorEmail": "DL88250@gmail.com",
* "articleContent": "<p>test<\/p>",
* "articleCreateDate": 1350635469922,
* "articlePermalink": "/articles/2012/10/19/1350635469866.html",
* "articleTags": "test",
* "articleTitle": "test",
* "clientArticleId": "1350635469866",
* "oId": "1350635469866"
* },
* "clientAdminEmail": "DL88250@gmail.com",
* "clientHost": "http://localhost:11099",
* "clientName": "B3log Solo",
* "clientTitle": "简约设计の艺术",
* "clientRuntimeEnv": "LOCAL",
* "clientVersion": "0.5.0",
* "symphonyKey": "....",
* "userB3Key": "Your key"
* }
* </pre>
* </p>
*
* @param context the specified context
* @param request the specified request
* @param response the specified response
* @throws Exception exception
*/
@RequestProcessing(value = "/rhythm/article", method = HTTPRequestMethod.PUT)
@Before(adviceClass = StopwatchStartAdvice.class)
@After(adviceClass = StopwatchEndAdvice.class)
public void updateArticleFromRhythm(final HTTPRequestContext context,
final HttpServletRequest request, final HttpServletResponse response) throws Exception {
context.renderJSON();
final JSONObject requestJSONObject = Requests.parseRequestJSONObject(request, response);
final String clientB3Key = requestJSONObject.getString(UserExt.USER_B3_KEY);
final String symphonyKey = requestJSONObject.getString(Common.SYMPHONY_KEY);
final String clientAdminEmail = requestJSONObject.getString(Client.CLIENT_ADMIN_EMAIL);
final String clientName = requestJSONObject.getString(Client.CLIENT_NAME);
final String clientTitle = requestJSONObject.getString(Client.CLIENT_T_TITLE);
final String clientVersion = requestJSONObject.getString(Client.CLIENT_VERSION);
final String clientHost = requestJSONObject.getString(Client.CLIENT_HOST);
final String clientRuntimeEnv = requestJSONObject.getString(Client.CLIENT_RUNTIME_ENV);
// final String maybeIP = StringUtils.substringBetween(clientHost, "://", ":");
// if (Networks.isIPv4(maybeIP)) {
// LOGGER.log(Level.WARN, "Sync update article error, caused by the client host [{0}] is invalid", clientHost);
//
// return;
// }
final JSONObject user = userQueryService.getUserByEmail(clientAdminEmail);
if (null == user) {
LOGGER.log(Level.WARN, "The user[email={0}, host={1}] not found in community", clientAdminEmail, clientHost);
return;
}
final String userName = user.optString(User.USER_NAME);
String userKey = user.optString(UserExt.USER_B3_KEY);
if (StringUtils.isBlank(userKey) || (Strings.isNumeric(userKey) && userKey.length() == clientB3Key.length())) {
userKey = clientB3Key;
user.put(UserExt.USER_B3_KEY, userKey);
userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
}
if (!Symphonys.get("keyOfSymphony").equals(symphonyKey) || !userKey.equals(clientB3Key)) {
LOGGER.log(Level.WARN, "B3 key not match, ignored update article [name={0}, email={1}, host={2}, userSymKey={3}, userClientKey={4}]",
userName, clientAdminEmail, clientHost, user.optString(UserExt.USER_B3_KEY), clientB3Key);
return;
}
if (UserExt.USER_STATUS_C_VALID != user.optInt(UserExt.USER_STATUS)) {
LOGGER.log(Level.WARN, "The user[name={0}, email={1}, host={2}] has been forbidden", userName, clientAdminEmail, clientHost);
return;
}
final JSONObject originalArticle = requestJSONObject.getJSONObject(Article.ARTICLE);
final String articleTitle = originalArticle.optString(Article.ARTICLE_TITLE);
String articleTags = Tag.formatTags(originalArticle.optString(Article.ARTICLE_TAGS));
String articleContent = originalArticle.optString(Article.ARTICLE_CONTENT);
final String permalink = originalArticle.optString(Article.ARTICLE_PERMALINK);
// articleContent += "\n\n<p class='fn-clear'><span class='fn-right'><span class='ft-small'>该文章同步自</span> "
// + "<i style='margin-right:5px;'><a target='_blank' href='"
// + clientHost + permalink + "'>" + clientTitle + "</a></i></span></p>";
final String authorId = user.optString(Keys.OBJECT_ID);
final String clientArticleId = originalArticle.optString(Keys.OBJECT_ID);
final JSONObject oldArticle = articleQueryService.getArticleByClientArticleId(authorId, clientArticleId);
if (null == oldArticle) {
LOGGER.log(Level.WARN, "Not found article [clientHost={0}, clientArticleId={1}] to update", clientHost, clientArticleId);
return;
}
final JSONObject article = new JSONObject();
article.put(Keys.OBJECT_ID, oldArticle.optString(Keys.OBJECT_ID));
article.put(Article.ARTICLE_TITLE, articleTitle);
article.put(Article.ARTICLE_CONTENT, articleContent);
article.put(Article.ARTICLE_EDITOR_TYPE, 0);
article.put(Article.ARTICLE_SYNC_TO_CLIENT, false);
article.put(Article.ARTICLE_CLIENT_ARTICLE_ID, clientArticleId);
article.put(Article.ARTICLE_AUTHOR_ID, authorId);
article.put(Article.ARTICLE_T_IS_BROADCAST, false);
article.put(Article.ARTICLE_CLIENT_ARTICLE_PERMALINK, clientHost + permalink);
if (!Role.ROLE_ID_C_ADMIN.equals(user.optString(User.USER_ROLE))) {
articleTags = articleMgmtService.filterReservedTags(articleTags);
}
try {
articleTags = "B3log," + articleTags;
articleTags = Tag.formatTags(articleTags);
article.put(Article.ARTICLE_TAGS, articleTags);
articleMgmtService.updateArticle(article);
context.renderTrueResult();
} catch (final ServiceException e) {
final String msg = langPropsService.get("updateFailLabel") + " - " + e.getMessage();
LOGGER.log(Level.ERROR, msg, e);
context.renderMsg(msg);
}
// Updates client record
JSONObject client = clientQueryService.getClientByAdminEmail(clientAdminEmail);
if (null == client) {
client = new JSONObject();
client.put(Client.CLIENT_ADMIN_EMAIL, clientAdminEmail);
client.put(Client.CLIENT_HOST, clientHost);
client.put(Client.CLIENT_NAME, clientName);
client.put(Client.CLIENT_RUNTIME_ENV, clientRuntimeEnv);
client.put(Client.CLIENT_VERSION, clientVersion);
client.put(Client.CLIENT_LATEST_ADD_COMMENT_TIME, 0L);
client.put(Client.CLIENT_LATEST_ADD_ARTICLE_TIME, System.currentTimeMillis());
clientMgmtService.addClient(client);
} else {
client.put(Client.CLIENT_ADMIN_EMAIL, clientAdminEmail);
client.put(Client.CLIENT_HOST, clientHost);
client.put(Client.CLIENT_NAME, clientName);
client.put(Client.CLIENT_RUNTIME_ENV, clientRuntimeEnv);
client.put(Client.CLIENT_VERSION, clientVersion);
client.put(Client.CLIENT_LATEST_ADD_ARTICLE_TIME, System.currentTimeMillis());
clientMgmtService.updateClient(client);
}
}
/**
* Markdowns.
* <p>
* Renders the response with a json object, for example,
* <pre>
* {
* "html": ""
* }
* </pre>
* </p>
*
* @param request the specified http servlet request
* @param response the specified http servlet response
* @param context the specified http request context
* @throws Exception exception
*/
@RequestProcessing(value = "/markdown", method = HTTPRequestMethod.POST)
@Before(adviceClass = {StopwatchStartAdvice.class, LoginCheck.class})
@After(adviceClass = StopwatchEndAdvice.class)
public void markdown2HTML(final HttpServletRequest request, final HttpServletResponse response, final HTTPRequestContext context)
throws Exception {
context.renderJSON(true);
String markdownText = request.getParameter("markdownText");
if (Strings.isEmptyOrNull(markdownText)) {
context.renderJSONValue("html", "");
return;
}
markdownText = shortLinkQueryService.linkArticle(markdownText);
markdownText = shortLinkQueryService.linkTag(markdownText);
markdownText = Emotions.toAliases(markdownText);
markdownText = Emotions.convert(markdownText);
markdownText = Markdowns.toHTML(markdownText);
markdownText = Markdowns.clean(markdownText, "");
markdownText = MP3Players.render(markdownText);
context.renderJSONValue("html", markdownText);
}
/**
* Gets article preview content.
* <p>
* Renders the response with a json object, for example,
* <pre>
* {
* "html": ""
* }
* </pre>
* </p>
*
* @param request the specified http servlet request
* @param response the specified http servlet response
* @param context the specified http request context
* @param articleId the specified article id
* @throws Exception exception
*/
@RequestProcessing(value = "/article/{articleId}/preview", method = HTTPRequestMethod.GET)
@Before(adviceClass = StopwatchStartAdvice.class)
@After(adviceClass = StopwatchEndAdvice.class)
public void getArticlePreviewContent(final HttpServletRequest request, final HttpServletResponse response,
final HTTPRequestContext context, final String articleId) throws Exception {
final String content = articleQueryService.getArticlePreviewContent(articleId, request);
if (StringUtils.isBlank(content)) {
context.renderJSON().renderFalseResult();
return;
}
context.renderJSON(true).renderJSONValue("html", content);
}
/**
* Article rewards.
*
* @param request the specified http servlet request
* @param response the specified http servlet response
* @param context the specified http request context
* @throws Exception exception
*/
@RequestProcessing(value = "/article/reward", method = HTTPRequestMethod.POST)
@Before(adviceClass = StopwatchStartAdvice.class)
@After(adviceClass = StopwatchEndAdvice.class)
public void reward(final HttpServletRequest request, final HttpServletResponse response, final HTTPRequestContext context)
throws Exception {
final JSONObject currentUser = userQueryService.getCurrentUser(request);
if (null == currentUser) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
final String articleId = request.getParameter(Article.ARTICLE_T_ID);
if (Strings.isEmptyOrNull(articleId)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
context.renderJSON();
try {
articleMgmtService.reward(articleId, currentUser.optString(Keys.OBJECT_ID));
} catch (final ServiceException e) {
context.renderMsg(langPropsService.get("transferFailLabel"));
return;
}
final JSONObject article = articleQueryService.getArticle(articleId);
articleQueryService.processArticleContent(article, request);
String markdownText = article.optString(Article.ARTICLE_REWARD_CONTENT);
markdownText = shortLinkQueryService.linkArticle(markdownText);
markdownText = shortLinkQueryService.linkTag(markdownText);
markdownText = Emotions.toAliases(markdownText);
markdownText = Emotions.convert(markdownText);
markdownText = Markdowns.toHTML(markdownText);
markdownText = Markdowns.clean(markdownText, "");
context.renderTrueResult().
renderJSONValue(Article.ARTICLE_REWARD_CONTENT, markdownText);
}
/**
* Article thanks.
*
* @param request the specified http servlet request
* @param response the specified http servlet response
* @param context the specified http request context
* @throws Exception exception
*/
@RequestProcessing(value = "/article/thank", method = HTTPRequestMethod.POST)
@Before(adviceClass = {StopwatchStartAdvice.class, PermissionCheck.class})
@After(adviceClass = StopwatchEndAdvice.class)
public void thank(final HttpServletRequest request, final HttpServletResponse response, final HTTPRequestContext context)
throws Exception {
final JSONObject currentUser = userQueryService.getCurrentUser(request);
if (null == currentUser) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
final String articleId = request.getParameter(Article.ARTICLE_T_ID);
if (Strings.isEmptyOrNull(articleId)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
context.renderJSON();
try {
articleMgmtService.thank(articleId, currentUser.optString(Keys.OBJECT_ID));
} catch (final ServiceException e) {
context.renderMsg(langPropsService.get("transferFailLabel"));
return;
}
context.renderTrueResult();
}
/**
* Sticks an article.
*
* @param request the specified HTTP servlet request
* @param response the specified HTTP servlet response
* @param context the specified HTTP request context
* @throws Exception exception
*/
@RequestProcessing(value = "/article/stick", method = HTTPRequestMethod.POST)
@Before(adviceClass = {StopwatchStartAdvice.class, PermissionCheck.class})
@After(adviceClass = StopwatchEndAdvice.class)
public void stickArticle(final HttpServletRequest request, final HttpServletResponse response, final HTTPRequestContext context)
throws Exception {
final JSONObject currentUser = userQueryService.getCurrentUser(request);
if (null == currentUser) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
final String articleId = request.getParameter(Article.ARTICLE_T_ID);
if (Strings.isEmptyOrNull(articleId)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
final JSONObject article = articleQueryService.getArticle(articleId);
if (null == article) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (!currentUser.optString(Keys.OBJECT_ID).equals(article.optString(Article.ARTICLE_AUTHOR_ID))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
context.renderJSON();
try {
articleMgmtService.stick(articleId);
} catch (final ServiceException e) {
context.renderMsg(e.getMessage());
return;
}
context.renderTrueResult().renderMsg(langPropsService.get("stickSuccLabel"));
}
/**
* Expires a sticked article.
*
* @param request the specified HTTP servlet request
* @param response the specified HTTP servlet response
* @param context the specified HTTP request context
* @throws Exception exception
*/
@RequestProcessing(value = "/cron/article/stick-expire", method = HTTPRequestMethod.GET)
@Before(adviceClass = StopwatchStartAdvice.class)
@After(adviceClass = StopwatchEndAdvice.class)
public void expireStickArticle(final HttpServletRequest request, final HttpServletResponse response, final HTTPRequestContext context)
throws Exception {
final String key = Symphonys.get("keyOfSymphony");
if (!key.equals(request.getParameter("key"))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
articleMgmtService.expireStick();
context.renderJSON().renderTrueResult();
}
}