1 /*
2  * DesktopOptions.cpp
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 
16 #include "DesktopOptions.hpp"
17 
18 #include <QtGui>
19 #include <QApplication>
20 #include <QDesktopWidget>
21 
22 #include <core/Random.hpp>
23 #include <core/system/System.hpp>
24 #include <core/system/Environment.hpp>
25 
26 #include "DesktopInfo.hpp"
27 #include "DesktopUtils.hpp"
28 
29 using namespace rstudio::core;
30 
31 namespace rstudio {
32 namespace desktop {
33 
34 #ifdef _WIN32
35 // Defined in DesktopRVersion.cpp
36 QString binDirToHomeDir(QString binDir);
37 #endif
38 
39 #define kMainWindowGeometry (QStringLiteral("mainwindow/geometry"))
40 #define kFixedWidthFont     (QStringLiteral("font.fixedWidth"))
41 #define kProportionalFont   (QStringLiteral("font.proportional"))
42 
43 QString scratchPath;
44 
options()45 Options& options()
46 {
47    static Options singleton;
48    return singleton;
49 }
50 
Options()51 Options::Options() :
52    settings_(FORMAT,
53              QSettings::UserScope,
54              QString::fromUtf8("RStudio"),
55              QString::fromUtf8("desktop")),
56    runDiagnostics_(false)
57 {
58 #ifndef _WIN32
59    // ensure that the options file is only readable by this user
60    FilePath optionsFile(settings_.fileName().toStdString());
61    if (!optionsFile.exists())
62    {
63       // file doesn't yet exist - QT can lazily write to the settings file
64       // create an empty file so we can set its permissions before it's created by QT
65       std::shared_ptr<std::ostream> pOfs;
66       Error error = optionsFile.openForWrite(pOfs, false);
67       if (error)
68          LOG_ERROR(error);
69    }
70 
71    Error error = optionsFile.changeFileMode(FileMode::USER_READ_WRITE);
72    if (error)
73       LOG_ERROR(error);
74 #endif
75 }
76 
initFromCommandLine(const QStringList & arguments)77 void Options::initFromCommandLine(const QStringList& arguments)
78 {
79    for (int i=1; i<arguments.size(); i++)
80    {
81       const QString &arg = arguments.at(i);
82       if (arg == QString::fromUtf8(kRunDiagnosticsOption))
83          runDiagnostics_ = true;
84    }
85 
86    // synchronize zoom level with desktop frame
87    desktopInfo().setZoomLevel(zoomLevel());
88 }
89 
restoreMainWindowBounds(QMainWindow * win)90 void Options::restoreMainWindowBounds(QMainWindow* win)
91 {
92    // NOTE: on macOS, if the display configuration has changed, the attempt to
93    // restore window geometry can fail and the use can be left with a tiny
94    // RStudio window.
95    //
96    // to avoid this, we always first attempt to resize to a 'good' size, and
97    // then restore a saved window geometry (which may just silently fail if the
98    // display configuration has indeed changed)
99    //
100    // https://github.com/rstudio/rstudio/issues/3498
101    // https://github.com/rstudio/rstudio/issues/3159
102    //
103 
104    QSize size = QSize(1200, 900).boundedTo(
105             QApplication::primaryScreen()->availableGeometry().size());
106    if (size.width() > 800 && size.height() > 500)
107    {
108       // Only use default size if it seems sane; otherwise let Qt set it
109       win->resize(size);
110    }
111 
112    if (settings_.contains(kMainWindowGeometry))
113    {
114       // try to restore the geometry
115       win->restoreGeometry(settings_.value(kMainWindowGeometry).toByteArray());
116 
117       // double-check that we haven't accidentally restored a geometry that
118       // places the Window off-screen (can happen if the screen configuration
119       // changes between the time geometry was saved and loaded)
120       QRect desktopRect = QApplication::primaryScreen()->availableGeometry();
121       QRect winRect = win->geometry();
122 
123       // shrink the window rectangle a bit just to capture cases like RStudio
124       // too close to edge of window and hardly showing at all
125       QRect checkRect(
126                winRect.topLeft() + QPoint(5, 5),
127                winRect.bottomRight() - QPoint(5, 5));
128 
129       // check for intersection
130       if (!desktopRect.intersects(checkRect))
131       {
132          // restore size and center the window
133          win->resize(size);
134          win->move(
135                   desktopRect.width() / 2 - size.width() / 2,
136                   desktopRect.height() / 2 - size.height() / 2);
137       }
138    }
139 
140    // ensure a minimum width, height for the window on restore
141    win->resize(
142             std::max(300, win->width()),
143             std::max(200, win->height()));
144 
145 }
146 
saveMainWindowBounds(QMainWindow * win)147 void Options::saveMainWindowBounds(QMainWindow* win)
148 {
149    settings_.setValue(kMainWindowGeometry, win->saveGeometry());
150 }
151 
portNumber() const152 QString Options::portNumber() const
153 {
154    // lookup / generate on demand
155    if (portNumber_.length() == 0)
156    {
157       // Use a random-ish port number to avoid collisions between different
158       // instances of rdesktop-launched rsessions
159       int base = std::abs(core::random::uniformRandomInteger<int>());
160       portNumber_ = QString::number((base % 40000) + 8080);
161 
162       // recalculate the local peer and set RS_LOCAL_PEER so that
163       // rsession and it's children can use it
164 #ifdef _WIN32
165       QString localPeer = QString::fromUtf8("\\\\.\\pipe\\") +
166                           portNumber_ + QString::fromUtf8("-rsession");
167       localPeer_ = localPeer.toUtf8().constData();
168       core::system::setenv("RS_LOCAL_PEER", localPeer_);
169 #endif
170    }
171 
172    return portNumber_;
173 }
174 
newPortNumber()175 QString Options::newPortNumber()
176 {
177    portNumber_.clear();
178    return portNumber();
179 }
180 
localPeer() const181 std::string Options::localPeer() const
182 {
183    return localPeer_;
184 }
185 
desktopRenderingEngine() const186 QString Options::desktopRenderingEngine() const
187 {
188    return settings_.value(QStringLiteral("desktop.renderingEngine")).toString();
189 }
190 
setDesktopRenderingEngine(QString engine)191 void Options::setDesktopRenderingEngine(QString engine)
192 {
193    settings_.setValue(QStringLiteral("desktop.renderingEngine"), engine);
194 }
195 
196 namespace {
197 
findFirstMatchingFont(const QStringList & fonts,QString defaultFont,bool fixedWidthOnly)198 QString findFirstMatchingFont(const QStringList& fonts,
199                               QString defaultFont,
200                               bool fixedWidthOnly)
201 {
202    for (int i = 0; i < fonts.size(); i++)
203    {
204       QFont font(fonts.at(i));
205       if (font.exactMatch())
206          if (!fixedWidthOnly || isFixedWidthFont(QFont(fonts.at(i))))
207             return fonts.at(i);
208    }
209    return defaultFont;
210 }
211 
212 } // anonymous namespace
213 
setFont(QString key,QString font)214 void Options::setFont(QString key, QString font)
215 {
216    if (font.isEmpty())
217       settings_.remove(key);
218    else
219       settings_.setValue(key, font);
220 }
221 
setProportionalFont(QString font)222 void Options::setProportionalFont(QString font)
223 {
224    setFont(kProportionalFont, font);
225 }
226 
proportionalFont() const227 QString Options::proportionalFont() const
228 {
229    static QString detectedFont;
230 
231    QString font =
232          settings_.value(kProportionalFont).toString();
233    if (!font.isEmpty())
234    {
235       return font;
236    }
237 
238    if (!detectedFont.isEmpty())
239       return detectedFont;
240 
241    QStringList fontList;
242 #if defined(_WIN32)
243    fontList <<
244            QString::fromUtf8("Segoe UI") << QString::fromUtf8("Verdana") <<  // Windows
245            QString::fromUtf8("Lucida Sans") << QString::fromUtf8("DejaVu Sans") <<  // Linux
246            QString::fromUtf8("Lucida Grande") <<          // Mac
247            QString::fromUtf8("Helvetica");
248 #elif defined(__APPLE__)
249    fontList <<
250            QString::fromUtf8("Lucida Grande") <<          // Mac
251            QString::fromUtf8("Lucida Sans") << QString::fromUtf8("DejaVu Sans") <<  // Linux
252            QString::fromUtf8("Segoe UI") << QString::fromUtf8("Verdana") <<  // Windows
253            QString::fromUtf8("Helvetica");
254 #else
255    fontList <<
256            QString::fromUtf8("Lucida Sans") << QString::fromUtf8("DejaVu Sans") <<  // Linux
257            QString::fromUtf8("Lucida Grande") <<          // Mac
258            QString::fromUtf8("Segoe UI") << QString::fromUtf8("Verdana") <<  // Windows
259            QString::fromUtf8("Helvetica");
260 #endif
261 
262    QString sansSerif = QStringLiteral("sans-serif");
263    QString selectedFont = findFirstMatchingFont(fontList, sansSerif, false);
264 
265    // NOTE: browsers will refuse to render a default font if the name is in
266    // quotes; e.g. "sans-serif" is a signal to look for a font called sans-serif
267    // rather than use the default sans-serif font!
268    if (selectedFont == sansSerif)
269       return sansSerif;
270    else
271       return QStringLiteral("\"%1\"").arg(selectedFont);
272 }
273 
setFixedWidthFont(QString font)274 void Options::setFixedWidthFont(QString font)
275 {
276    setFont(kFixedWidthFont, font);
277 }
278 
fixedWidthFont() const279 QString Options::fixedWidthFont() const
280 {
281    static QString detectedFont;
282 
283    QString font =
284          settings_.value(kFixedWidthFont).toString();
285    if (!font.isEmpty())
286    {
287       return QString::fromUtf8("\"") + font + QString::fromUtf8("\"");
288    }
289 
290    if (!detectedFont.isEmpty())
291       return detectedFont;
292 
293    QStringList fontList;
294    fontList <<
295 #if defined(Q_OS_MAC)
296            QString::fromUtf8("Monaco")
297 #elif defined (Q_OS_LINUX)
298            QString::fromUtf8("Ubuntu Mono") << QString::fromUtf8("Droid Sans Mono") << QString::fromUtf8("DejaVu Sans Mono") << QString::fromUtf8("Monospace")
299 #else
300            QString::fromUtf8("Lucida Console") << QString::fromUtf8("Consolas") // Windows;
301 #endif
302            ;
303 
304    // NOTE: browsers will refuse to render a default font if the name is in
305    // quotes; e.g. "monospace" is a signal to look for a font called monospace
306    // rather than use the default monospace font!
307    QString monospace = QStringLiteral("monospace");
308    QString matchingFont = findFirstMatchingFont(fontList, monospace, true);
309    if (matchingFont == monospace)
310       return monospace;
311    else
312       return QStringLiteral("\"%1\"").arg(matchingFont);
313 }
314 
315 
zoomLevel() const316 double Options::zoomLevel() const
317 {
318    QVariant zoom = settings_.value(QString::fromUtf8("view.zoomLevel"), 1.0);
319    return zoom.toDouble();
320 }
321 
setZoomLevel(double zoomLevel)322 void Options::setZoomLevel(double zoomLevel)
323 {
324    desktopInfo().setZoomLevel(zoomLevel);
325    settings_.setValue(QString::fromUtf8("view.zoomLevel"), zoomLevel);
326 }
327 
enableAccessibility() const328 bool Options::enableAccessibility() const
329 {
330    QVariant accessibility = settings_.value(QString::fromUtf8("view.accessibility"), false);
331    return accessibility.toBool();
332 }
333 
setEnableAccessibility(bool enable)334 void Options::setEnableAccessibility(bool enable)
335 {
336    settings_.setValue(QString::fromUtf8("view.accessibility"), enable);
337 }
338 
clipboardMonitoring() const339 bool Options::clipboardMonitoring() const
340 {
341    QVariant monitoring = settings_.value(QString::fromUtf8("clipboard.monitoring"), true);
342    return monitoring.toBool();
343 }
344 
setClipboardMonitoring(bool monitoring)345 void Options::setClipboardMonitoring(bool monitoring)
346 {
347    settings_.setValue(QString::fromUtf8("clipboard.monitoring"), monitoring);
348 }
349 
ignoreGpuExclusionList() const350 bool Options::ignoreGpuExclusionList() const
351 {
352    QVariant ignore = settings_.value(QStringLiteral("general.ignoreGpuExclusionList"), false);
353    return ignore.toBool();
354 }
355 
setIgnoreGpuExclusionList(bool ignore)356 void Options::setIgnoreGpuExclusionList(bool ignore)
357 {
358    settings_.setValue(QStringLiteral("general.ignoreGpuExclusionList"), ignore);
359 }
360 
disableGpuDriverBugWorkarounds() const361 bool Options::disableGpuDriverBugWorkarounds() const
362 {
363    QVariant disable = settings_.value(QStringLiteral("general.disableGpuDriverBugWorkarounds"), false);
364    return disable.toBool();
365 }
366 
setDisableGpuDriverBugWorkarounds(bool disable)367 void Options::setDisableGpuDriverBugWorkarounds(bool disable)
368 {
369    settings_.setValue(QStringLiteral("general.disableGpuDriverBugWorkarounds"), disable);
370 }
371 
useFontConfigDatabase() const372 bool Options::useFontConfigDatabase() const
373 {
374    QVariant use = settings_.value(QStringLiteral("general.useFontConfigDatabase"), true);
375    return use.toBool();
376 }
377 
setUseFontConfigDatabase(bool use)378 void Options::setUseFontConfigDatabase(bool use)
379 {
380    settings_.setValue(QStringLiteral("general.useFontConfigDatabase"), use);
381 }
382 
383 #ifdef _WIN32
rBinDir() const384 QString Options::rBinDir() const
385 {
386    // HACK: If RBinDir doesn't appear at all, that means the user has never
387    // specified a preference for R64 vs. 32-bit R. In this situation we should
388    // accept either. We'll distinguish between this case (where preferR64
389    // should be ignored) and the other case by using null for this case and
390    // empty string for the other.
391    if (!settings_.contains(QString::fromUtf8("RBinDir")))
392       return QString();
393 
394    QString value = settings_.value(QString::fromUtf8("RBinDir")).toString();
395    return value.isNull() ? QString() : value;
396 }
397 
setRBinDir(QString path)398 void Options::setRBinDir(QString path)
399 {
400    settings_.setValue(QString::fromUtf8("RBinDir"), path);
401 }
402 
preferR64() const403 bool Options::preferR64() const
404 {
405    if (!core::system::isWin64())
406       return false;
407 
408    if (!settings_.contains(QString::fromUtf8("PreferR64")))
409       return true;
410    return settings_.value(QString::fromUtf8("PreferR64")).toBool();
411 }
412 
setPreferR64(bool preferR64)413 void Options::setPreferR64(bool preferR64)
414 {
415    settings_.setValue(QString::fromUtf8("PreferR64"), preferR64);
416 }
417 #endif
418 
scriptsPath() const419 FilePath Options::scriptsPath() const
420 {
421    return scriptsPath_;
422 }
423 
setScriptsPath(const FilePath & scriptsPath)424 void Options::setScriptsPath(const FilePath& scriptsPath)
425 {
426    scriptsPath_ = scriptsPath;
427 }
428 
executablePath() const429 FilePath Options::executablePath() const
430 {
431    if (executablePath_.isEmpty())
432    {
433       Error error = core::system::executablePath(QApplication::arguments().at(0).toUtf8(),
434                                                  &executablePath_);
435       if (error)
436          LOG_ERROR(error);
437    }
438    return executablePath_;
439 }
440 
supportingFilePath() const441 FilePath Options::supportingFilePath() const
442 {
443    if (supportingFilePath_.isEmpty())
444    {
445       // default to install path
446       core::system::installPath("..",
447                                 QApplication::arguments().at(0).toUtf8(),
448                                 &supportingFilePath_);
449 
450       // adapt for OSX resource bundles
451 #ifdef __APPLE__
452          if (supportingFilePath_.completePath("Info.plist").exists())
453             supportingFilePath_ = supportingFilePath_.completePath("Resources");
454 #endif
455    }
456    return supportingFilePath_;
457 }
458 
resourcesPath() const459 FilePath Options::resourcesPath() const
460 {
461    if (resourcesPath_.isEmpty())
462    {
463 #ifdef RSTUDIO_PACKAGE_BUILD
464       // release configuration: the 'resources' folder is
465       // part of the supporting files folder
466       resourcesPath_ = supportingFilePath().completePath("resources");
467 #else
468       // developer configuration: the 'resources' folder is
469       // a sibling of the RStudio executable
470       resourcesPath_ = scriptsPath().completePath("resources");
471 #endif
472    }
473 
474    return resourcesPath_;
475 }
476 
wwwDocsPath() const477 FilePath Options::wwwDocsPath() const
478 {
479    FilePath supportingFilePath = desktop::options().supportingFilePath();
480    FilePath wwwDocsPath = supportingFilePath.completePath("www/docs");
481    if (!wwwDocsPath.exists())
482       wwwDocsPath = supportingFilePath.completePath("../gwt/www/docs");
483 #ifdef __APPLE__
484    if (!wwwDocsPath.exists())
485       wwwDocsPath = supportingFilePath.completePath("../../../../../gwt/www/docs");
486 #endif
487    return wwwDocsPath;
488 }
489 
490 #ifdef _WIN32
491 
urlopenerPath() const492 FilePath Options::urlopenerPath() const
493 {
494    FilePath parentDir = scriptsPath();
495 
496    // detect dev configuration
497    if (parentDir.getFilename() == "desktop")
498       parentDir = parentDir.completePath("urlopener");
499 
500    return parentDir.completePath("urlopener.exe");
501 }
502 
rsinversePath() const503 FilePath Options::rsinversePath() const
504 {
505    FilePath parentDir = scriptsPath();
506 
507    // detect dev configuration
508    if (parentDir.getFilename() == "desktop")
509       parentDir = parentDir.completePath("synctex/rsinverse");
510 
511    return parentDir.completePath("rsinverse.exe");
512 }
513 
514 #endif
515 
ignoredUpdateVersions() const516 QStringList Options::ignoredUpdateVersions() const
517 {
518    return settings_.value(QString::fromUtf8("ignoredUpdateVersions"), QStringList()).toStringList();
519 }
520 
setIgnoredUpdateVersions(const QStringList & ignoredVersions)521 void Options::setIgnoredUpdateVersions(const QStringList& ignoredVersions)
522 {
523    settings_.setValue(QString::fromUtf8("ignoredUpdateVersions"), ignoredVersions);
524 }
525 
scratchTempDir(core::FilePath defaultPath)526 core::FilePath Options::scratchTempDir(core::FilePath defaultPath)
527 {
528    core::FilePath dir(scratchPath.toUtf8().constData());
529 
530    if (!dir.isEmpty() && dir.exists())
531    {
532       dir = dir.completeChildPath("tmp");
533       core::Error error = dir.ensureDirectory();
534       if (!error)
535          return dir;
536    }
537    return defaultPath;
538 }
539 
cleanUpScratchTempDir()540 void Options::cleanUpScratchTempDir()
541 {
542    core::FilePath temp = scratchTempDir(core::FilePath());
543    if (!temp.isEmpty())
544       temp.removeIfExists();
545 }
546 
lastRemoteSessionUrl(const QString & serverUrl)547 QString Options::lastRemoteSessionUrl(const QString& serverUrl)
548 {
549    settings_.beginGroup(QString::fromUtf8("remote-sessions-list"));
550    QString sessionUrl = settings_.value(serverUrl).toString();
551    settings_.endGroup();
552    return sessionUrl;
553 }
554 
setLastRemoteSessionUrl(const QString & serverUrl,const QString & sessionUrl)555 void Options::setLastRemoteSessionUrl(const QString& serverUrl, const QString& sessionUrl)
556 {
557    settings_.beginGroup(QString::fromUtf8("remote-sessions-list"));
558    settings_.setValue(serverUrl, sessionUrl);
559    settings_.endGroup();
560 }
561 
cookiesFromList(const QStringList & cookieStrs) const562 QList<QNetworkCookie> Options::cookiesFromList(const QStringList& cookieStrs) const
563 {
564    QList<QNetworkCookie> cookies;
565 
566    for (const QString& cookieStr : cookieStrs)
567    {
568       QByteArray cookieArr = QByteArray::fromStdString(cookieStr.toStdString());
569       QList<QNetworkCookie> innerCookies = QNetworkCookie::parseCookies(cookieArr);
570       for (const QNetworkCookie& cookie : innerCookies)
571       {
572          cookies.push_back(cookie);
573       }
574    }
575 
576    return cookies;
577 }
578 
authCookies() const579 QList<QNetworkCookie> Options::authCookies() const
580 {
581    QStringList cookieStrs = settings_.value(QString::fromUtf8("cookies"), QStringList()).toStringList();
582    return cookiesFromList(cookieStrs);
583 }
584 
tempAuthCookies() const585 QList<QNetworkCookie> Options::tempAuthCookies() const
586 {
587    QStringList cookieStrs = settings_.value(QString::fromUtf8("temp-auth-cookies"), QStringList()).toStringList();
588    return cookiesFromList(cookieStrs);
589 }
590 
cookiesToList(const QList<QNetworkCookie> & cookies) const591 QStringList Options::cookiesToList(const QList<QNetworkCookie>& cookies) const
592 {
593    QStringList cookieStrs;
594    for (const QNetworkCookie& cookie : cookies)
595    {
596       cookieStrs.push_back(QString::fromStdString(cookie.toRawForm().toStdString()));
597    }
598 
599    return cookieStrs;
600 }
601 
setAuthCookies(const QList<QNetworkCookie> & cookies)602 void Options::setAuthCookies(const QList<QNetworkCookie>& cookies)
603 {
604    settings_.setValue(QString::fromUtf8("cookies"), cookiesToList(cookies));
605 }
606 
setTempAuthCookies(const QList<QNetworkCookie> & cookies)607 void Options::setTempAuthCookies(const QList<QNetworkCookie>& cookies)
608 {
609    settings_.setValue(QString::fromUtf8("temp-auth-cookies"), cookiesToList(cookies));
610 }
611 
612 } // namespace desktop
613 } // namespace rstudio
614 
615