1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package org.jetbrains.ide
4 import com.intellij.ide.impl.ProjectUtil.focusProjectWindow
5 import com.intellij.openapi.application.ApplicationManager
6 import com.intellij.openapi.application.ModalityState
7 import com.intellij.openapi.application.runWriteAction
8 import com.intellij.openapi.fileEditor.OpenFileDescriptor
9 import com.intellij.openapi.project.Project
10 import com.intellij.openapi.project.ProjectManager
11 import com.intellij.openapi.project.guessProjectForContentFile
12 import com.intellij.openapi.util.io.FileUtil
13 import com.intellij.openapi.util.text.StringUtil
14 import com.intellij.openapi.util.text.StringUtilRt
15 import com.intellij.openapi.vcs.ProjectLevelVcsManager
16 import com.intellij.openapi.vfs.LocalFileSystem
17 import com.intellij.openapi.vfs.VirtualFile
18 import com.intellij.openapi.vfs.newvfs.ManagingFS
19 import com.intellij.openapi.vfs.newvfs.RefreshQueue
20 import com.intellij.ui.AppUIUtil
21 import com.intellij.util.io.exists
22 import com.intellij.util.io.systemIndependentPath
23 import io.netty.channel.ChannelHandlerContext
24 import io.netty.handler.codec.http.*
25 import org.jetbrains.builtInWebServer.WebServerPathToFileManager
26 import org.jetbrains.builtInWebServer.checkAccess
27 import org.jetbrains.concurrency.*
28 import org.jetbrains.io.send
29 import java.nio.file.Path
30 import java.nio.file.Paths
31 import java.util.concurrent.ConcurrentLinkedQueue
32 import java.util.regex.Pattern
33 import javax.swing.SwingUtilities
35 @Suppress("HardCodedStringLiteral")
36 private val NOT_FOUND = createError("not found")
37 private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$")
40 * @api {get} /file Open file
44 * @apiParam {String} file The path of the file. Relative (to project base dir, VCS root, module source or content root) or absolute.
45 * @apiParam {Integer} [line] The line number of the file (1-based).
46 * @apiParam {Integer} [column] The column number of the file (1-based).
47 * @apiParam {Boolean} [focused=true] Whether to focus project window.
49 * @apiExample {curl} Absolute path
50 * curl http://localhost:63342/api/file//absolute/path/to/file.kt
52 * @apiExample {curl} Relative path
53 * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt
55 * @apiExample {curl} With line and column
56 * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34
58 * @apiExample {curl} Query parameters
59 * curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34
61 @Suppress("HardCodedStringLiteral")
62 internal class OpenFileHttpService : RestService() {
63 @Volatile private var refreshSessionId: Long = 0
64 private val requests = ConcurrentLinkedQueue<OpenFileTask>()
66 override fun getServiceName() = "file"
68 override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST
70 override fun isOriginAllowed(request: HttpRequest) = OriginCheckResult.ASK_CONFIRMATION
72 override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
73 val keepAlive = HttpUtil.isKeepAlive(request)
74 val channel = context.channel()
76 val apiRequest: OpenFileRequest
77 if (request.method() === HttpMethod.POST) {
78 apiRequest = gson.fromJson(createJsonReader(request), OpenFileRequest::class.java)
81 apiRequest = OpenFileRequest()
82 apiRequest.file = StringUtil.nullize(getStringParameter("file", urlDecoder), true)
83 apiRequest.line = getIntParameter("line", urlDecoder)
84 apiRequest.column = getIntParameter("column", urlDecoder)
85 apiRequest.focused = getBooleanParameter("focused", urlDecoder, true)
88 val prefixLength = 1 + PREFIX.length + 1 + getServiceName().length + 1
89 val path = urlDecoder.path()
90 if (path.length > prefixLength) {
91 val matcher = LINE_AND_COLUMN.matcher(path).region(prefixLength, path.length)
92 LOG.assertTrue(matcher.matches())
93 if (apiRequest.file == null) {
94 apiRequest.file = matcher.group(1).trim { it <= ' ' }
96 if (apiRequest.line == -1) {
97 apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1)
99 if (apiRequest.column == -1) {
100 apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1)
104 if (apiRequest.file == null) {
105 return parameterMissedErrorMessage("file")
108 val promise = openFile(apiRequest, context, request) ?: return null
111 sendOk(request, context)
114 if (it === NOT_FOUND) {
115 // don't expose file status
116 sendStatus(HttpResponseStatus.NOT_FOUND.orInSafeMode(HttpResponseStatus.OK), keepAlive, channel)
117 LOG.warn("File ${apiRequest.file} not found")
121 sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel)
128 private fun openFile(request: OpenFileRequest, context: ChannelHandlerContext, httpRequest: HttpRequest?): Promise<Void?>? {
129 val systemIndependentPath = FileUtil.toSystemIndependentName(FileUtil.expandUserHome(request.file!!))
130 val file = Paths.get(FileUtil.toSystemDependentName(systemIndependentPath))
131 if (file.isAbsolute) {
132 if (!file.exists()) {
133 return rejectedPromise(NOT_FOUND)
136 var isAllowed = checkAccess(file)
137 if (isAllowed && com.intellij.ide.impl.ProjectUtil.isRemotePath(systemIndependentPath)) {
138 // invokeAndWait is added to avoid processing many requests in this place: e.g. to prevent abuse of opening many remote files
139 SwingUtilities.invokeAndWait {
140 isAllowed = com.intellij.ide.impl.ProjectUtil.confirmLoadingFromRemotePath(systemIndependentPath, "warning.load.file.from.share", "title.load.file.from.share")
145 return openAbsolutePath(file, request)
148 HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.OK).send(context.channel(), httpRequest)
153 // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation
154 val queue = RefreshQueue.getInstance()
155 queue.cancelSession(refreshSessionId)
156 val mainTask = OpenFileTask(FileUtil.toCanonicalPath(systemIndependentPath, '/'), request)
157 requests.offer(mainTask)
158 val session = queue.createSession(true, true, {
160 val task = requests.poll() ?: break
161 task.promise.catchError {
162 if (openRelativePath(task.path, task.request)) {
163 task.promise.setResult(null)
166 task.promise.setError(NOT_FOUND)
170 }, ModalityState.NON_MODAL)
172 session.addAllFiles(*ManagingFS.getInstance().localRoots)
173 refreshSessionId = session.id
175 return mainTask.promise
179 internal class OpenFileRequest {
180 var file: String? = null
181 // The line number of the file (1-based)
183 // The column number of the file (1-based)
189 private class OpenFileTask(internal val path: String, internal val request: OpenFileRequest) {
190 internal val promise = AsyncPromise<Void?>()
193 private fun navigate(project: Project?, file: VirtualFile, request: OpenFileRequest) {
194 val effectiveProject = project ?: RestService.getLastFocusedOrOpenedProject() ?: ProjectManager.getInstance().defaultProject
195 // OpenFileDescriptor line and column number are 0-based.
196 OpenFileDescriptor(effectiveProject, file, Math.max(request.line - 1, 0), Math.max(request.column - 1, 0)).navigate(true)
197 if (request.focused) {
198 focusProjectWindow(project, true)
202 // path must be normalized
203 private fun openRelativePath(path: String, request: OpenFileRequest): Boolean {
204 var virtualFile: VirtualFile? = null
205 var project: Project? = null
207 val projects = ProjectManager.getInstance().openProjects
208 for (openedProject in projects) {
209 openedProject.baseDir?.let {
210 virtualFile = it.findFileByRelativePath(path)
213 if (virtualFile == null) {
214 virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path)
216 if (virtualFile != null) {
217 project = openedProject
222 if (virtualFile == null) {
223 for (openedProject in projects) {
224 for (vcsRoot in ProjectLevelVcsManager.getInstance(openedProject).allVcsRoots) {
225 virtualFile = vcsRoot.path.findFileByRelativePath(path)
226 if (virtualFile != null) {
227 project = openedProject
234 return virtualFile?.let {
235 AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) })
240 private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise<Void?> {
241 val promise = AsyncPromise<Void?>()
242 val task = Runnable {
244 val virtualFile = runWriteAction {
245 LocalFileSystem.getInstance().refreshAndFindFileByPath(file.systemIndependentPath)
247 if (virtualFile == null) {
248 promise.setError(NOT_FOUND)
251 navigate(guessProjectForContentFile(virtualFile), virtualFile, request)
252 promise.setResult(null)
257 val app = ApplicationManager.getApplication()
258 if (app.isUnitTestMode) {
259 app.invokeAndWait(task)
262 app.invokeLater(task)