Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
server-core/src/main/java/io/onedev/server/web/component/markdown/InsertUrlPanel.java
### 代码内容：
package io.onedev.server.web.component.markdown;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.activation.MimetypesFileTypeMap;
import javax.annotation.Nullable;

import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.wicket.Component;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.form.upload.FileUpload;
import org.apache.wicket.markup.html.image.ExternalImage;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.protocol.http.WebSession;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.util.lang.Bytes;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.unbescape.javascript.JavaScriptEscape;

import com.google.common.base.Preconditions;

import io.onedev.commons.utils.PathUtils;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.git.BlobIdent;
import io.onedev.server.git.BlobIdentFilter;
import io.onedev.server.git.exception.GitException;
import io.onedev.server.model.Project;
import io.onedev.server.util.UrlUtils;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.behavior.ReferenceInputBehavior;
import io.onedev.server.web.component.blob.folderpicker.BlobFolderPicker;
import io.onedev.server.web.component.blob.picker.BlobPicker;
import io.onedev.server.web.component.dropzonefield.DropzoneField;
import io.onedev.server.web.component.floating.FloatingPanel;
import io.onedev.server.web.component.link.DropdownLink;
import io.onedev.server.web.component.tabbable.AjaxActionTab;
import io.onedev.server.web.component.tabbable.Tab;
import io.onedev.server.web.component.tabbable.Tabbable;
import io.onedev.server.web.page.project.blob.ProjectBlobPage;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext;

@SuppressWarnings("serial")
abstract class InsertUrlPanel extends Panel {

	private static final MimetypesFileTypeMap MIME_TYPES = new MimetypesFileTypeMap();

	private static final MetaDataKey<String> ACTIVE_TAB = new MetaDataKey<String>(){};

	private static final MetaDataKey<String> UPLOAD_DIRECTORY = new MetaDataKey<String>(){};

	private static final MetaDataKey<HashSet<String>> FILE_PICKER_STATE = new MetaDataKey<HashSet<String>>(){};

	private static final MetaDataKey<HashSet<String>> FOLDER_PICKER_STATE = new MetaDataKey<HashSet<String>>(){};

	static final String TAB_INPUT_URL = "Input URL";

	static final String TAB_PICK_EXISTING = "Pick Existing";

	static final String TAB_UPLOAD = "Upload";

	private static final String CONTENT_ID = "content";

	private Collection<FileUpload> uploads;

	private String url;

	private String text;

	private String summaryCommitMessage;

	private String detailCommitMessage;

	private final MarkdownEditor markdownEditor;

	private final boolean isImage;

	public InsertUrlPanel(String id, MarkdownEditor markdownEditor, boolean isImage) {
		super(id);
		this.markdownEditor = markdownEditor;
		this.isImage = isImage;
	}

	private Component newInputUrlPanel() {
		Fragment fragment = new Fragment(CONTENT_ID, "inputUrlFrag", this) {

			@Override
			public void renderHead(IHeaderResponse response) {
				super.renderHead(response);
				String script = String.format("onedev.server.markdown.onInputUrlDomReady('%s');", getMarkupId());
				response.render(OnDomReadyHeaderItem.forScript(script));
			}

		};

		Form<?> form = new Form<Void>("form");
		form.add(new FencedFeedbackPanel("feedback", form));

		form.add(new Label("urlLabel", isImage?"Image URL":"Link URL"));
		form.add(new Label("urlHelp", isImage?"Absolute or relative url of the image":"Absolute or relative url of the link"));
		form.add(new TextField<String>("url", new PropertyModel<String>(this, "url")));

		form.add(new Label("textLabel", isImage?"Image Text": "Link Text"));
		form.add(new TextField<String>("text", new PropertyModel<String>(this, "text")));

		form.add(new AjaxButton("insert", form) {

			@Override
			protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
				super.onSubmit(target, form);
				if (StringUtils.isBlank(url)) {
					if (isImage)
						error("Image URL should be specified");
					else
						error("Link URL should be specified");
					target.add(fragment);
				} else {
					if (text == null)
						text = UrlUtils.describe(url);
					markdownEditor.insertUrl(target, isImage, url, text, null);
					onClose(target);
				}
			}

		});

