ac0248283e5077ee939e133b5d0863685161c332
[idea/community.git] / platform / built-in-server / src / org / jetbrains / ide / OpenFileHttpService.kt
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
3
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
34
35 @Suppress("HardCodedStringLiteral")
36 private val NOT_FOUND = createError("not found")
37 private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$")
38
39 /**
40  * @api {get} /file Open file
41  * @apiName file
42  * @apiGroup Platform
43  *
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.
48  *
49  * @apiExample {curl} Absolute path
50  * curl http://localhost:63342/api/file//absolute/path/to/file.kt
51  *
52  * @apiExample {curl} Relative path
53  * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt
54  *
55  * @apiExample {curl} With line and column
56  * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34
57  *
58  * @apiExample {curl} Query parameters
59  * curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34
60  */
61 @Suppress("HardCodedStringLiteral")
62 internal class OpenFileHttpService : RestService() {
63   @Volatile private var refreshSessionId: Long = 0
64   private val requests = ConcurrentLinkedQueue<OpenFileTask>()
65
66   override fun getServiceName() = "file"
67
68   override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST
69
70   override fun isOriginAllowed(request: HttpRequest) = OriginCheckResult.ASK_CONFIRMATION
71
72   override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
73     val keepAlive = HttpUtil.isKeepAlive(request)
74     val channel = context.channel()
75
76     val apiRequest: OpenFileRequest
77     if (request.method() === HttpMethod.POST) {
78       apiRequest = gson.fromJson(createJsonReader(request), OpenFileRequest::class.java)
79     }
80     else {
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)
86     }
87
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 <= ' ' }
95       }
96       if (apiRequest.line == -1) {
97         apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1)
98       }
99       if (apiRequest.column == -1) {
100         apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1)
101       }
102     }
103
104     if (apiRequest.file == null) {
105       return parameterMissedErrorMessage("file")
106     }
107
108     val promise = openFile(apiRequest, context, request) ?: return null
109     promise
110       .onSuccess {
111         sendOk(request, context)
112       }
113       .onError {
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")
118         }
119         else {
120           // todo send error
121           sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel)
122           LOG.error(it)
123         }
124       }
125     return null
126   }
127
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)
134       }
135
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")
141         }
142       }
143
144       if (isAllowed) {
145         return openAbsolutePath(file, request)
146       }
147       else {
148         HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.OK).send(context.channel(), httpRequest)
149         return null
150       }
151     }
152
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, {
159       while (true) {
160         val task = requests.poll() ?: break
161         task.promise.catchError {
162           if (openRelativePath(task.path, task.request)) {
163             task.promise.setResult(null)
164           }
165           else {
166             task.promise.setError(NOT_FOUND)
167           }
168         }
169       }
170     }, ModalityState.NON_MODAL)
171
172     session.addAllFiles(*ManagingFS.getInstance().localRoots)
173     refreshSessionId = session.id
174     session.launch()
175     return mainTask.promise
176   }
177 }
178
179 internal class OpenFileRequest {
180   var file: String? = null
181   // The line number of the file (1-based)
182   var line = 0
183   // The column number of the file (1-based)
184   var column = 0
185
186   var focused = true
187 }
188
189 private class OpenFileTask(internal val path: String, internal val request: OpenFileRequest) {
190   internal val promise = AsyncPromise<Void?>()
191 }
192
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)
199   }
200 }
201
202 // path must be normalized
203 private fun openRelativePath(path: String, request: OpenFileRequest): Boolean {
204   var virtualFile: VirtualFile? = null
205   var project: Project? = null
206
207   val projects = ProjectManager.getInstance().openProjects
208   for (openedProject in projects) {
209     openedProject.baseDir?.let {
210       virtualFile = it.findFileByRelativePath(path)
211     }
212
213     if (virtualFile == null) {
214       virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path)
215     }
216     if (virtualFile != null) {
217       project = openedProject
218       break
219     }
220   }
221
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
228           break
229         }
230       }
231     }
232   }
233
234   return virtualFile?.let {
235     AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) })
236     true
237   } ?: false
238 }
239
240 private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise<Void?> {
241   val promise = AsyncPromise<Void?>()
242   val task = Runnable {
243     promise.catchError {
244       val virtualFile = runWriteAction {
245         LocalFileSystem.getInstance().refreshAndFindFileByPath(file.systemIndependentPath)
246       }
247       if (virtualFile == null) {
248         promise.setError(NOT_FOUND)
249       }
250       else {
251         navigate(guessProjectForContentFile(virtualFile), virtualFile, request)
252         promise.setResult(null)
253       }
254     }
255   }
256
257   val app = ApplicationManager.getApplication()
258   if (app.isUnitTestMode) {
259     app.invokeAndWait(task)
260   }
261   else {
262     app.invokeLater(task)
263   }
264   return promise
265 }