Serenity Operating System
at master 409 lines 14 kB view raw
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}