		fragment.add(form);
		fragment.setOutputMarkupId(true);
		return fragment;
	}

	private ObjectId resolveCommitId(BlobRenderContext context) {
		/*
		 * We resolve revision to get latest commit id so that we can select to insert newly
		 * added/uploaded files while editing a markdown file
		 */
		String revision = context.getBlobIdent().revision;
		if (revision == null)
			revision = "master";
		try {
			return context.getProject().getRepository().resolve(revision);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private Set<BlobIdent> getPickerState(@Nullable ObjectId commitId, BlobIdent currentBlobIdent,
			@Nullable Set<String> expandedPaths) {
		Set<BlobIdent> pickerState = new HashSet<>();
		if (commitId != null) {
			if (expandedPaths != null) {
				for (String path: expandedPaths)
					pickerState.add(new BlobIdent(commitId.name(), path, FileMode.TREE.getBits()));
			}

			String parentPath;
			if (currentBlobIdent.isTree())
				parentPath = currentBlobIdent.path;
			else if (currentBlobIdent.path.contains("/"))
				parentPath = StringUtils.substringBeforeLast(currentBlobIdent.path, "/");
			else
				parentPath = null;

			while (parentPath != null) {
				pickerState.add(new BlobIdent(commitId.name(), parentPath, FileMode.TYPE_TREE));
				if (parentPath.contains("/"))
					parentPath = StringUtils.substringBeforeLast(parentPath, "/");
				else
					parentPath = null;
			}
		}
		return pickerState;
	}

	private Component newPickExistingPanel() {
		Fragment fragment;
		BlobRenderContext context = markdownEditor.getBlobRenderContext();
		if (context != null) {
			fragment = new Fragment(CONTENT_ID, "pickBlobFrag", this);
			BlobIdentFilter blobIdentFilter = new BlobIdentFilter() {

				@Override
				public boolean filter(BlobIdent blobIdent) {
					if (isImage) {
						if (blobIdent.isTree()) {
							return true;
						} else {
					        String mimetype= MIME_TYPES.getContentType(new File(blobIdent.path));
					        return mimetype.split("/")[0].equals("image");
						}
					} else {
						return true;
					}
				}

			};

			ObjectId commitId = resolveCommitId(context);

			Set<BlobIdent> filePickerState = getPickerState(commitId, context.getBlobIdent(),
					WebSession.get().getMetaData(FILE_PICKER_STATE));

			fragment.add(new BlobPicker("files", commitId) {

				@Override
				protected void onSelect(AjaxRequestTarget target, BlobIdent blobIdent) {
					blobIdent = new BlobIdent(context.getBlobIdent().revision, blobIdent.path, blobIdent.mode);
					String baseUrl = context.getDirectoryUrl();
					String referenceUrl = urlFor(ProjectBlobPage.class,
							ProjectBlobPage.paramsOf(context.getProject(), blobIdent)).toString();
					String relativized = PathUtils.relativize(baseUrl, referenceUrl);
					markdownEditor.insertUrl(target, isImage, relativized, UrlUtils.describe(blobIdent.getName()), null);
					onClose(target);
				}

				@Override
				protected BlobIdentFilter getBlobIdentFilter() {
					return blobIdentFilter;
				}

				@Override
				protected Project getProject() {
					return markdownEditor.getBlobRenderContext().getProject();
				}

				@Override
				protected void onStateChange() {
					HashSet<String> expandedPaths = new HashSet<>();
					for (BlobIdent blobIdent: filePickerState)
						expandedPaths.add(blobIdent.path);
					WebSession.get().setMetaData(FILE_PICKER_STATE, expandedPaths);
				}

				@Override
				protected Set<BlobIdent> getState() {
					return filePickerState;
				}

			});
		} else {
			AttachmentSupport attachmentSupport = Preconditions.checkNotNull(markdownEditor.getAttachmentSupport());
			if (isImage) {
				fragment = new Fragment(CONTENT_ID, "pickAttachedImageFrag", this);
				fragment.add(new ListView<String>("attachments", new LoadableDetachableModel<List<String>>() {

					@Override
					protected List<String> load() {
						List<String> attachmentNames = new ArrayList<>();
						for (String attachmentName: attachmentSupport.getAttachments()) {
							if (markdownEditor.isWebSafeImage(attachmentName))
								attachmentNames.add(attachmentName);
						}
						return attachmentNames;
					}

				}) {

					@Override
					protected void populateItem(final ListItem<String> item) {
						String attachmentName = item.getModelObject();
						String attachmentUrl = attachmentSupport.getAttachmentUrl(attachmentName);

						AjaxLink<Void> selectLink = new AjaxLink<Void>("select") {

							@Override
							public void onClick(AjaxRequestTarget target) {
								String displayName = UrlUtils.describe(attachmentName);
								markdownEditor.insertUrl(target, true, attachmentUrl, displayName, null);
								onClose(target);
							}

						};
						selectLink.add(new ExternalImage("image", StringEscapeUtils.escapeHtml4(attachmentUrl)));
						item.add(selectLink);

						item.add(new AjaxLink<Void>("delete") {

							@Override
							protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
								super.updateAjaxAttributes(attributes);
								attributes.getAjaxCallListeners().add(new ConfirmClickListener("Do you really want to delete '" + attachmentName + "'?"));
							}

							@Override
							protected void onConfigure() {
								super.onConfigure();
								setVisible(attachmentSupport.canDeleteAttachment());
							}

							@Override
							public void onClick(AjaxRequestTarget target) {
								attachmentSupport.deleteAttachemnt(attachmentName);
								target.add(fragment);
							}

						});
					}

				});

			} else {
				fragment = new Fragment(CONTENT_ID, "pickAttachedFileFrag", this);
				fragment.add(new ListView<String>("attachments", new LoadableDetachableModel<List<String>>() {

					@Override
					protected List<String> load() {
						return attachmentSupport.getAttachments();
					}

				}) {

					@Override
					protected void populateItem(final ListItem<String> item) {
						String attachmentName = item.getModelObject();
						String attachmentUrl = attachmentSupport.getAttachmentUrl(attachmentName);

						AjaxLink<Void> selectLink = new AjaxLink<Void>("select") {

							@Override
							public void onClick(AjaxRequestTarget target) {
								String displayName = UrlUtils.describe(attachmentName);
								markdownEditor.insertUrl(target, false, attachmentUrl, displayName, null);
								onClose(target);
							}

						};
						selectLink.add(new Label("file", StringEscapeUtils.escapeHtml4(attachmentName)));
						item.add(selectLink);

						item.add(new AjaxLink<Void>("delete") {

							@Override
							protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
								super.updateAjaxAttributes(attributes);
								attributes.getAjaxCallListeners().add(new ConfirmClickListener("Do you really want to delete '" + attachmentName + "'?"));
							}

							@Override
							public void onClick(AjaxRequestTarget target) {
								attachmentSupport.deleteAttachemnt(attachmentName);
								target.add(fragment);
							}

						});
					}

				});
			}
		}
		fragment.setOutputMarkupId(true);
		return fragment;
	}

	private Component newUploadPanel() {
		Fragment fragment;

		IModel<Collection<FileUpload>> model = new PropertyModel<Collection<FileUpload>>(this, "uploads");
		String acceptedFiles;
		if (isImage)
			acceptedFiles = "image/*";
		else
			acceptedFiles = null;

		AttachmentSupport attachmentSupport = markdownEditor.getAttachmentSupport();
		if (attachmentSupport != null) {
			fragment = new Fragment(CONTENT_ID, "uploadAttachmentFrag", this);

			Form<?> form = new Form<Void>("form") {

				@Override
				protected void onSubmit() {
					super.onSubmit();

					AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class);
					String attachmentName;
					FileUpload upload = uploads.iterator().next();
					try (InputStream is = upload.getInputStream()) {
						attachmentName = attachmentSupport.saveAttachment(upload.getClientFileName(), is);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
					markdownEditor.insertUrl(target, isImage,
							attachmentSupport.getAttachmentUrl(attachmentName), UrlUtils.describe(attachmentName), null);
					onClose(target);
				}

				@Override
				protected void onFileUploadException(FileUploadException e, Map<String, Object> model) {
					throw new RuntimeException(e);
				}

			};
			form.setMaxSize(Bytes.bytes(attachmentSupport.getAttachmentMaxSize()));
			form.setMultiPart(true);
			form.add(new FencedFeedbackPanel("feedback", form));

			int maxFilesize = (int) (attachmentSupport.getAttachmentMaxSize()/1024/1024);
			if (maxFilesize <= 0)
				maxFilesize = 1;
			form.add(new DropzoneField("file", model, acceptedFiles, 1, maxFilesize)
					.setRequired(true).setLabel(Model.of("Attachment")));

			form.add(new AjaxButton("insert"){});

			fragment.add(form);
		} else {
			fragment = new Fragment(CONTENT_ID, "uploadBlobFrag", this);
			Form<?> form = new Form<Void>("form");
			form.setMultiPart(true);
			form.setFileMaxSize(Bytes.megabytes(Project.MAX_UPLOAD_SIZE));
			add(form);

			FencedFeedbackPanel feedback = new FencedFeedbackPanel("feedback", form);
			feedback.setOutputMarkupPlaceholderTag(true);
			form.add(feedback);

			form.add(new DropzoneField("file", model, acceptedFiles, 1, Project.MAX_UPLOAD_SIZE)
					.setRequired(true).setLabel(Model.of("Attachment")));

			form.add(new TextField<String>("directory", new IModel<String>() {

				@Override
				public void detach() {
				}

				@Override
				public String getObject() {
					return WebSession.get().getMetaData(UPLOAD_DIRECTORY);
				}

				@Override
				public void setObject(String object) {
					WebSession.get().setMetaData(UPLOAD_DIRECTORY, object);
				}

			}));

			BlobRenderContext context = Preconditions.checkNotNull(markdownEditor.getBlobRenderContext());
			ObjectId commitId = resolveCommitId(context);
			Set<BlobIdent> folderPickerState = getPickerState(commitId, context.getBlobIdent(),
					WebSession.get().getMetaData(FOLDER_PICKER_STATE));

			form.add(new DropdownLink("select") {

				@Override
				protected Component newContent(String id, FloatingPanel dropdown) {
					return new BlobFolderPicker(id, commitId) {

						@Override
						protected void onSelect(AjaxRequestTarget target, BlobIdent blobIdent) {
							dropdown.close();

							String relativePath = PathUtils.relativize(context.getDirectory(), blobIdent.path);
							String script = String.format("$('form.upload-blob .directory input').val('%s');",
									JavaScriptEscape.escapeJavaScript(relativePath));
							target.appendJavaScript(script);
						}

						@Override
						protected Project getProject() {
							return markdownEditor.getBlobRenderContext().getProject();
						}

						@Override
						protected void onStateChange() {
							HashSet<String> expandedPaths = new HashSet<>();
							for (BlobIdent blobIdent: folderPickerState)
								expandedPaths.add(blobIdent.path);
							WebSession.get().setMetaData(FOLDER_PICKER_STATE, expandedPaths);
						}

						@Override
						protected Set<BlobIdent> getState() {
							return folderPickerState;
						}

					};
				}

			});

			ReferenceInputBehavior behavior = new ReferenceInputBehavior(true) {

				@Override
				protected Project getProject() {
					return markdownEditor.getBlobRenderContext().getProject();
				}

			};
			form.add(new TextField<String>("summaryCommitMessage",
					new PropertyModel<String>(this, "summaryCommitMessage")).add(behavior));

			behavior = new ReferenceInputBehavior(true) {

				@Override
				protected Project getProject() {
					return markdownEditor.getBlobRenderContext().getProject();
				}

			};
			form.add(new TextArea<String>("detailCommitMessage",
					new PropertyModel<String>(this, "detailCommitMessage")).add(behavior));

			form.add(new AjaxButton("insert") {

				@Override
				protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
					super.onSubmit(target, form);

					BlobRenderContext context = Preconditions.checkNotNull(markdownEditor.getBlobRenderContext());
					String commitMessage = summaryCommitMessage;
					if (StringUtils.isBlank(commitMessage))
						commitMessage = "Add files via upload";

					if (StringUtils.isNotBlank(detailCommitMessage))
						commitMessage += "\n\n" + detailCommitMessage;

					try {
						String directory = WebSession.get().getMetaData(UPLOAD_DIRECTORY);
						context.onCommitted(null, context.uploadFiles(uploads, directory, commitMessage), null);

						String fileName = uploads.iterator().next().getClientFileName();
						String url;
						if (directory != null)
							url = directory + "/" + UrlUtils.encodePath(fileName);
						else
							url = UrlUtils.encodePath(fileName);
						markdownEditor.insertUrl(target, isImage, url, UrlUtils.describe(fileName), null);
						onClose(target);
					} catch (GitException e) {
						form.error(e.getMessage());
						target.add(feedback);
					}
				}

				@Override
				protected void onError(AjaxRequestTarget target, Form<?> form) {
					super.onError(target, form);
					target.add(feedback);
				}

			});

			fragment.add(form);
		}

		fragment.setOutputMarkupId(true);
		return fragment;
	}

	@Override
	protected void onInitialize() {
		super.onInitialize();

		add(new Label("title", isImage?"Insert Image":"Insert Link"));

		if (markdownEditor.getBlobRenderContext() == null && markdownEditor.getAttachmentSupport() == null) {
			add(newInputUrlPanel());
		} else {
			Fragment fragment = new Fragment(CONTENT_ID, "tabbedFrag", this);
			List<Tab> tabs = new ArrayList<>();
			AjaxActionTab inputUrlTab = new AjaxActionTab(Model.of(TAB_INPUT_URL)) {

				@Override
				protected void onSelect(AjaxRequestTarget target, Component tabLink) {
					Component content = newInputUrlPanel();
					target.add(content);
					fragment.replace(content);
					WebSession.get().setMetaData(ACTIVE_TAB, TAB_INPUT_URL);
				}

			};
			tabs.add(inputUrlTab);

			AjaxActionTab pickExistingTab = new AjaxActionTab(Model.of(TAB_PICK_EXISTING)) {

				@Override
				protected void onSelect(AjaxRequestTarget target, Component tabLink) {
					Component content = newPickExistingPanel();
					target.add(content);
					fragment.replace(content);
					WebSession.get().setMetaData(ACTIVE_TAB, TAB_PICK_EXISTING);
				}

			};
			tabs.add(pickExistingTab);

			AjaxActionTab uploadTab = new AjaxActionTab(Model.of(TAB_UPLOAD)) {

				@Override
				protected void onSelect(AjaxRequestTarget target, Component tabLink) {
					Component content = newUploadPanel();
					target.add(content);
					fragment.replace(content);
					WebSession.get().setMetaData(ACTIVE_TAB, TAB_UPLOAD);
				}

			};
			tabs.add(uploadTab);

			fragment.add(new Tabbable("tabs", tabs));

			inputUrlTab.setSelected(false);
			String activeTab = WebSession.get().getMetaData(ACTIVE_TAB);
			if (TAB_PICK_EXISTING.equals(activeTab)) {
				pickExistingTab.setSelected(true);
				fragment.add(newPickExistingPanel());
			} else if (TAB_UPLOAD.equals(activeTab)) {
				uploadTab.setSelected(true);
				fragment.add(newUploadPanel());
			} else {
				inputUrlTab.setSelected(true);
				fragment.add(newInputUrlPanel());
			}
			add(fragment);
		}

		add(new AjaxLink<Void>("close") {

			@Override
			public void onClick(AjaxRequestTarget target) {
				onClose(target);
			}

		});

	}

	protected abstract void onClose(AjaxRequestTarget target);
}

## 内容2
### 文件路径：
server-core/src/main/java/io/onedev/server/web/component/markdown/MarkdownEditor.java
### 代码内容：
package io.onedev.server.web.component.markdown;

import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;

import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxChannel;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.head.OnLoadHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.FormComponentPanel;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.unbescape.javascript.JavaScriptEscape;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;

import io.onedev.commons.launcher.loader.AppLoader;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.ProjectManager;
import io.onedev.server.model.Build;
import io.onedev.server.model.Issue;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.User;
import io.onedev.server.util.markdown.MarkdownManager;
import io.onedev.server.util.validation.ProjectNameValidator;
import io.onedev.server.web.avatar.AvatarManager;
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.component.floating.FloatingPanel;
import io.onedev.server.web.component.link.DropdownLink;
import io.onedev.server.web.component.markdown.emoji.EmojiOnes;
import io.onedev.server.web.component.modal.ModalPanel;
import io.onedev.server.web.page.project.ProjectPage;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext;

@SuppressWarnings("serial")
public class MarkdownEditor extends FormComponentPanel<String> {

	protected static final int ATWHO_LIMIT = 10;

	private static final Logger logger = LoggerFactory.getLogger(MarkdownEditor.class);

	private final boolean compactMode;

	private final boolean initialSplit;

	private final BlobRenderContext blobRenderContext;

	private WebMarkupContainer container;

