Serenity Operating System
1/*
2 * Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "Launcher.h"
8#include <AK/Function.h>
9#include <AK/JsonObject.h>
10#include <AK/JsonObjectSerializer.h>
11#include <AK/JsonValue.h>
12#include <AK/LexicalPath.h>
13#include <AK/StringBuilder.h>
14#include <LibCore/ConfigFile.h>
15#include <LibCore/DeprecatedFile.h>
16#include <LibCore/MimeData.h>
17#include <LibCore/Process.h>
18#include <LibDesktop/AppFile.h>
19#include <errno.h>
20#include <serenity.h>
21#include <spawn.h>
22#include <stdio.h>
23#include <sys/stat.h>
24
25namespace LaunchServer {
26
27static Launcher* s_the;
28static bool spawn(DeprecatedString executable, Vector<DeprecatedString> const& arguments);
29
30DeprecatedString Handler::name_from_executable(StringView executable)
31{
32 auto separator = executable.find_last('/');
33 if (separator.has_value()) {
34 auto start = separator.value() + 1;
35 return executable.substring_view(start, executable.length() - start);
36 }
37 return executable;
38}
39
40void Handler::from_executable(Type handler_type, DeprecatedString const& executable)
41{
42 this->handler_type = handler_type;
43 this->name = name_from_executable(executable);
44 this->executable = executable;
45}
46
47DeprecatedString Handler::to_details_str() const
48{
49 StringBuilder builder;
50 auto obj = MUST(JsonObjectSerializer<>::try_create(builder));
51 MUST(obj.add("executable"sv, executable));
52 MUST(obj.add("name"sv, name));
53 switch (handler_type) {
54 case Type::Application:
55 MUST(obj.add("type"sv, "app"));
56 break;
57 case Type::UserDefault:
58 MUST(obj.add("type"sv, "userdefault"));
59 break;
60 case Type::UserPreferred:
61 MUST(obj.add("type"sv, "userpreferred"));
62 break;
63 default:
64 break;
65 }
66 MUST(obj.finish());
67 return builder.to_deprecated_string();
68}
69
70Launcher::Launcher()
71{
72 VERIFY(s_the == nullptr);
73 s_the = this;
74}
75
76Launcher& Launcher::the()
77{
78 VERIFY(s_the);
79 return *s_the;
80}
81
82void Launcher::load_handlers(DeprecatedString const& af_dir)
83{
84 Desktop::AppFile::for_each([&](auto af) {
85 auto app_name = af->name();
86 auto app_executable = af->executable();
87 HashTable<DeprecatedString> mime_types;
88 for (auto& mime_type : af->launcher_mime_types())
89 mime_types.set(mime_type);
90 HashTable<DeprecatedString> file_types;
91 for (auto& file_type : af->launcher_file_types())
92 file_types.set(file_type);
93 HashTable<DeprecatedString> protocols;
94 for (auto& protocol : af->launcher_protocols())
95 protocols.set(protocol);
96 if (access(app_executable.characters(), X_OK) == 0)
97 m_handlers.set(app_executable, { Handler::Type::Default, app_name, app_executable, mime_types, file_types, protocols });
98 },
99 af_dir);
100}
101
102void Launcher::load_config(Core::ConfigFile const& cfg)
103{
104 for (auto key : cfg.keys("MimeType")) {
105 auto handler = cfg.read_entry("MimeType", key).trim_whitespace();
106 if (handler.is_empty())
107 continue;
108 if (access(handler.characters(), X_OK) != 0)
109 continue;
110 m_mime_handlers.set(key.to_lowercase(), handler);
111 }
112
113 for (auto key : cfg.keys("FileType")) {
114 auto handler = cfg.read_entry("FileType", key).trim_whitespace();
115 if (handler.is_empty())
116 continue;
117 if (access(handler.characters(), X_OK) != 0)
118 continue;
119 m_file_handlers.set(key.to_lowercase(), handler);
120 }
121
122 for (auto key : cfg.keys("Protocol")) {
123 auto handler = cfg.read_entry("Protocol", key).trim_whitespace();
124 if (handler.is_empty())
125 continue;
126 if (access(handler.characters(), X_OK) != 0)
127 continue;
128 m_protocol_handlers.set(key.to_lowercase(), handler);
129 }
130}
131
132bool Launcher::has_mime_handlers(DeprecatedString const& mime_type)
133{
134 for (auto& handler : m_handlers)
135 if (handler.value.mime_types.contains(mime_type))
136 return true;
137 return false;
138}
139
140Vector<DeprecatedString> Launcher::handlers_for_url(const URL& url)
141{
142 Vector<DeprecatedString> handlers;
143 if (url.scheme() == "file") {
144 for_each_handler_for_path(url.path(), [&](auto& handler) -> bool {
145 handlers.append(handler.executable);
146 return true;
147 });
148 } else {
149 for_each_handler(url.scheme(), m_protocol_handlers, [&](auto const& handler) -> bool {
150 if (handler.handler_type != Handler::Type::Default || handler.protocols.contains(url.scheme())) {
151 handlers.append(handler.executable);
152 return true;
153 }
154 return false;
155 });
156 }
157 return handlers;
158}
159
160Vector<DeprecatedString> Launcher::handlers_with_details_for_url(const URL& url)
161{
162 Vector<DeprecatedString> handlers;
163 if (url.scheme() == "file") {
164 for_each_handler_for_path(url.path(), [&](auto& handler) -> bool {
165 handlers.append(handler.to_details_str());
166 return true;
167 });
168 } else {
169 for_each_handler(url.scheme(), m_protocol_handlers, [&](auto const& handler) -> bool {
170 if (handler.handler_type != Handler::Type::Default || handler.protocols.contains(url.scheme())) {
171 handlers.append(handler.to_details_str());
172 return true;
173 }
174 return false;
175 });
176 }
177 return handlers;
178}
179
180Optional<DeprecatedString> Launcher::mime_type_for_file(DeprecatedString path)
181{
182 auto file_or_error = Core::DeprecatedFile::open(path, Core::OpenMode::ReadOnly);
183 if (file_or_error.is_error()) {
184 return {};
185 } else {
186 auto file = file_or_error.release_value();
187 // Read accounts for longest possible offset + signature we currently match against.
188 auto bytes = file->read(0x9006);
189
190 return Core::guess_mime_type_based_on_sniffed_bytes(bytes.bytes());
191 }
192}
193
194bool Launcher::open_url(const URL& url, DeprecatedString const& handler_name)
195{
196 if (!handler_name.is_null())
197 return open_with_handler_name(url, handler_name);
198
199 if (url.scheme() == "file")
200 return open_file_url(url);
201
202 return open_with_user_preferences(m_protocol_handlers, url.scheme(), { url.to_deprecated_string() });
203}
204
205bool Launcher::open_with_handler_name(const URL& url, DeprecatedString const& handler_name)
206{
207 auto handler_optional = m_handlers.get(handler_name);
208 if (!handler_optional.has_value())
209 return false;
210
211 auto& handler = handler_optional.value();
212 DeprecatedString argument;
213 if (url.scheme() == "file")
214 argument = url.path();
215 else
216 argument = url.to_deprecated_string();
217 return spawn(handler.executable, { argument });
218}
219
220bool spawn(DeprecatedString executable, Vector<DeprecatedString> const& arguments)
221{
222 return !Core::Process::spawn(executable, arguments).is_error();
223}
224
225Handler Launcher::get_handler_for_executable(Handler::Type handler_type, DeprecatedString const& executable) const
226{
227 Handler handler;
228 auto existing_handler = m_handlers.get(executable);
229 if (existing_handler.has_value()) {
230 handler = existing_handler.value();
231 handler.handler_type = handler_type;
232 } else {
233 handler.from_executable(handler_type, executable);
234 }
235 return handler;
236}
237
238bool Launcher::open_with_user_preferences(HashMap<DeprecatedString, DeprecatedString> const& user_preferences, DeprecatedString const& key, Vector<DeprecatedString> const& arguments, DeprecatedString const& default_program)
239{
240 auto program_path = user_preferences.get(key);
241 if (program_path.has_value())
242 return spawn(program_path.value(), arguments);
243
244 DeprecatedString executable = "";
245 if (for_each_handler(key, user_preferences, [&](auto const& handler) -> bool {
246 if (executable.is_empty() && (handler.mime_types.contains(key) || handler.file_types.contains(key) || handler.protocols.contains(key))) {
247 executable = handler.executable;
248 return true;
249 }
250 return false;
251 })) {
252 return spawn(executable, arguments);
253 }
254
255 // There wasn't a handler for this, so try the fallback instead
256 program_path = user_preferences.get("*");
257 if (program_path.has_value())
258 return spawn(program_path.value(), arguments);
259
260 // Absolute worst case, try the provided default program, if any
261 if (!default_program.is_empty())
262 return spawn(default_program, arguments);
263
264 return false;
265}
266
267size_t Launcher::for_each_handler(DeprecatedString const& key, HashMap<DeprecatedString, DeprecatedString> const& user_preference, Function<bool(Handler const&)> f)
268{
269 auto user_preferred = user_preference.get(key);
270 if (user_preferred.has_value())
271 f(get_handler_for_executable(Handler::Type::UserPreferred, user_preferred.value()));
272
273 size_t counted = 0;
274 for (auto& handler : m_handlers) {
275 // Skip over the existing item in the list
276 if (user_preferred.has_value() && user_preferred.value() == handler.value.executable)
277 continue;
278 if (f(handler.value))
279 counted++;
280 }
281
282 auto user_default = user_preference.get("*");
283 if (counted == 0 && user_default.has_value())
284 f(get_handler_for_executable(Handler::Type::UserDefault, user_default.value()));
285 // Return the number of times f() was called,
286 // which can be used to know whether there were any handlers
287 return counted;
288}
289
290void Launcher::for_each_handler_for_path(DeprecatedString const& path, Function<bool(Handler const&)> f)
291{
292 struct stat st;
293 if (lstat(path.characters(), &st) < 0) {
294 perror("lstat");
295 return;
296 }
297
298 if (S_ISDIR(st.st_mode)) {
299 auto handler_optional = m_file_handlers.get("directory");
300 if (!handler_optional.has_value())
301 return;
302 auto& handler = handler_optional.value();
303 f(get_handler_for_executable(Handler::Type::Default, handler));
304 return;
305 }
306
307 if (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode))
308 return;
309
310 if (S_ISLNK(st.st_mode)) {
311 auto link_target_or_error = Core::DeprecatedFile::read_link(path);
312 if (link_target_or_error.is_error()) {
313 perror("read_link");
314 return;
315 }
316
317 auto link_target = LexicalPath { link_target_or_error.release_value() };
318 LexicalPath absolute_link_target = link_target.is_absolute() ? link_target : LexicalPath::join(LexicalPath::dirname(path), link_target.string());
319 auto real_path = Core::DeprecatedFile::real_path_for(absolute_link_target.string());
320 return for_each_handler_for_path(real_path, [&](auto const& handler) -> bool {
321 return f(handler);
322 });
323 }
324
325 if ((st.st_mode & S_IFMT) == S_IFREG && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
326 f(get_handler_for_executable(Handler::Type::Application, path));
327
328 auto extension = LexicalPath::extension(path).to_lowercase();
329 auto mime_type = mime_type_for_file(path);
330
331 if (mime_type.has_value()) {
332 if (for_each_handler(mime_type.value(), m_mime_handlers, [&](auto const& handler) -> bool {
333 if (handler.handler_type != Handler::Type::Default || handler.mime_types.contains(mime_type.value()))
334 return f(handler);
335 return false;
336 })) {
337 return;
338 }
339 }
340
341 for_each_handler(extension, m_file_handlers, [&](auto const& handler) -> bool {
342 if (handler.handler_type != Handler::Type::Default || handler.file_types.contains(extension))
343 return f(handler);
344 return false;
345 });
346}
347
348bool Launcher::open_file_url(const URL& url)
349{
350 struct stat st;
351 if (stat(url.path().characters(), &st) < 0) {
352 perror("stat");
353 return false;
354 }
355
356 if (S_ISDIR(st.st_mode)) {
357 Vector<DeprecatedString> fm_arguments;
358 if (url.fragment().is_empty()) {
359 fm_arguments.append(url.path());
360 } else {
361 fm_arguments.append("-s");
362 fm_arguments.append("-r");
363 fm_arguments.append(DeprecatedString::formatted("{}/{}", url.path(), url.fragment()));
364 }
365
366 auto handler_optional = m_file_handlers.get("directory");
367 if (!handler_optional.has_value())
368 return false;
369 auto& handler = handler_optional.value();
370
371 return spawn(handler, fm_arguments);
372 }
373
374 if ((st.st_mode & S_IFMT) == S_IFREG && st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
375 return spawn(url.path(), {});
376
377 auto extension = LexicalPath::extension(url.path()).to_lowercase();
378 auto mime_type = mime_type_for_file(url.path());
379
380 auto mime_type_or_extension = extension;
381 bool should_use_mime_type = mime_type.has_value() && has_mime_handlers(mime_type.value());
382 if (should_use_mime_type)
383 mime_type_or_extension = mime_type.value();
384
385 auto handler_optional = m_file_handlers.get("txt");
386 DeprecatedString default_handler = "";
387 if (handler_optional.has_value())
388 default_handler = handler_optional.value();
389
390 // Additional parameters parsing, specific for the file protocol and txt file handlers
391 Vector<DeprecatedString> additional_parameters;
392 DeprecatedString filepath = url.path();
393
394 auto parameters = url.query().split('&');
395 for (auto const& parameter : parameters) {
396 auto pair = parameter.split('=');
397 if (pair.size() == 2 && pair[0] == "line_number") {
398 auto line = pair[1].to_int();
399 if (line.has_value())
400 // TextEditor uses file:line:col to open a file at a specific line number
401 filepath = DeprecatedString::formatted("{}:{}", filepath, line.value());
402 }
403 }
404
405 additional_parameters.append(filepath);
406
407 return open_with_user_preferences(should_use_mime_type ? m_mime_handlers : m_file_handlers, mime_type_or_extension, additional_parameters, default_handler);
408}
409}