1/*
2 * xdg.ts
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/*
17 * These routines return system and user paths for RStudio configuration and data, roughly in
18 * accordance with the FreeDesktop XDG Base Directory Specification.
19 *
20 * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
21 *
22 * All of these can be configured with environment variables as described below.
23 *
24 * The values of the environment variables can include the following special variables:
25 *
26 * $USER  The user's name
27 * $HOME  The user's home directory
28 * ~      The user's home directory
29 *
30 * These values will be resolved against the current user by default. If you wish to
31 * resolve them against a different user, supply their name and home directory using
32 * the boost::optional arguments.
33 */
34
35import os from 'os';
36
37import { logger } from './logger';
38import { Environment, expandEnvVars, getenv } from './environment';
39import { username, userHomePath } from './user';
40import { FilePath } from './file-path';
41
42export enum WinFolderID {
43  FOLDERID_RoamingAppData,
44  FOLDERID_LocalAppData,
45  FOLDERID_ProgramData
46}
47
48/**
49 * Simplified implementation of Windows `SHGetKnownFolderPath` API using environment
50 * variables.
51 */
52export function SHGetKnownFolderPath(folderId: WinFolderID): string {
53  let envVar = '';
54  switch (folderId) {
55  case WinFolderID.FOLDERID_RoamingAppData:
56    envVar = 'APPDATA';
57    break;
58  case WinFolderID.FOLDERID_LocalAppData:
59    envVar = 'LOCALAPPDATA';
60    break;
61  case WinFolderID.FOLDERID_ProgramData:
62    envVar = 'ProgramData';
63    break;
64  }
65  return getenv(envVar);
66}
67
68// Store the hostname so we don't have to look it up multiple times
69let hostname = '';
70
71/**
72 * Returns the hostname from the operating system
73 */
74function getHostname(): string {
75  if (!hostname)
76    hostname = os.hostname();
77  return hostname;
78}
79
80/**
81 * Resolves an XDG directory based on the user and environment.
82 *
83 * `rstudioEnvVer` The RStudio-specific environment variable specifying
84 *   the directory (given precedence)
85 *
86 * `xdgEnvVar` The XDG standard environment variable
87 *
88 * `defaultDir` Fallback default directory if neither environment variable
89 *   is present
90 *
91 * `windowsFolderId` The ID of the Windows folder to resolve against
92 *
93 * `user` Optionally, the user to return a directory for; if omitted the
94 *   current user is used
95 *
96 * `homeDir` Optionally, the home directory to resolve against; if omitted
97 *   the current user's home directory is used
98 */
99function resolveXdgDir(
100  rstudioEnvVar: string,
101  xdgEnvVar: string,
102  windowsFolderId: WinFolderID,
103  defaultDir: string,
104  user?: string,
105  homeDir?: FilePath
106): FilePath {
107  let xdgHome = new FilePath();
108  let finalPath = true;
109
110  // Look for the RStudio-specific environment variable
111  let env = getenv(rstudioEnvVar);
112  if (!env) {
113    // The RStudio environment variable specifies the final path; if it isn't
114    // set we will need to append "rstudio" to the path later.
115    finalPath = false;
116    env = getenv(xdgEnvVar);
117  }
118
119  if (!env) {
120    // No root specified for xdg home; we will need to generate one.
121    if (process.platform === 'win32') {
122      // On Windows, the default path is in Application Data/Roaming.
123      const path = SHGetKnownFolderPath(windowsFolderId);
124      if (path) {
125        xdgHome = new FilePath(path);
126      } else {
127        logger().logError(new Error(`Unable to retrieve app settings path (${windowsFolderId}).`));
128      }
129    }
130    if (xdgHome.isEmpty()) {
131      // Use the default subdir for POSIX. We also use this folder as a fallback on Windows
132      //if we couldn't read the app settings path.
133      xdgHome = new FilePath(defaultDir);
134    }
135  } else {
136    // We have a manually specified xdg directory from an environment variable.
137    xdgHome = new FilePath(env);
138  }
139
140  // expand HOME, USER, and HOSTNAME if given
141  const environment: Environment = {
142    'HOME': homeDir ? homeDir.getAbsolutePath() : userHomePath().getAbsolutePath(),
143    'USER': user ? user : username()
144  };
145
146  // check for manually specified hostname in environment variable
147  let hostname = getenv('HOSTNAME');
148
149  // when omitted, look up the hostname using a system call
150  if (!hostname)
151    hostname = getHostname();
152
153  environment.HOSTNAME = hostname;
154
155  const expanded = expandEnvVars(environment, xdgHome.getAbsolutePath());
156
157  // resolve aliases in the path
158  xdgHome = FilePath.resolveAliasedPathSync(expanded, homeDir ? homeDir : userHomePath());
159
160  // If this is the final path, we can return it as-is
161  if (finalPath)
162    return xdgHome;
163
164  // Otherwise, it's a root folder in which we need to create our own subfolder
165  const folderName = process.platform === 'win32' ? 'RStudio' : 'rstudio';
166  return xdgHome.completePath(folderName);
167}
168
169export class Xdg {
170
171  /**
172   * Returns the RStudio XDG user config directory.
173   *
174   * On Unix-alikes, this is `~/.config/rstudio`, or `XDG_CONFIG_HOME`.
175   * On Windows, this is `FOLDERID_RoamingAppData` (typically `AppData/Roaming`).
176   */
177  static userConfigDir(user?: string, homeDir?: FilePath): FilePath {
178    return resolveXdgDir(
179      'RSTUDIO_CONFIG_HOME',
180      'XDG_CONFIG_HOME',
181      WinFolderID.FOLDERID_RoamingAppData,
182      '~/.config',
183      user,
184      homeDir
185    );
186  }
187
188  /**
189   * Returns the RStudio XDG user data directory.
190   *
191   * On Unix-alikes, this is `~/.local/share/rstudio`, or `XDG_DATA_HOME`.
192   * On Windows, this is `FOLDERID_LocalAppData` (typically `AppData/Local`).
193   */
194  static userDataDir(user?: string, homeDir?: FilePath): FilePath {
195    return resolveXdgDir(
196      'RSTUDIO_DATA_HOME',
197      'XDG_DATA_HOME',
198      WinFolderID.FOLDERID_LocalAppData,
199      '~/.local/share',
200      user,
201      homeDir
202    );
203  }
204
205  /**
206   * This function verifies that the `userConfigDir()` and `userDataDir()` exist and are
207   * owned by the running user.
208   *
209   * It should be invoked once. Any issues with these directories will be emitted to the
210   * session log.
211   */
212  static verifyUserDirs(
213    // eslint-disable-next-line @typescript-eslint/no-unused-vars
214    user?: string,
215    // eslint-disable-next-line @typescript-eslint/no-unused-vars
216    homeDir?: FilePath
217  ): void {
218    throw Error('Xdg.verifyUserDirs is NYI');
219  }
220
221
222  /**
223   * Returns the RStudio XDG system config directory.
224   *
225   * On Unix-alikes, this is `/etc/rstudio`, `XDG_CONFIG_DIRS`.
226   * On Windows, this is `FOLDERID_ProgramData` (typically `C:/ProgramData`).
227   */
228  static systemConfigDir(): FilePath {
229    if (process.platform !== 'win32') {
230      if (!getenv('RSTUDIO_CONFIG_DIR')) {
231        // On POSIX operating systems, it's possible to specify multiple config
232        // directories. We have to select one, so read the list and take the first
233        // one that contains an "rstudio" folder.
234        const env = getenv('XDG_CONFIG_DIRS');
235        if (env.indexOf(':') >= 0) {
236          const dirs = env.split(':');
237          for (const dir of dirs) {
238            const resolved = new FilePath(dir).completePath('rstudio');
239            if (resolved.existsSync()) {
240              return resolved;
241            }
242          }
243        }
244      }
245    }
246    return resolveXdgDir(
247      'RSTUDIO_CONFIG_DIR',
248      'XDG_CONFIG_DIRS',
249      WinFolderID.FOLDERID_ProgramData,
250      '/etc',
251      undefined,  // no specific user
252      undefined   // no home folder resolution
253    );
254  }
255
256  /**
257   * Convenience method for finding a configuration file. Checks all the
258   * directories in `XDG_CONFIG_DIRS` for the file. If it doesn't find it,
259   * the path where we expected to find it is returned instead.
260   */
261  static systemConfigFile(filename: string): FilePath {
262    if (process.platform === 'win32') {
263      // Passthrough on Windows
264      return Xdg.systemConfigDir().completeChildPath(filename);
265    }
266    if (!getenv('RSTUDIO_CONFIG_DIR')) {
267      // On POSIX, check for a search path.
268      const env = getenv('XDG_CONFIG_DIRS');
269      if (env.indexOf(':') >= 0) {
270        // This is a search path; check each element for the file.
271        const dirs = env.split(':');
272        for (const dir of dirs) {
273          const resolved = new FilePath(dir).completePath('rstudio').completeChildPath(filename);
274          if (resolved.existsSync()) {
275            return resolved;
276          }
277        }
278      }
279    }
280
281    // We didn't find the file on the search path, so return the location where
282    // we expected to find it.
283    return Xdg.systemConfigDir().completeChildPath(filename);
284  }
285
286  /**
287   * Sets relevant XDG environment variables
288   */
289  // eslint-disable-next-line @typescript-eslint/no-unused-vars
290  static forwardXdgEnvVars(pEnvironment: Environment): void {
291    throw Error('forwardXdgEnvVars is NYI');
292  }
293}