	private TextArea<String> input;

	private AbstractPostAjaxBehavior actionBehavior;

	private AbstractPostAjaxBehavior attachmentUploadBehavior;

	/**
	 * @param id
	 * 			component id of the editor
	 * @param model
	 * 			markdown model of the editor
	 * @param compactMode
	 * 			editor in compact mode occupies horizontal space and is suitable
	 * 			to be used in places such as comment aside the code
	 */
	public MarkdownEditor(String id, IModel<String> model, boolean compactMode,
			@Nullable BlobRenderContext blobRenderContext) {
		super(id, model);
		this.compactMode = compactMode;

		String cookieKey;
		if (compactMode)
			cookieKey = "markdownEditor.compactMode.split";
		else
			cookieKey = "markdownEditor.normalMode.split";

		WebRequest request = (WebRequest) RequestCycle.get().getRequest();
		Cookie cookie = request.getCookie(cookieKey);
		initialSplit = cookie!=null && "true".equals(cookie.getValue());

		this.blobRenderContext = blobRenderContext;
	}

	@Override
	protected void onModelChanged() {
		super.onModelChanged();
		input.setModelObject(getModelObject());
	}

	public void clearMarkdown() {
		setModelObject("");
		input.setConvertedInput(null);
	}

	private String renderInput(String input) {
		if (StringUtils.isNotBlank(input)) {
			// Normalize line breaks to make source position tracking information comparable
			// to textarea caret position when sync edit/preview scroll bar
			input = StringUtils.replace(input, "\r\n", "\n");
			return renderMarkdown(input);
		} else {
			return "<div class='message'>Nothing to preview</div>";
		}
	}

	protected String renderMarkdown(String markdown) {
		Project project ;
		if (getPage() instanceof ProjectPage)
			project = ((ProjectPage) getPage()).getProject();
		else
			project = null;

		MarkdownManager manager = OneDev.getInstance(MarkdownManager.class);
		return manager.process(manager.render(markdown), project, blobRenderContext);
	}

	@Override
	protected void onInitialize() {
		super.onInitialize();

		container = new WebMarkupContainer("container");
		container.setOutputMarkupId(true);

		add(container);

		WebMarkupContainer editLink = new WebMarkupContainer("editLink");
		WebMarkupContainer splitLink = new WebMarkupContainer("splitLink");
		WebMarkupContainer preview = new WebMarkupContainer("preview");
		WebMarkupContainer edit = new WebMarkupContainer("edit");
		container.add(editLink);
		container.add(splitLink);
		container.add(preview);
		container.add(edit);

		container.add(AttributeAppender.append("class", compactMode?"compact-mode":"normal-mode"));

		container.add(new DropdownLink("doReference") {

			@Override
			protected Component newContent(String id, FloatingPanel dropdown) {
				return new Fragment(id, "referenceMenuFrag", MarkdownEditor.this) {

					@Override
					public void renderHead(IHeaderResponse response) {
						super.renderHead(response);
						String script = String.format("onedev.server.markdown.setupActionMenu($('#%s'), $('#%s'));",
								container.getMarkupId(), getMarkupId());
						response.render(OnDomReadyHeaderItem.forScript(script));
					}

				}.setOutputMarkupId(true);
			}

		}.setVisible(getReferenceSupport() != null));

		container.add(new DropdownLink("actionMenuTrigger") {

			@Override
			protected Component newContent(String id, FloatingPanel dropdown) {
				return new Fragment(id, "actionMenuFrag", MarkdownEditor.this) {

					@Override
					protected void onInitialize() {
						super.onInitialize();
						add(new WebMarkupContainer("doMention").setVisible(getUserMentionSupport() != null));

						if (getReferenceSupport() != null)
							add(new Fragment("doReference", "referenceMenuFrag", MarkdownEditor.this));
						else
							add(new WebMarkupContainer("doReference").setVisible(false));
					}

					@Override
					public void renderHead(IHeaderResponse response) {
						super.renderHead(response);
						String script = String.format("onedev.server.markdown.setupActionMenu($('#%s'), $('#%s'));",
								container.getMarkupId(), getMarkupId());
						response.render(OnDomReadyHeaderItem.forScript(script));
					}

				}.setOutputMarkupId(true);
			}

		});

		container.add(new WebMarkupContainer("doMention").setVisible(getUserMentionSupport() != null));

		edit.add(input = new TextArea<String>("input", Model.of(getModelObject())));
		for (AttributeModifier modifier: getInputModifiers())
			input.add(modifier);

		if (initialSplit) {
			container.add(AttributeAppender.append("class", "split-mode"));
			preview.add(new Label("rendered", new LoadableDetachableModel<String>() {

				@Override
				protected String load() {
					return renderInput(input.getConvertedInput());
				}

			}) {

				@Override
				public void renderHead(IHeaderResponse response) {
					super.renderHead(response);
					String script = String.format(
							"onedev.server.markdown.initRendered($('#%s>.body>.preview>.markdown-rendered'));",
							container.getMarkupId());
					response.render(OnDomReadyHeaderItem.forScript(script));
				}

			}.setEscapeModelStrings(false));
			splitLink.add(AttributeAppender.append("class", "active"));
		} else {
			container.add(AttributeAppender.append("class", "edit-mode"));
			preview.add(new WebMarkupContainer("rendered"));
			editLink.add(AttributeAppender.append("class", "active"));
		}

		container.add(new WebMarkupContainer("canAttachFile").setVisible(getAttachmentSupport()!=null));

		container.add(actionBehavior = new AbstractPostAjaxBehavior() {

			@Override
			protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
				super.updateAjaxAttributes(attributes);
				attributes.setChannel(new AjaxChannel("markdown-preview", AjaxChannel.Type.DROP));
			}

			@Override
			protected void respond(AjaxRequestTarget target) {
				IRequestParameters params = RequestCycle.get().getRequest().getPostParameters();
				String action = params.getParameterValue("action").toString("");
				switch (action) {
				case "render":
					String markdown = params.getParameterValue("param1").toString();
					String rendered = renderInput(markdown);
					String script = String.format("onedev.server.markdown.onRendered('%s', '%s');",
							container.getMarkupId(), JavaScriptEscape.escapeJavaScript(rendered));
					target.appendJavaScript(script);
					break;
				case "emojiQuery":
					List<String> emojiNames = new ArrayList<>();
					String emojiQuery = params.getParameterValue("param1").toOptionalString();
					if (StringUtils.isNotBlank(emojiQuery)) {
						emojiQuery = emojiQuery.toLowerCase();
						for (String emojiName: EmojiOnes.getInstance().all().keySet()) {
							if (emojiName.toLowerCase().contains(emojiQuery))
								emojiNames.add(emojiName);
						}
						emojiNames.sort((name1, name2) -> name1.length() - name2.length());
					} else {
						emojiNames.add("smile");
						emojiNames.add("worried");
						emojiNames.add("blush");
						emojiNames.add("+1");
						emojiNames.add("-1");
					}

					List<Map<String, String>> emojis = new ArrayList<>();
					for (String emojiName: emojiNames) {
						if (emojis.size() < ATWHO_LIMIT) {
							String emojiCode = EmojiOnes.getInstance().all().get(emojiName);
							CharSequence url = RequestCycle.get().urlFor(new PackageResourceReference(
									EmojiOnes.class, "icon/" + emojiCode + ".png"), new PageParameters());
							Map<String, String> emoji = new HashMap<>();
							emoji.put("name", emojiName);
							emoji.put("url", url.toString());
							emojis.add(emoji);
						}
					}
					String json;
					try {
						json = AppLoader.getInstance(ObjectMapper.class).writeValueAsString(emojis);
					} catch (JsonProcessingException e) {
						throw new RuntimeException(e);
					}
					script = String.format("$('#%s').data('atWhoEmojiRenderCallback')(%s);", container.getMarkupId(), json);
					target.appendJavaScript(script);
					break;
				case "loadEmojis":
					emojis = new ArrayList<>();
					String urlPattern =  RequestCycle.get().urlFor(new PackageResourceReference(EmojiOnes.class,
					        "icon/FILENAME.png"), new PageParameters()).toString();

					for (Map.Entry<String, String> entry: EmojiOnes.getInstance().all().entrySet()) {
						Map<String, String> emoji = new HashMap<>();
						emoji.put("name", entry.getKey());
						emoji.put("url", urlPattern.replace("FILENAME", entry.getValue()));
						emojis.add(emoji);
					}

					try {
						json = AppLoader.getInstance(ObjectMapper.class).writeValueAsString(emojis);
					} catch (JsonProcessingException e) {
						throw new RuntimeException(e);
					}

					script = String.format("onedev.server.markdown.onEmojisLoaded('%s', %s);", container.getMarkupId(), json);
					target.appendJavaScript(script);
					break;
				case "userQuery":
					String userQuery = params.getParameterValue("param1").toOptionalString();

					AvatarManager avatarManager = OneDev.getInstance(AvatarManager.class);
					List<Map<String, String>> userList = new ArrayList<>();
					for (User user: getUserMentionSupport().findUsers(userQuery, ATWHO_LIMIT)) {
						Map<String, String> userMap = new HashMap<>();
						userMap.put("name", user.getName());
						if (user.getFullName() != null)
							userMap.put("fullName", user.getFullName());
						String noSpaceName = StringUtils.deleteWhitespace(user.getName());
						if (user.getFullName() != null) {
							String noSpaceFullName = StringUtils.deleteWhitespace(user.getFullName());
							userMap.put("searchKey", noSpaceName + " " + noSpaceFullName);
						} else {
							userMap.put("searchKey", noSpaceName);
						}
						String avatarUrl = avatarManager.getAvatarUrl(user);
						userMap.put("avatarUrl", avatarUrl);
						userList.add(userMap);
					}

					try {
						json = OneDev.getInstance(ObjectMapper.class).writeValueAsString(userList);
					} catch (JsonProcessingException e) {
						throw new RuntimeException(e);
					}
					script = String.format("$('#%s').data('atWhoUserRenderCallback')(%s);", container.getMarkupId(), json);
					target.appendJavaScript(script);
					break;
				case "referenceQuery":
					String referenceQuery = params.getParameterValue("param1").toOptionalString();
					String referenceQueryType = params.getParameterValue("param2").toOptionalString();
					String referenceProjectName = params.getParameterValue("param3").toOptionalString();
					List<Map<String, String>> referenceList = new ArrayList<>();
					Project referenceProject;
					if (StringUtils.isNotBlank(referenceProjectName))
						referenceProject = OneDev.getInstance(ProjectManager.class).find(referenceProjectName);
					else
						referenceProject = null;
					if (referenceProject != null || StringUtils.isBlank(referenceProjectName)) {
						if ("issue".equals(referenceQueryType)) {
							for (Issue issue: getReferenceSupport().findIssues(referenceProject, referenceQuery, ATWHO_LIMIT)) {
								Map<String, String> referenceMap = new HashMap<>();
								referenceMap.put("referenceType", "issue");
								referenceMap.put("referenceNumber", String.valueOf(issue.getNumber()));
								referenceMap.put("referenceTitle", issue.getTitle());
								referenceMap.put("searchKey", issue.getNumber() + " " + StringUtils.deleteWhitespace(issue.getTitle()));
								referenceList.add(referenceMap);
							}
						} else if ("pullrequest".equals(referenceQueryType)) {
							for (PullRequest request: getReferenceSupport().findPullRequests(referenceProject, referenceQuery, ATWHO_LIMIT)) {
								Map<String, String> referenceMap = new HashMap<>();
								referenceMap.put("referenceType", "pull request");
								referenceMap.put("referenceNumber", String.valueOf(request.getNumber()));
								referenceMap.put("referenceTitle", request.getTitle());
								referenceMap.put("searchKey", request.getNumber() + " " + StringUtils.deleteWhitespace(request.getTitle()));
								referenceList.add(referenceMap);
							}
						} else if ("build".equals(referenceQueryType)) {
							for (Build build: getReferenceSupport().findBuilds(referenceProject, referenceQuery, ATWHO_LIMIT)) {
								Map<String, String> referenceMap = new HashMap<>();
								referenceMap.put("referenceType", "build");
								referenceMap.put("referenceNumber", String.valueOf(build.getNumber()));

								String title;
								if (build.getVersion() != null)
									title = "(" + build.getVersion() + ") " + build.getJobName();
								else
									title = build.getJobName();
								referenceMap.put("referenceTitle", title);
								referenceMap.put("searchKey", build.getNumber() + " " + StringUtils.deleteWhitespace(title));
								referenceList.add(referenceMap);
							}
						}
					}

					try {
						json = OneDev.getInstance(ObjectMapper.class).writeValueAsString(referenceList);
					} catch (JsonProcessingException e) {
						throw new RuntimeException(e);
					}
					script = String.format("$('#%s').data('atWhoReferenceRenderCallback')(%s);", container.getMarkupId(), json);
					target.appendJavaScript(script);
					break;
				case "selectImage":
				case "selectLink":
					new ModalPanel(target) {

						@Override
						protected Component newContent(String id) {
							return new InsertUrlPanel(id, MarkdownEditor.this, action.equals("selectImage")) {

								@Override
								protected void onClose(AjaxRequestTarget target) {
									close();
								}

							};
						}

						@Override
						protected void onClosed() {
							super.onClosed();
							AjaxRequestTarget target =
									Preconditions.checkNotNull(RequestCycle.get().find(AjaxRequestTarget.class));
							target.appendJavaScript(String.format("$('#%s textarea').focus();", container.getMarkupId()));
						}

					};
					break;
				case "insertUrl":
					String name;
					try {
						name = URLDecoder.decode(params.getParameterValue("param1").toString(), StandardCharsets.UTF_8.name());
					} catch (UnsupportedEncodingException e) {
						throw new RuntimeException(e);
					}
					String replaceMessage = params.getParameterValue("param2").toString();
					String url = getAttachmentSupport().getAttachmentUrl(name);
					insertUrl(target, isWebSafeImage(name), url, name, replaceMessage);

					break;
				default:
					throw new IllegalStateException("Unknown action: " + action);
				}
			}

		});

