1 /*
2  * FileDialog.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.core.client.files.filedialog;
16 
17 import com.google.gwt.aria.client.DialogRole;
18 import com.google.gwt.event.logical.shared.SelectionEvent;
19 
20 import org.rstudio.core.client.StringUtil;
21 import org.rstudio.core.client.files.FileSystemContext;
22 import org.rstudio.core.client.files.FileSystemItem;
23 import org.rstudio.core.client.widget.ProgressOperationWithInput;
24 
25 public abstract class FileDialog extends FileSystemDialog
26 {
FileDialog(String title, String caption, DialogRole role, String buttonName, boolean promptOnOverwrite, boolean allowNonexistentFile, boolean allowFolderCreation, FileSystemContext context, String filter, ProgressOperationWithInput<FileSystemItem> operation)27    protected FileDialog(String title,
28                         String caption,
29                         DialogRole role,
30                         String buttonName,
31                         boolean promptOnOverwrite,
32                         boolean allowNonexistentFile,
33                         boolean allowFolderCreation,
34                         FileSystemContext context,
35                         String filter,
36                         ProgressOperationWithInput<FileSystemItem> operation)
37    {
38       super(title, caption, role, buttonName, context, filter, allowFolderCreation,
39             operation);
40 
41       promptOnOverwrite_ = promptOnOverwrite;
42       allowNonexistentFile_ = allowNonexistentFile;
43    }
44 
45    /**
46     * This should ONLY be called from the accept() method, otherwise the
47     * filename may contain invalid data which could throw an exception.
48     * @return
49     */
50    @Override
getSelectedItem()51    protected FileSystemItem getSelectedItem()
52    {
53       return context_.itemForName(browser_.getFilename(), false, false);
54    }
55 
56    @Override
getFilenameLabel()57    public String getFilenameLabel()
58    {
59       return "File name";
60    }
61 
62    /**
63     * Validate the current state of the dialog. Subclasses can override
64     * this but super.shouldAccept() MUST be the last validation that occurs.
65     * @return true if the dialog is in a valid state for acceptance
66     */
shouldAccept()67    protected boolean shouldAccept()
68    {
69       // It's possible the user has typed the name of a file, but hasn't
70       // actually selected a file in the browser. If the user has typed
71       // a filename, then prefer using that over what the previously
72       // selected file might have been.
73       //
74       // Note that selecting an item in the file widget will also update
75       // the browser filename.
76       String filename = browser_.getFilename().trim();
77       if (StringUtil.isNullOrEmpty(filename))
78          filename = browser_.getSelectedValue().trim();
79 
80       if (filename.length() == 0)
81          return false;
82 
83       // Make sure the browser's notion of the filename is in sync.
84       browser_.setFilename(filename);
85 
86       int lastIndex = filename.lastIndexOf('/');
87       if (lastIndex >= 0)
88       {
89          String dir = filename.substring(0, lastIndex);
90          if (dir.length() == 0)
91             dir = "/";
92          String file = filename.substring(lastIndex + 1);
93 
94          // Targeted fix for "611: Permission denied error when attempting to
95          // browse /shared folder in open file dialog". The /shared folder
96          // doesn't have list permissions.
97          if (dir.equals("/shared"))
98          {
99             cd(filename);
100             return false;
101          }
102 
103          browser_.setFilename(file);
104          browser_.setFilenameEnabled(false);
105          attemptAcceptOnNextNavigate_ = true;
106          cd(dir);
107          return false;
108       }
109 
110       String filenameValidationError = context_.validatePathElement(filename, true);
111       if (filenameValidationError != null)
112       {
113          browser_.selectFilename();
114          showError(filenameValidationError);
115          return false;
116       }
117 
118       if (navigateIfDirectory())
119          return false;
120 
121       boolean useExactFilename =
122             browser_.getSelectedValue() != null &&
123             browser_.getSelectedValue() == filename;
124 
125       if (!useExactFilename || getAlwaysMungeFilename())
126       {
127          browser_.setFilename(mungeFilename(filename));
128       }
129 
130       if (navigateIfDirectory())
131          return false;
132 
133       FileSystemItem item = context_.itemForName(filename, true, false);
134       if (item == null)
135       {
136          if (!allowNonexistentFile_)
137          {
138             showError("File does not exist");
139             return false;
140          }
141       }
142       else
143       {
144          if (item.isDirectory())
145          {
146             assert false : "This case should be covered by navigateIfDirectory";
147             return false;
148          }
149          else if (promptOnOverwrite_)
150          {
151             /* WARNING. showOverwritePrompt() MAY CAUSE accept() TO BE CALLED
152                 DIRECTLY. ALL OTHER VALIDATION *MUST* BE COMPLETE BEFORE
153                 CALLING showOverwritePrompt()!!! */
154             showOverwritePrompt();
155             return false;
156          }
157       }
158 
159       return true;
160    }
161 
navigateIfDirectory()162    private boolean navigateIfDirectory()
163    {
164       FileSystemItem item = context_.itemForName(browser_.getFilename(),
165                                                  true,
166                                                  false);
167       if (item != null && item.isDirectory())
168       {
169          browser_.setFilename("");
170          cd(item.getName());
171          return true;
172       }
173       return false;
174    }
175 
176    /**
177     * Gives subclasses an opportunity to change the filename before acceptance.
178     * This happens AFTER validation so it's imperative that no potentially
179     * illegal values be returned from this method.
180     */
mungeFilename(String filename)181    protected String mungeFilename(String filename)
182    {
183       return filename;
184    }
185 
getAlwaysMungeFilename()186    protected boolean getAlwaysMungeFilename()
187    {
188       return false;
189    }
190 
cd(String path)191    protected void cd(String path)
192    {
193       browser_.setFilenameEnabled(false);
194       browser_.cd(path);
195    }
196 
197    @Override
onNavigated()198    public void onNavigated()
199    {
200       super.onNavigated();
201 
202       browser_.setFilenameEnabled(true);
203       if (attemptAcceptOnNextNavigate_)
204       {
205          attemptAcceptOnNextNavigate_ = false;
206          maybeAccept();
207       }
208    }
209 
210    @Override
focusInitialControl()211    protected void focusInitialControl()
212    {
213       browser_.setFilenameFocus(true);
214       browser_.selectFilename();
215    }
216 
217    @Override
onSelection(SelectionEvent<FileSystemItem> event)218    public void onSelection(SelectionEvent<FileSystemItem> event)
219    {
220       super.onSelection(event);
221 
222       FileSystemItem item = event.getSelectedItem();
223       if (item != null && !item.isDirectory())
224          browser_.setFilename(item.getName());
225    }
226 
227    @Override
onError(String errorMessage)228    public void onError(String errorMessage)
229    {
230       attemptAcceptOnNextNavigate_ = false;
231       super.onError(errorMessage);
232    }
233 
234    protected boolean promptOnOverwrite_;
235    protected boolean allowNonexistentFile_;
236    private boolean attemptAcceptOnNextNavigate_ = false;
237 }