		container.add(attachmentUploadBehavior = new AbstractPostAjaxBehavior() {

			@Override
			protected void respond(AjaxRequestTarget target) {
				Preconditions.checkNotNull(getAttachmentSupport(), "Unexpected attachment upload request");
				HttpServletRequest request = (HttpServletRequest) RequestCycle.get().getRequest().getContainerRequest();
				HttpServletResponse response = (HttpServletResponse) RequestCycle.get().getResponse().getContainerResponse();
				try {
					String fileName = URLDecoder.decode(request.getHeader("File-Name"), StandardCharsets.UTF_8.name());
					String attachmentName = getAttachmentSupport().saveAttachment(fileName, request.getInputStream());
					response.getWriter().print(URLEncoder.encode(attachmentName, StandardCharsets.UTF_8.name()));
					response.setStatus(HttpServletResponse.SC_OK);
				} catch (Exception e) {
					logger.error("Error uploading attachment.", e);
					try {
						if (e.getMessage() != null)
							response.getWriter().print(e.getMessage());
						else
							response.getWriter().print("Internal server error");
					} catch (IOException e2) {
						throw new RuntimeException(e2);
					}
					response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				}
			}

		});
	}

	@Override
	protected void onComponentTag(ComponentTag tag) {
		super.onComponentTag(tag);
	}

	@Override
	public void convertInput() {
		setConvertedInput(input.getConvertedInput());
	}

	@Override
	public void renderHead(IHeaderResponse response) {
		super.renderHead(response);
		response.render(JavaScriptHeaderItem.forReference(new MarkdownResourceReference()));

		String actionCallback = actionBehavior.getCallbackFunction(explicit("action"), explicit("param1"), explicit("param2"),
				explicit("param3")).toString();
		String attachmentUploadUrl = attachmentUploadBehavior.getCallbackUrl().toString();

		String autosaveKey = getAutosaveKey();
		if (autosaveKey != null)
			autosaveKey = "'" + JavaScriptEscape.escapeJavaScript(autosaveKey) + "'";
		else
			autosaveKey = "undefined";

		String script = String.format("onedev.server.markdown.onDomReady('%s', %s, %d, %s, %d, %b, %b, '%s', %s);",
				container.getMarkupId(),
				actionCallback,
				ATWHO_LIMIT,
				getAttachmentSupport()!=null? "'" + attachmentUploadUrl + "'": "undefined",
				getAttachmentSupport()!=null? getAttachmentSupport().getAttachmentMaxSize(): 0,
				getUserMentionSupport() != null,
				getReferenceSupport() != null,
				JavaScriptEscape.escapeJavaScript(ProjectNameValidator.PATTERN.pattern()),
				autosaveKey);
		response.render(OnDomReadyHeaderItem.forScript(script));

		script = String.format("onedev.server.markdown.onLoad('%s');", container.getMarkupId());
		response.render(OnLoadHeaderItem.forScript(script));
	}

	public void insertUrl(AjaxRequestTarget target, boolean isImage, String url,
			String name, @Nullable String replaceMessage) {
		String script = String.format("onedev.server.markdown.insertUrl('%s', %s, '%s', '%s', %s);",
				container.getMarkupId(), isImage, StringEscapeUtils.escapeEcmaScript(url),
				StringEscapeUtils.escapeEcmaScript(name),
				replaceMessage!=null?"'"+replaceMessage+"'":"undefined");
		target.appendJavaScript(script);
	}

	public boolean isWebSafeImage(String fileName) {
		fileName = fileName.toLowerCase();
		return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")
				|| fileName.endsWith(".gif") || fileName.endsWith(".png");
	}

	@Nullable
	protected AttachmentSupport getAttachmentSupport() {
		return null;
	}

	@Nullable
	protected UserMentionSupport getUserMentionSupport() {
		return null;
	}

	@Nullable
	protected AtWhoReferenceSupport getReferenceSupport() {
		return null;
	}

	protected List<AttributeModifier> getInputModifiers() {
		return new ArrayList<>();
	}

	@Nullable
	protected String getAutosaveKey() {
		return null;
	}

	@Nullable
	public BlobRenderContext getBlobRenderContext() {
		return blobRenderContext;
	}

	static class ReferencedEntity implements Serializable {
		String entityType;

		String entityTitle;

		String entityNumber;

		String searchKey;
	}

}

## 内容3
### 文件路径：
server-core/src/main/java/io/onedev/server/web/page/project/blob/ProjectBlobPage.java
### 代码内容：
package io.onedev.server.web.page.project.blob;

import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;

import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nullable;

import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.upload.FileUpload;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;

import io.onedev.commons.launcher.loader.ListenerRegistry;
import io.onedev.commons.utils.PlanarRange;
import io.onedev.server.OneDev;
import io.onedev.server.buildspec.BuildSpec;
import io.onedev.server.entitymanager.CodeCommentManager;
import io.onedev.server.entitymanager.ProjectManager;
import io.onedev.server.entitymanager.PullRequestManager;
import io.onedev.server.entitymanager.PullRequestUpdateManager;
import io.onedev.server.event.RefUpdated;
import io.onedev.server.git.BlobContent;
import io.onedev.server.git.BlobEdits;
import io.onedev.server.git.BlobIdent;
import io.onedev.server.git.GitUtils;
import io.onedev.server.git.exception.BlobEditException;
import io.onedev.server.git.exception.NotTreeException;
import io.onedev.server.git.exception.ObjectAlreadyExistsException;
import io.onedev.server.git.exception.ObsoleteCommitException;
import io.onedev.server.model.CodeComment;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.User;
import io.onedev.server.persistence.SessionManager;
import io.onedev.server.persistence.TransactionManager;
import io.onedev.server.search.code.CommitIndexed;
import io.onedev.server.search.code.IndexManager;
import io.onedev.server.search.code.SearchManager;
import io.onedev.server.search.code.hit.QueryHit;
import io.onedev.server.search.code.query.BlobQuery;
import io.onedev.server.search.code.query.TextQuery;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.script.identity.JobIdentity;
import io.onedev.server.util.script.identity.ScriptIdentity;
import io.onedev.server.util.script.identity.ScriptIdentityAware;
import io.onedev.server.web.PrioritizedComponentRenderer;
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.behavior.WebSocketObserver;
import io.onedev.server.web.component.commit.status.CommitStatusPanel;
import io.onedev.server.web.component.floating.FloatingPanel;
import io.onedev.server.web.component.link.DropdownLink;
import io.onedev.server.web.component.link.ViewStateAwareAjaxLink;
import io.onedev.server.web.component.link.ViewStateAwarePageLink;
import io.onedev.server.web.component.menu.MenuItem;
import io.onedev.server.web.component.menu.MenuLink;
import io.onedev.server.web.component.modal.ModalLink;
import io.onedev.server.web.component.modal.ModalPanel;
import io.onedev.server.web.component.revisionpicker.RevisionPicker;
import io.onedev.server.web.page.project.ProjectPage;
import io.onedev.server.web.page.project.blob.navigator.BlobNavigator;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext;
import io.onedev.server.web.page.project.blob.render.BlobRendererContribution;
import io.onedev.server.web.page.project.blob.render.renderers.source.SourceRendererProvider;
import io.onedev.server.web.page.project.blob.render.view.Positionable;
import io.onedev.server.web.page.project.blob.search.SearchMenuContributor;
import io.onedev.server.web.page.project.blob.search.advanced.AdvancedSearchPanel;
import io.onedev.server.web.page.project.blob.search.quick.QuickSearchPanel;
import io.onedev.server.web.page.project.blob.search.result.SearchResultPanel;
import io.onedev.server.web.page.project.commits.ProjectCommitsPage;
import io.onedev.server.web.resource.RawBlobResourceReference;
import io.onedev.server.web.util.EditParamsAware;
import io.onedev.server.web.websocket.WebSocketManager;

@SuppressWarnings("serial")
public class ProjectBlobPage extends ProjectPage implements BlobRenderContext, ScriptIdentityAware, EditParamsAware {

	private static final String PARAM_REVISION = "revision";

	private static final String PARAM_PATH = "path";

	private static final String PARAM_INITIAL_NEW_PATH = "initial-new-path";

	private static final String PARAM_REQUEST = "request";

	private static final String PARAM_COMMENT = "comment";

	private static final String PARAM_MODE = "mode";

	private static final String PARAM_VIEW_PLAIN = "view-plain";

	private static final String PARAM_URL_BEFORE_EDIT = "url-before-edit";

	private static final String PARAM_URL_AFTER_EDIT = "url-after-edit";

	private static final String PARAM_QUERY = "query";

	private static final String PARAM_POSITION = "position";

	private static final String PARAM_RAW = "raw";

	private static final String REVISION_PICKER_ID = "revisionPicker";

	private static final String BLOB_NAVIGATOR_ID = "blobNavigator";

	private static final String BLOB_CONTENT_ID = "blobContent";

	private static final Logger logger = LoggerFactory.getLogger(ProjectBlobPage.class);

	private State state = new State();

	private ObjectId resolvedRevision;

	private Component revisionIndexing;

	private WebMarkupContainer searchResult;

	private AbstractPostAjaxBehavior ajaxBehavior;

	public ProjectBlobPage(PageParameters params) {
		super(params);

		List<String> revisionAndPathSegments = new ArrayList<>();
		String segment = params.get(PARAM_REVISION).toString();
		if (segment != null && segment.length() != 0)
			revisionAndPathSegments.add(segment);
		segment = params.get(PARAM_PATH).toString();
		if (segment != null && segment.length() != 0)
			revisionAndPathSegments.add(segment);

		for (int i=0; i<params.getIndexedCount(); i++) {
			segment = params.get(i).toString();
			if (segment.length() != 0)
				revisionAndPathSegments.add(segment);
		}

		BlobIdent blobIdent = new BlobIdent(getProject(), revisionAndPathSegments);
		state = new State(blobIdent);

		String modeStr = params.get(PARAM_MODE).toString();
		if (modeStr != null)
			state.mode = Mode.valueOf(modeStr.toUpperCase());

		String viewPlain = params.get(PARAM_VIEW_PLAIN).toString();
		state.viewPlain = "true".equals(viewPlain);

		state.urlBeforeEdit = params.get(PARAM_URL_BEFORE_EDIT).toString();
		state.urlAfterEdit = params.get(PARAM_URL_AFTER_EDIT).toString();

		if (state.blobIdent.revision != null)
			resolvedRevision = getProject().getRevCommit(state.blobIdent.revision, true).copy();

		state.position = params.get(PARAM_POSITION).toString();

		state.requestId = params.get(PARAM_REQUEST).toOptionalLong();

		state.commentId = params.get(PARAM_COMMENT).toOptionalLong();

		state.query = params.get(PARAM_QUERY).toString();

		state.initialNewPath = params.get(PARAM_INITIAL_NEW_PATH).toString();

		if (state.mode == Mode.ADD || state.mode == Mode.EDIT || state.mode == Mode.DELETE) {
			if (!isOnBranch())
				throw new IllegalArgumentException("Files can only be edited on branch");

			String path = state.blobIdent.path;
			if (path != null && state.blobIdent.isTree())
				path += "/";
			if (!SecurityUtils.canModify(getProject(), state.blobIdent.revision, path))
				unauthorized();
		}

		if (params.get(PARAM_RAW).toBoolean(false)) {
			RequestCycle.get().scheduleRequestHandlerAfterCurrent(
					new ResourceReferenceRequestHandler(new RawBlobResourceReference(), getPageParameters()));
		}
	}

	@Override
	protected void onInitialize() {
		super.onInitialize();

		newRevisionPicker(null);
		newCommitStatus(null);
		newBlobNavigator(null);
		newBlobOperations(null);

		add(revisionIndexing = new WebMarkupContainer("revisionIndexing") {

			@Override
			protected void onConfigure() {
				super.onConfigure();

				if (resolvedRevision != null) {
					RevCommit commit = getProject().getRevCommit(resolvedRevision, true);
					IndexManager indexManager = OneDev.getInstance(IndexManager.class);
					if (!indexManager.isIndexed(getProject(), commit)) {
						OneDev.getInstance(IndexManager.class).indexAsync(getProject(), commit);
						setVisible(true);
					} else {
						setVisible(false);
					}
				} else {
					setVisible(false);
				}
			}

		}.setOutputMarkupPlaceholderTag(true));

		revisionIndexing.add(new WebSocketObserver() {

			@Override
			public void onObservableChanged(IPartialPageRequestHandler handler) {
				handler.add(revisionIndexing);
				resizeWindow(handler);
			}

			@Override
			public Collection<String> getObservables() {
				Set<String> observables = new HashSet<>();
				if (resolvedRevision != null)
					observables.add(CommitIndexed.getWebSocketObservable(getProject().getRevCommit(resolvedRevision, true).name()));
				return observables;
			}

		});

		newBuildSupportNote(null);
		newBlobContent(null);

		add(searchResult = new WebMarkupContainer("searchResult"));

		List<QueryHit> queryHits;
		if (state.query != null) {
			BlobQuery query = new TextQuery.Builder()
					.term(state.query)
					.wholeWord(true)
					.caseSensitive(true)
					.count(SearchResultPanel.MAX_QUERY_ENTRIES)
					.build();
			try {
				SearchManager searchManager = OneDev.getInstance(SearchManager.class);
				queryHits = searchManager.search(projectModel.getObject(), getProject().getRevCommit(resolvedRevision, true),
						query);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
		} else {
			queryHits = null;
		}

		newSearchResult(null, queryHits);

		add(ajaxBehavior = new AbstractPostAjaxBehavior() {

			@Override
			protected void respond(AjaxRequestTarget target) {
				IRequestParameters params = RequestCycle.get().getRequest().getPostParameters();
				String action = params.getParameterValue("action").toString("");
				switch (action) {
				case "quickSearch":
					new ModalPanel(target) {

						@Override
						protected Component newContent(String id) {
							return newQuickSearchPanel(id, this);
						}

					};
					break;
				case "advancedSearch":
					new ModalPanel(target) {

						@Override
						protected Component newContent(String id) {
							return newAdvancedSearchPanel(id, this);
						}

					};
					break;
				case "permalink":
					if (isOnBranch()) {
						BlobIdent newBlobIdent = new BlobIdent(state.blobIdent);
						newBlobIdent.revision = resolvedRevision.name();
						ProjectBlobPage.this.onSelect(target, newBlobIdent, null);
					}
					break;
				default:
					throw new IllegalStateException("Unexpected action: " + action);
				}
			}

		});
	}

	/*
	 * In case we are on a branch, this operation makes sure that the branch resolves
	 * to a certain commit during the life cycle of our page, unless the page is
	 * refreshed. This can avoid the issue that displayed file content and subsequent
	 * operations encounters different commit if someone commits to the branch while
	 * we are staying on the page.
	 */
	@Override
	protected Map<String, ObjectId> getObjectIdCache() {
		Map<String, ObjectId> objectIdCache = new HashMap<>();
		if (resolvedRevision != null)
			objectIdCache.put(state.blobIdent.revision, resolvedRevision);
		return objectIdCache;
	}

	@Override
	public CodeComment getOpenComment() {
		if (state.commentId != null)
			return OneDev.getInstance(CodeCommentManager.class).load(state.commentId);
		else
			return null;
	}

	@Override
	public PullRequest getPullRequest() {
		if (state.requestId != null)
			return OneDev.getInstance(PullRequestManager.class).load(state.requestId);
		else
			return null;
	}

	private void newBlobNavigator(@Nullable AjaxRequestTarget target) {
		Component blobNavigator = new BlobNavigator(BLOB_NAVIGATOR_ID, this);
		if (target != null) {
			replace(blobNavigator);
			target.add(blobNavigator);
		} else {
			add(blobNavigator);
		}
	}

	private void newBlobOperations(@Nullable AjaxRequestTarget target) {
		WebMarkupContainer blobOperations = new WebMarkupContainer("blobOperations");
		blobOperations.setOutputMarkupId(true);

		String revision = state.blobIdent.revision;

		AtomicBoolean reviewRequired = new AtomicBoolean(true);
		try {
			reviewRequired.set(revision!=null && getProject().isReviewRequiredForModification(getLoginUser(), revision, null));
		} catch (Exception e) {
			logger.error("Error checking review requirement", e);
		}

		AtomicBoolean buildRequired = new AtomicBoolean(true);
		try {
			buildRequired.set(revision!=null && getProject().isBuildRequiredForModification(getLoginUser(), revision, null));
		} catch (Exception e) {
			logger.error("Error checking build requirement", e);
		}

		blobOperations.add(new MenuLink("add") {

			@Override
			protected List<MenuItem> getMenuItems(FloatingPanel dropdown) {
				List<MenuItem> menuItems = new ArrayList<>();
				menuItems.add(new MenuItem() {

					@Override
					public String getLabel() {
						return "Create New File";
					}

					@Override
					public WebMarkupContainer newLink(String id) {
						return new ViewStateAwareAjaxLink<Void>(id) {

							@Override
							public void onClick(AjaxRequestTarget target) {
								onModeChange(target, Mode.ADD, null);
								dropdown.close();
							}

						};
					}

				});

				menuItems.add(new MenuItem() {

					@Override
					public String getLabel() {
						return "Upload Files";
					}

					@Override
					public WebMarkupContainer newLink(String id) {
						return new ModalLink(id) {

							@Override
							public void onClick(AjaxRequestTarget target) {
								super.onClick(target);
								dropdown.close();
							}

							@Override
							protected Component newContent(String id, ModalPanel modal) {
								return new BlobUploadPanel(id, ProjectBlobPage.this) {

									@Override
									public void onCancel(AjaxRequestTarget target) {
										modal.close();
									}

									@Override
									public void onCommitted(AjaxRequestTarget target, RefUpdated refUpdated) {
										ProjectBlobPage.this.onCommitted(target, refUpdated, null);
										modal.close();
									}

								};
							}

						};
					}

				});
				return menuItems;
			}

			@Override
			protected void onComponentTag(ComponentTag tag) {
				super.onComponentTag(tag);

				if (reviewRequired.get()) {
					tag.append("class", "disabled", " ");
					tag.put("title", "Review required for this change. Submit pull request instead");
				} else if (buildRequired.get()) {
					tag.append("class", "disabled", " ");
					tag.put("title", "Build required for this change. Submit pull request instead");
				} else {
					tag.put("title", "Add on branch " + state.blobIdent.revision);
				}
			}

			@Override
			protected void onConfigure() {
				super.onConfigure();

				Project project = getProject();
				if ((state.mode == Mode.VIEW || state.mode == Mode.BLAME)
						&& isOnBranch() && state.blobIdent.isTree()
						&& SecurityUtils.canWriteCode(project)) {
					setVisible(true);
					setEnabled(!reviewRequired.get() && !buildRequired.get());
				} else {
					setVisible(false);
				}
			}

		});

		blobOperations.add(new MenuLink("search") {

			@Override
			protected List<MenuItem> getMenuItems(FloatingPanel dropdown) {
				List<MenuItem> menuItems = new ArrayList<>();
				menuItems.add(new MenuItem() {

					@Override
					public String getShortcut() {
						return "T";
					}

					@Override
					public String getLabel() {
						return "Quick Search";
					}

					@Override
					public WebMarkupContainer newLink(String id) {
						return new ModalLink(id) {

							@Override
							public void onClick(AjaxRequestTarget target) {
								super.onClick(target);
								dropdown.close();
							}

							@Override
							protected Component newContent(String id, ModalPanel modal) {
								return newQuickSearchPanel(id, modal);
							}

						};
					}

				});
				menuItems.add(new MenuItem() {

					@Override
					public String getShortcut() {
						return "V";
					}

					@Override
					public String getLabel() {
						return "Advanced Search";
					}

					@Override
					public WebMarkupContainer newLink(String id) {
						return new ModalLink(id) {

							@Override
							public void onClick(AjaxRequestTarget target) {
								super.onClick(target);
								dropdown.close();
							}

							@Override
							protected Component newContent(String id, ModalPanel modal) {
								return newAdvancedSearchPanel(id, modal);
							}

						};
					}

				});

				IVisitor<Component, Component> visitor = new IVisitor<Component, Component>() {

					@Override
					public void component(Component object, IVisit<Component> visit) {
						visit.stop(object);
					}

				};
				SearchMenuContributor contributor = (SearchMenuContributor) getPage().visitChildren(
						SearchMenuContributor.class, visitor);
				if (contributor != null)
					menuItems.addAll(contributor.getMenuItems(dropdown));

				return menuItems;
			}

		});

		String compareWith = resolvedRevision!=null?resolvedRevision.name():null;
		String query;
		if (state.blobIdent.path != null)
			query = String.format("path(%s)", state.blobIdent.path);
		else
			query = null;
		blobOperations.add(new ViewStateAwarePageLink<Void>("history", ProjectCommitsPage.class,
				ProjectCommitsPage.paramsOf(getProject(), query, compareWith)));

		blobOperations.add(new DropdownLink("getCode") {

			@Override
			protected void onConfigure() {
				super.onConfigure();
				setVisible(state.blobIdent.revision != null && state.blobIdent.path == null);
			}

			@Override
			protected void onInitialize(FloatingPanel dropdown) {
				super.onInitialize(dropdown);
				dropdown.add(AttributeAppender.append("class", "get-code"));
			}

			@Override
			protected Component newContent(String id, FloatingPanel dropdown) {
				return new GetCodePanel(id, this) {

					@Override
					protected Project getProject() {
						return ProjectBlobPage.this.getProject();
					}

					@Override
					protected String getRevision() {
						return state.blobIdent.revision;
					}

				};
			}

		});

		if (target != null) {
			replace(blobOperations);
			target.add(blobOperations);
		} else {
			add(blobOperations);
		}
	}

	private QuickSearchPanel newQuickSearchPanel(String id, ModalPanel modal) {
		return new QuickSearchPanel(id, projectModel, new AbstractReadOnlyModel<String>() {

			@Override
			public String getObject() {
				return state.blobIdent.revision;
			}

		}) {

			@Override
			protected void onSelect(AjaxRequestTarget target, QueryHit hit) {
				BlobIdent selected = new BlobIdent(state.blobIdent.revision, hit.getBlobPath(),
						FileMode.REGULAR_FILE.getBits());
				ProjectBlobPage.this.onSelect(target, selected, SourceRendererProvider.getPosition(hit.getTokenPos()));
				modal.close();
			}

			@Override
			protected void onMoreQueried(AjaxRequestTarget target, List<QueryHit> hits) {
				newSearchResult(target, hits);
				resizeWindow(target);
				modal.close();
			}

			@Override
			protected void onCancel(AjaxRequestTarget target) {
				modal.close();
			}

		};
	}

	private AdvancedSearchPanel advancedSearchPanel;

	private ModalPanel advancedSearchPanelModal;

	private AdvancedSearchPanel newAdvancedSearchPanel(String id, ModalPanel modal) {
		/*
		 * Re-use advanced search panel instance so that search options can be preserved in the page
		 */
		advancedSearchPanelModal = modal;
		if (advancedSearchPanel == null) {
			advancedSearchPanel = new AdvancedSearchPanel(id, projectModel, new AbstractReadOnlyModel<String>() {

				@Override
				public String getObject() {
					return state.blobIdent.revision;
				}

			}) {

				@Override
				protected void onSearchComplete(AjaxRequestTarget target, List<QueryHit> hits) {
					newSearchResult(target, hits);
					resizeWindow(target);
					advancedSearchPanelModal.close();
				}

				@Override
				protected void onCancel(AjaxRequestTarget target) {
					advancedSearchPanelModal.close();
				}

				@Override
				protected BlobIdent getCurrentBlob() {
					return state.blobIdent;
				}

			};
		}
		return advancedSearchPanel;
	}

	private void newBlobContent(@Nullable AjaxRequestTarget target) {
		PrioritizedComponentRenderer mostPrioritizedRenderer = null;
		for (BlobRendererContribution contribution: OneDev.getExtensions(BlobRendererContribution.class)) {
			PrioritizedComponentRenderer renderer = contribution.getRenderer(this);
			if (renderer != null) {
				if (mostPrioritizedRenderer == null || mostPrioritizedRenderer.getPriority() > renderer.getPriority())
					mostPrioritizedRenderer = renderer;
			}
		}
		Component blobContent = Preconditions.checkNotNull(mostPrioritizedRenderer).render(BLOB_CONTENT_ID);
		if (target != null) {
			replace(blobContent);
			target.add(blobContent);
		} else {
			add(blobContent);
		}
	}

	private void pushState(AjaxRequestTarget target) {
		PageParameters params = paramsOf(getProject(), state);
		CharSequence url = RequestCycle.get().urlFor(ProjectBlobPage.class, params);
		pushState(target, url.toString(), state);
	}

	private void newCommitStatus(@Nullable AjaxRequestTarget target) {
		Component commitStatus;
		if (resolvedRevision != null) {
			commitStatus = new CommitStatusPanel("buildStatus", resolvedRevision) {

				@Override
				protected Project getProject() {
					return ProjectBlobPage.this.getProject();
				}

			};
		} else {
			commitStatus = new WebMarkupContainer("buildStatus").add(AttributeAppender.append("class", "d-none"));
		}

		commitStatus.setOutputMarkupPlaceholderTag(true);

		if (target != null) {
			replace(commitStatus);
			target.add(commitStatus);
		} else {
			add(commitStatus);
		}
	}

	private void newBuildSupportNote(@Nullable AjaxRequestTarget target) {
		Component buildSupportNote = new WebMarkupContainer("buildSupportNote") {

			@Override
			protected void onInitialize() {
				super.onInitialize();

				String branch = state.blobIdent.revision;
				if (branch == null)
					branch = "master";
				if (SecurityUtils.canModify(getProject(), branch, BuildSpec.BLOB_PATH)) {
					add(new ViewStateAwareAjaxLink<Void>("addFile") {

						@Override
						public void onClick(AjaxRequestTarget target) {
							onModeChange(target, Mode.ADD, BuildSpec.BLOB_PATH);
						}

						@Override
						public IModel<?> getBody() {
							return Model.of("adding " + BuildSpec.BLOB_PATH);
						}

					});
				} else {
					add(new Label("addFile", "adding " + BuildSpec.BLOB_PATH) {

						@Override
						protected void onComponentTag(ComponentTag tag) {
							super.onComponentTag(tag);
							tag.setName("span");
						}

					});
				}
				setOutputMarkupPlaceholderTag(true);
			}

			@Override
			protected void onConfigure() {
				super.onConfigure();
				if (resolvedRevision != null && isOnBranch() && state.blobIdent.path == null && state.mode == Mode.VIEW) {
					BlobIdent oldBlobIdent = new BlobIdent(resolvedRevision.name(), ".onedev-buildspec", FileMode.TYPE_FILE);
					BlobIdent blobIdent = new BlobIdent(resolvedRevision.name(), BuildSpec.BLOB_PATH, FileMode.TYPE_FILE);
					setVisible(getProject().getBlob(blobIdent, false) == null && getProject().getBlob(oldBlobIdent, false) == null);
				} else {
					setVisible(false);
				}
			}

		};

		if (target != null) {
			replace(buildSupportNote);
			target.add(buildSupportNote);
		} else {
			add(buildSupportNote);
		}
	}

	private void newRevisionPicker(@Nullable AjaxRequestTarget target) {
		String revision = state.blobIdent.revision;
		boolean canCreateRef;
		if (revision == null) {
			revision = "master";
			canCreateRef = false;
		} else {
			canCreateRef = true;
		}

		Component revisionPicker = new RevisionPicker(REVISION_PICKER_ID, projectModel, revision, canCreateRef) {

			@Override
			protected void onComponentTag(ComponentTag tag) {
				super.onComponentTag(tag);
				if (isOnBranch())
					tag.put("title", "Press 'y' to get permalink");
			}

			@Override
			protected String getRevisionUrl(String revision) {
				BlobIdent blobIdent = new BlobIdent(revision, null, FileMode.TREE.getBits());
				State state = new State(blobIdent);
				PageParameters params = ProjectBlobPage.paramsOf(projectModel.getObject(), state);
				return urlFor(ProjectBlobPage.class, params).toString();
			}

			@Override
			protected void onSelect(AjaxRequestTarget target, String revision) {
				BlobIdent newBlobIdent = new BlobIdent(state.blobIdent);
				newBlobIdent.revision = revision;
				if (newBlobIdent.path != null) {
					try (RevWalk revWalk = new RevWalk(getProject().getRepository())) {
						RevTree revTree = getProject().getRevCommit(revision, true).getTree();
						TreeWalk treeWalk = TreeWalk.forPath(getProject().getRepository(), newBlobIdent.path, revTree);
						if (treeWalk != null) {
							newBlobIdent.mode = treeWalk.getRawMode(0);
						} else {
							newBlobIdent.path = null;
							newBlobIdent.mode = FileMode.TREE.getBits();
						}
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}

				ProjectBlobPage.this.onSelect(target, newBlobIdent, null);
			}

		};
		if (target != null) {
			replace(revisionPicker);
			target.add(revisionPicker);
		} else {
			add(revisionPicker);
		}
	}

	/*
	 * This method represents changing of resolved revision, instead of the named revision.
	 * It is possible that resolved revision changes while named revision remains unchanged.
	 * For instance when a file on a branch is edited from this page, the named revision
	 * remains unchanged (still point to the branch), but the underlying resolved revision
	 * (the commit) has been changed
	 *
	 * @param target
	 */
	private void onResolvedRevisionChange(AjaxRequestTarget target) {
		/*
		 * A hack to reset resolved revision to null to disable getObjectIdCache()
		 * temporarily as otherwise getObjectId() method below will always
		 * resolved to existing value of resolvedRevision
		 */
		resolvedRevision = null;
		resolvedRevision = getProject().getRevCommit(state.blobIdent.revision, true).copy();

		OneDev.getInstance(WebSocketManager.class).observe(this);
		newRevisionPicker(target);
		newCommitStatus(target);
		target.add(revisionIndexing);
		newBlobNavigator(target);
		newBlobOperations(target);
		newBuildSupportNote(target);
		newBlobContent(target);
	}

	@Override
	public void renderHead(IHeaderResponse response) {
		super.renderHead(response);

		response.render(JavaScriptHeaderItem.forReference(new ProjectBlobResourceReference()));

		String callback = ajaxBehavior.getCallbackFunction(explicit("action")).toString();
		String script = String.format("onedev.server.projectBlob.onDomReady(%s);", callback);

		response.render(OnDomReadyHeaderItem.forScript(script));
	}

	public static ProjectBlobPage.State getState(CodeComment comment) {
		BlobIdent blobIdent = new BlobIdent(comment.getMark().getCommitHash(), comment.getMark().getPath(),
				FileMode.REGULAR_FILE.getBits());
		ProjectBlobPage.State state = new ProjectBlobPage.State(blobIdent);
		state.commentId = comment.getId();
		state.position = SourceRendererProvider.getPosition(comment.getMark().getRange());
		return state;
	}

	public static PageParameters paramsOf(CodeComment comment) {
		return paramsOf(comment.getProject(), getState(comment));
	}

	public static PageParameters paramsOf(Project project, BlobIdent blobIdent) {
		return paramsOf(project, new State(blobIdent));
	}

	public static void fillParams(PageParameters params, State state) {
		if (state.blobIdent.revision != null)
			params.add(PARAM_REVISION, state.blobIdent.revision);
		if (state.blobIdent.path != null)
			params.add(PARAM_PATH, state.blobIdent.path);
		if (state.position != null)
			params.add(PARAM_POSITION, state.position.toString());
		if (state.requestId != null)
			params.add(PARAM_REQUEST, state.requestId);
		if (state.commentId != null)
			params.add(PARAM_COMMENT, state.commentId);
		if (state.mode != Mode.VIEW)
			params.add(PARAM_MODE, state.mode.name().toLowerCase());
		if (state.mode == Mode.VIEW && state.viewPlain)
			params.add(PARAM_VIEW_PLAIN, true);
		if (state.urlBeforeEdit != null)
			params.add(PARAM_URL_BEFORE_EDIT, state.urlBeforeEdit);
		if (state.urlAfterEdit != null)
			params.add(PARAM_URL_AFTER_EDIT, state.urlAfterEdit);

		if (state.query != null)
			params.add(PARAM_QUERY, state.query);
		if (state.initialNewPath != null)
			params.add(PARAM_INITIAL_NEW_PATH, state.initialNewPath);
	}

	public static PageParameters paramsOf(Project project, State state) {
		PageParameters params = paramsOf(project);
		fillParams(params, state);
		return params;
	}

	private void newSearchResult(@Nullable AjaxRequestTarget target, @Nullable List<QueryHit> hits) {
		Component content;
		if (hits != null) {
			content = new SearchResultPanel("content", this, hits) {

				@Override
				protected void onClose(AjaxRequestTarget target) {
					newSearchResult(target, null);
					resizeWindow(target);
				}

			};
			if (target != null) {
				target.appendJavaScript(""
						+ "$('.project-blob>.search-result').css('display', 'flex'); "
						+ "$('.project-blob .search-result>.body').focus();");
			}
		} else {
			content = new WebMarkupContainer("content").setOutputMarkupId(true);
			if (target != null)
				target.appendJavaScript("$('.project-blob>.search-result').hide();");
			else
				searchResult.add(AttributeAppender.replace("style", "display: none;"));
		}
		if (target != null) {
			searchResult.replace(content);
			target.add(content);
		} else {
			searchResult.add(content);
		}
	}

	@Override
	protected void onPopState(AjaxRequestTarget target, Serializable data) {
		super.onPopState(target, data);

		State popState = (State) data;
		if (!popState.blobIdent.revision.equals(state.blobIdent.revision)) {
			state = popState;
			newSearchResult(target, null);
			onResolvedRevisionChange(target);
		} else {
			state = popState;
			newBlobNavigator(target);
			newBlobOperations(target);
			newBuildSupportNote(target);
			newBlobContent(target);
		}
	}

	@Override
	public BlobIdent getBlobIdent() {
		return state.blobIdent;
	}

	@Override
	public String getPosition() {
		return state.position;
	}

	@Override
	public void onPosition(AjaxRequestTarget target, String position) {
		state.position = position;
		pushState(target);
	}

	@Override
	public String getPositionUrl(String position) {
		State positionState = SerializationUtils.clone(state);
		positionState.blobIdent.revision = resolvedRevision.name();
		positionState.commentId = null;
		positionState.position = position;
		PageParameters params = paramsOf(getProject(), positionState);
		return RequestCycle.get().urlFor(ProjectBlobPage.class, params).toString();
	}

	@Override
	public Mode getMode() {
		return state.mode;
	}

	@Override
	public boolean isViewPlain() {
		return state.viewPlain;
	}

	@Override
	public String getUrlBeforeEdit() {
		return state.urlBeforeEdit;
	}

	@Override
	public String getUrlAfterEdit() {
		return state.urlAfterEdit;
	}

	@Override
	public PageParameters getParamsBeforeEdit() {
		return paramsOf(getProject(), state);
	}

	@Override
	public PageParameters getParamsAfterEdit() {
		return paramsOf(getProject(), state);
	}

	@Override
	public void onSelect(AjaxRequestTarget target, BlobIdent blobIdent, @Nullable String position) {
		String prevPosition = state.position;
		state.position = position;
		if (!blobIdent.revision.equals(state.blobIdent.revision)) {
			state.blobIdent = blobIdent;
			state.mode = Mode.VIEW;
			state.commentId = null;
			state.requestId = null;
			newSearchResult(target, null);
			onResolvedRevisionChange(target);
			resizeWindow(target);
		} else if (!Objects.equal(state.blobIdent.path, blobIdent.path)) {
			state.blobIdent.path = blobIdent.path;
			state.blobIdent.mode = blobIdent.mode;
			state.mode = Mode.VIEW;
			state.commentId = null;
			newBlobNavigator(target);
			newBlobOperations(target);
			newBuildSupportNote(target);
			newBlobContent(target);
			resizeWindow(target);
			OneDev.getInstance(WebSocketManager.class).observe(this);
		} else if (state.position != null) {
			if (get(BLOB_CONTENT_ID) instanceof Positionable) {
				// This logic is added for performance reason, we do not want to
				// reload the file if go to different mark positions in same file
				((Positionable)get(BLOB_CONTENT_ID)).position(target, state.position);
			} else {
				state.mode = Mode.VIEW;
				newBlobOperations(target);
				newBuildSupportNote(target);
				newBlobContent(target);
				resizeWindow(target);
			}
		} else if (prevPosition != null) {
			state.mode = Mode.VIEW;
			newBlobOperations(target);
			newBuildSupportNote(target);
			newBlobContent(target);
			resizeWindow(target);
		}
		pushState(target);
	}

	@Override
	public void pushState(AjaxRequestTarget target, BlobIdent blobIdent, @Nullable String position) {
		state.blobIdent = blobIdent;
		state.position = position;
		pushState(target);
	}

	@Override
	public void onSearchComplete(AjaxRequestTarget target, List<QueryHit> hits) {
		newSearchResult(target, hits);
		resizeWindow(target);
	}

	@Override
	public void onCommentOpened(AjaxRequestTarget target, CodeComment comment) {
		state.commentId = comment.getId();
		state.position = SourceRendererProvider.getPosition(Preconditions.checkNotNull(comment.mapRange(state.blobIdent)));
		pushState(target);
	}

	@Override
	public void onCommentClosed(AjaxRequestTarget target) {
		state.commentId = null;
		state.position = null;
		pushState(target);
	}

	@Override
	public void onAddComment(AjaxRequestTarget target, PlanarRange range) {
		state.commentId = null;
		state.position = SourceRendererProvider.getPosition(range);
		pushState(target);
	}

	@Override
	public boolean isOnBranch() {
		return state.blobIdent.revision == null || getProject().getBranchRef(state.blobIdent.revision) != null;
	}

	@Override
	public RevCommit getCommit() {
		if (resolvedRevision != null)
			return getProject().getRevCommit(resolvedRevision, true);
		else
			return null;
	}

	public static class State implements Serializable {

		private static final long serialVersionUID = 1L;

		public BlobIdent blobIdent;

		public Long requestId;

		public Long commentId;

		public String position;

		public Mode mode = Mode.VIEW;

		/*
		 * Some blob can be rendered in a way for easier understanding, such as .onedev-buildspec.yml,
		 * In these cases, the VIEW_PLAIN mode enables to view plain text of the blob. Applicable
		 * only when mode is VIEW
		 */
		public boolean viewPlain;

		public String urlBeforeEdit;

		public String urlAfterEdit;

		public boolean renderSource;

		public String query;

		public String initialNewPath;

		public State(BlobIdent blobIdent) {
			this.blobIdent = blobIdent;
		}

		public State() {
			blobIdent = new BlobIdent();
		}

	}

	@Override
	public void onModeChange(AjaxRequestTarget target, Mode mode, @Nullable String newPath) {
		onModeChange(target, mode, false, newPath);
	}

	@Override
	public void onModeChange(AjaxRequestTarget target, Mode mode, boolean viewPlain, @Nullable String newPath) {
		state.viewPlain = viewPlain;
		state.initialNewPath = newPath;

		/*
		 * User might be changing blob name when adding a file, and onModeChange will be called.
		 * In this case, we only need to re-create blob content
		 */
		if (mode != Mode.ADD || state.mode != Mode.ADD) {
			state.mode = mode;
			pushState(target);
			if (state.mode == Mode.VIEW || state.mode == Mode.EDIT || state.mode == Mode.ADD) {
				newBlobNavigator(target);
				newBlobOperations(target);
			}
		}
		newBuildSupportNote(target);
		newBlobContent(target);
		resizeWindow(target);
	}

	@Override
	public void onCommitted(@Nullable AjaxRequestTarget target, RefUpdated refUpdated, @Nullable String position) {
		Project project = getProject();
		if (state.blobIdent.revision == null) {
			state.blobIdent.revision = "master";
			resolvedRevision = refUpdated.getNewCommitId();
			project.setDefaultBranch("master");
		}
		String branch = state.blobIdent.revision;

		getProject().cacheObjectId(branch, refUpdated.getNewCommitId());

		Long projectId = project.getId();
		String refName = refUpdated.getRefName();
		ObjectId oldCommitId = refUpdated.getOldCommitId();
		ObjectId newCommitId = refUpdated.getNewCommitId();

		/*
		 * Update pull request as this is a direct consequence of editing source branch. Also this
		 * is necessary to show the latest changes of the pull request after editing
		 */
		PullRequest request = getPullRequest();
		if (request != null)
			OneDev.getInstance(PullRequestUpdateManager.class).checkUpdate(request);

		OneDev.getInstance(TransactionManager.class).runAfterCommit(new Runnable() {

			@Override
			public void run() {
				OneDev.getInstance(SessionManager.class).runAsync(new Runnable() {

					@Override
					public void run() {
						Project project = OneDev.getInstance(ProjectManager.class).load(projectId);
						project.cacheObjectId(branch, newCommitId);
						RefUpdated refUpdated = new RefUpdated(project, refName, oldCommitId, newCommitId);
						OneDev.getInstance(ListenerRegistry.class).post(refUpdated);
					}

				});
			}

		});

		if (target != null) {
			if (state.urlAfterEdit != null) {
				throw new RedirectToUrlException(state.urlAfterEdit);
			} else {
				BlobIdent newBlobIdent;
				if (state.mode == Mode.DELETE) {
					try (RevWalk revWalk = new RevWalk(getProject().getRepository())) {
						RevTree revTree = getProject().getRevCommit(refUpdated.getNewCommitId(), true).getTree();
						String parentPath = StringUtils.substringBeforeLast(state.blobIdent.path, "/");
						while (TreeWalk.forPath(getProject().getRepository(), parentPath, revTree) == null) {
							if (parentPath.contains("/")) {
								parentPath = StringUtils.substringBeforeLast(parentPath, "/");
							} else {
								parentPath = null;
								break;
							}
						}
						newBlobIdent = new BlobIdent(branch, parentPath, FileMode.TREE.getBits());
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				} else if (state.mode == Mode.ADD) {
					newBlobIdent = new BlobIdent(branch, getNewPath(), FileMode.REGULAR_FILE.getBits());
				} else if (state.mode == Mode.EDIT) {
					newBlobIdent = new BlobIdent(branch, getNewPath(), FileMode.REGULAR_FILE.getBits());
				} else {
					// We've uploaded some files
					newBlobIdent = null;
				}

				if (newBlobIdent != null) {
					state.blobIdent = newBlobIdent;
					state.position = position;
					state.commentId = null;
					state.mode = Mode.VIEW;
					onResolvedRevisionChange(target);
					pushState(target);
				} else {
					state.mode = Mode.VIEW;
					onResolvedRevisionChange(target);
				}

				// fix the issue that sometimes indexing indicator of new commit does not disappear
				target.appendJavaScript("Wicket.WebSocket.send('RenderCallback');");
			}
		}
	}

	@Override
	public String getInitialNewPath() {
		return state.initialNewPath;
	}

	@Override
	public String getDirectory() {
		String path;
		if (state.mode == Mode.ADD || state.mode == Mode.EDIT) {
			path = getNewPath();
			if (path != null) {
				if (path.contains("/"))
					path = StringUtils.substringBeforeLast(path, "/");
				else
					path = null;
			} else {
				throw new IllegalStateException();
			}
		} else if (state.blobIdent.isTree()) {
			path = state.blobIdent.path;
		} else if (state.blobIdent.path.contains("/")) {
			path = StringUtils.substringBeforeLast(state.blobIdent.path, "/");
		} else {
			path = null;
		}
		return path;
	}

	@Override
	public String getDirectoryUrl() {
		String revision = state.blobIdent.revision;
		BlobIdent blobIdent = new BlobIdent(revision, getDirectory(), FileMode.TREE.getBits());
		ProjectBlobPage.State state = new ProjectBlobPage.State(blobIdent);
		return urlFor(ProjectBlobPage.class, ProjectBlobPage.paramsOf(getProject(), state)).toString();
	}

	@Override
	public String getRootDirectoryUrl() {
		BlobIdent blobIdent = new BlobIdent(state.blobIdent.revision, null, FileMode.TREE.getBits());
		return RequestCycle.get().urlFor(ProjectBlobPage.class,
				ProjectBlobPage.paramsOf(getProject(), blobIdent)).toString();
	}

	@Override
	public String getNewPath() {
		BlobNavigator blobNavigator = (BlobNavigator) get(BLOB_NAVIGATOR_ID);
		return blobNavigator.getNewPath();
	}

	@Override
	public void onEvent(IEvent<?> event) {
		super.onEvent(event);
		if (event.getPayload() instanceof RevisionResolved) {
			RevisionResolved revisionResolveEvent = (RevisionResolved) event.getPayload();
			resolvedRevision = revisionResolveEvent.getResolvedRevision();
		}
	}

	@Override
	public String getAutosaveKey() {
		if (state.mode == Mode.ADD) {
			return String.format("autosave:addBlob:%d:%s:%s",
					getProject().getId(), state.blobIdent.revision, getNewPath());
		} else if (state.mode == Mode.EDIT) {
			return String.format("autosave:editBlob:%d:%s:%s:%s",
					getProject().getId(), state.blobIdent.revision,
					state.blobIdent.path, getProject().getBlob(state.blobIdent, true).getBlobId());
		} else {
			throw new IllegalStateException();
		}
	}

	@Override
	public RefUpdated uploadFiles(Collection<FileUpload> uploads, String directory, String commitMessage) {
		Map<String, BlobContent> newBlobs = new HashMap<>();

		String parentPath = getDirectory();

		if (directory != null) {
			if (parentPath != null)
				parentPath += "/" + directory;
			else
				parentPath = directory;
		}

		User user = Preconditions.checkNotNull(SecurityUtils.getUser());
		BlobIdent blobIdent = getBlobIdent();

		for (FileUpload upload: uploads) {
			String blobPath = upload.getClientFileName();
			if (parentPath != null)
				blobPath = parentPath + "/" + blobPath;

			if (getProject().isReviewRequiredForModification(user, blobIdent.revision, blobPath))
				throw new BlobEditException("Review required for this change. Please submit pull request instead");
			else if (getProject().isBuildRequiredForModification(user, blobIdent.revision, blobPath))
				throw new BlobEditException("Build required for this change. Please submit pull request instead");

			BlobContent blobContent = new BlobContent.Immutable(upload.getBytes(), FileMode.REGULAR_FILE);
			newBlobs.put(blobPath, blobContent);
		}

		BlobEdits blobEdits = new BlobEdits(Sets.newHashSet(), newBlobs);
		String refName = blobIdent.revision!=null?GitUtils.branch2ref(blobIdent.revision):"refs/heads/master";

		ObjectId prevCommitId;
		if (blobIdent.revision != null)
			prevCommitId = getProject().getRevCommit(blobIdent.revision, true).copy();
		else
			prevCommitId = ObjectId.zeroId();

		while (true) {
			try {
				ObjectId newCommitId = blobEdits.commit(getProject().getRepository(), refName, prevCommitId,
						prevCommitId, user.asPerson(), commitMessage);
				return new RefUpdated(getProject(), refName, prevCommitId, newCommitId);
			} catch (ObjectAlreadyExistsException|NotTreeException e) {
				throw new BlobEditException(e.getMessage());
			} catch (ObsoleteCommitException e) {
				prevCommitId = e.getOldCommitId();
			}
		}
	}

	@Override
	protected boolean isPermitted() {
		return SecurityUtils.canReadCode(getProject());
	}

	@Override
	public String appendRaw(String url) {
		try {
			URIBuilder builder;
			builder = new URIBuilder(url);
			for (NameValuePair pair: builder.getQueryParams()) {
				if (pair.getName().equals(PARAM_RAW))
					return url;
			}
			return builder.addParameter(PARAM_RAW, "true").build().toString();
		} catch (URISyntaxException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public ScriptIdentity getScriptIdentity() {
		if (getBlobIdent().revision != null)
			return new JobIdentity(getProject(), getCommit().copy());
		else // when we add file to an empty project
			return new JobIdentity(getProject(), null);
	}

	@Override
	protected Component newProjectTitle(String componentId) {
		return new Label(componentId, "Files");
	}

}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】这是一个路径遍历漏洞。在`InsertUrlPanel.java`和`ProjectBlobPage.java`中，上传文件的路径没有得到适当的验证和清理。具体表现为，用户可以直接通过修改请求头或文件路径来上传文件到任意位置，只要该位置在OneDev的写权限范围内。
【关键代码模式】
- `upload.getClientFileName()`
- `request.getHeader("File-Name")`
- `FilenameUtils.sanitizeFilename()`
【修复方法说明】修复方法通过使用`FilenameUtils.sanitizeFilename()`对上传文件的名称进行清理，以防止路径遍历攻击。