1 /** 2 Copyright: © 2013 rejectedsoftware e.K. 3 License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file. 4 Authors: Sönke Ludwig 5 */ 6 module dubregistry.web; 7 8 import dubregistry.dbcontroller; 9 import dubregistry.internal.utils; 10 import dubregistry.repositories.bitbucket; 11 import dubregistry.repositories.github; 12 import dubregistry.registry; 13 import dubregistry.viewutils; // dummy import to make rdmd happy 14 15 import dub.semver; 16 import std.algorithm : sort, startsWith, splitter; 17 import std.array; 18 import std.file; 19 import std.path; 20 import std..string; 21 import userman.web; 22 import vibe.d; 23 24 25 DubRegistryWebFrontend registerDubRegistryWebFrontend(URLRouter router, DubRegistry registry, UserManController userman) 26 { 27 DubRegistryWebFrontend webfrontend; 28 if (userman) { 29 auto ff = new DubRegistryFullWebFrontend(registry, userman); 30 webfrontend = ff; 31 router.registerWebInterface(ff); 32 router.registerUserManWebInterface(userman); 33 } else { 34 webfrontend = new DubRegistryWebFrontend(registry, userman); 35 router.registerWebInterface(webfrontend); 36 } 37 router.get("*", serveStaticFiles("./public")); 38 return webfrontend; 39 } 40 41 class DubRegistryWebFrontend { 42 protected { 43 static struct CachedPackageSlot { 44 static struct Version { 45 string version_; 46 SysTime date; 47 } 48 49 BsonObjectID id; 50 string name; 51 DbPackageStats stats; 52 string[] categories; 53 Version[] versions; 54 @property SysTime dateAdded() { return id.timeStamp; } 55 } 56 57 DubRegistry m_registry; 58 UserManController m_userman; 59 Category[] m_categories; 60 Category[string] m_categoryMap; 61 CachedPackageSlot[] m_packages; 62 } 63 64 this(DubRegistry registry, UserManController userman) 65 { 66 m_registry = registry; 67 m_userman = userman; 68 updateCategories(); 69 updatePackageList(); 70 setTimer(30.seconds, &updatePackageList, true); 71 } 72 73 @path("/") 74 void getHome(string sort = "updated", string category = null, ulong skip = 0, ulong limit = 20) 75 { 76 import std.algorithm.comparison : min; 77 import std.algorithm.iteration : filter, map; 78 79 static struct Info { 80 // TODO: No need to use untyped Json 81 static struct Package { DbPackageStats stats; Json _; alias _ this; } 82 Package[] packages; 83 size_t packageCount; 84 ulong skip; 85 ulong limit; 86 Category[] categories; 87 Category[string] categoryMap; 88 } 89 90 static import std.algorithm.sorting; 91 import std.algorithm.searching : any; 92 93 CachedPackageSlot[] packages; 94 if (category.length) { 95 packages = m_packages 96 .filter!(p => p.categories.any!(c => c.startsWith(category))) 97 .array; 98 } else { 99 packages = m_packages.dup; 100 } 101 102 // sort by date of last version 103 SysTime getDate(in ref CachedPackageSlot p) { 104 if (p.versions.length == 0) return SysTime(0, UTC()); 105 return p.versions[$-1].date; 106 } 107 bool compare(in ref CachedPackageSlot a, in ref CachedPackageSlot b) { 108 bool a_has_ver = a.versions.any!(v => !v.version_.startsWith("~")); 109 bool b_has_ver = b.versions.any!(v => !v.version_.startsWith("~")); 110 if (a_has_ver != b_has_ver) return a_has_ver; 111 return getDate(a) > getDate(b); 112 } 113 switch (sort) { 114 default: std.algorithm.sorting.sort!compare(packages); break; 115 case "name": std.algorithm.sorting.sort!((a, b) => a.name < b.name)(packages); break; 116 case "score": std.algorithm.sorting.sort!((a, b) => a.stats.score > b.stats.score)(packages); break; 117 case "added": std.algorithm.sorting.sort!((a, b) => a.dateAdded > b.dateAdded)(packages); break; 118 } 119 120 // limit package list to current page 121 size_t pcnt = packages.length; 122 packages = packages[min(skip, $) .. min(skip + limit, $)]; 123 124 // collect package infos 125 Json[string] infos; 126 foreach (p; m_registry.getPackageInfos(packages.map!(p => p.name).array)) 127 infos[p.info["name"].opt!string] = p.info; 128 129 Info info; 130 info.packageCount = pcnt; 131 info.packages = packages 132 .map!(p => Info.Package(p.stats, infos.get(p.name, Json.init))) 133 .array; 134 info.skip = skip; 135 info.limit = limit; 136 info.categories = m_categories; 137 info.categoryMap = m_categoryMap; 138 139 render!("home.dt", info); 140 } 141 142 // compatibility route 143 void getAvailable(HTTPServerRequest req, HTTPServerResponse res) 144 { 145 res.redirect("/packages/index.json"); 146 } 147 148 @path("/packages/index.json") 149 void getPackages(HTTPServerRequest req, HTTPServerResponse res) 150 { 151 res.writeJsonBody(m_registry.availablePackages.array); 152 } 153 154 @path("/view_package/:packname") 155 void getRedirectViewPackage(string _packname) 156 { 157 redirect("/packages/"~_packname); 158 } 159 160 @path("/packages/:packname") 161 void getPackage(HTTPServerRequest req, HTTPServerResponse res, string _packname) 162 { 163 getPackageVersion(req, res, _packname, null); 164 } 165 166 @path("/packages/:packname/logo") 167 void getPackageLogo(HTTPServerRequest req, HTTPServerResponse res, string _packname) 168 { 169 bdata_t rev, logo; 170 logo = m_registry.getPackageLogo(_packname, rev); 171 172 if (logo.length) { 173 // TODO: add caching, etag (using rev), content-control, etc. 174 res.writeBody(logo, "image/png"); 175 } else { 176 bool acceptsSVG; 177 // make sure requester actually supports svg, if a custom IDE or tool for fetching images is used it should only get png if it doesn't support svg. 178 // if requester is an IDE sending Accept: */*, but not accepting SVG it's their own fault. 179 foreach (accept; req.headers.get("Accept", "").splitter(",")) { 180 if (accept.startsWith("image/*", "image/svg", "*/*", "*/svg")) { 181 acceptsSVG = true; 182 break; 183 } 184 } 185 auto settings = new HTTPFileServerSettings(); 186 if (acceptsSVG) 187 sendFile(req, res, NativePath("public/images/default-logo.svg"), settings); 188 else 189 sendFile(req, res, NativePath("public/images/default-logo.png"), settings); 190 } 191 } 192 193 @path("/packages/:packname/:version") 194 void getPackageVersion(HTTPServerRequest req, HTTPServerResponse res, string _packname, string _version) 195 { 196 import std.algorithm : canFind; 197 198 auto pname = _packname; 199 auto ver = _version.replace(" ", "+"); 200 string ext; 201 202 if (_version.length) { 203 if (ver.endsWith(".zip")) ext = "zip", ver = ver[0 .. $-4]; 204 else if( ver.endsWith(".json") ) ext = "json", ver = ver[0 .. $-5]; 205 } else { 206 if (pname.endsWith(".json")) { 207 pname = pname[0 .. $-5]; 208 ext = "json"; 209 } 210 } 211 212 PackageInfo packinfo; 213 PackageVersionInfo verinfo; 214 if (!getPackageInfo(pname, ver, packinfo, verinfo)) 215 return; 216 217 auto packageInfo = packinfo.info; 218 auto versionInfo = verinfo.info; 219 220 if (ext == "zip") { 221 if (pname.canFind(":")) return; 222 223 // This log line is a weird workaround to make otherwise undefined Json fields 224 // available. Smells like a compiler bug. 225 logDebug("%s %s", packageInfo["id"].toString(), verinfo.downloadURL); 226 227 // add download to statistic 228 m_registry.addDownload(BsonObjectID.fromString(packageInfo["id"].get!string), ver, req.headers.get("User-agent", null)); 229 if (verinfo.downloadURL.length > 0) { 230 // redirect to hosting service specific URL 231 redirect(verinfo.downloadURL); 232 } else { 233 // directly forward from hoster 234 res.headers["Content-Disposition"] = "attachment; filename=\""~pname~"-"~(ver.startsWith("~") ? ver[1 .. $] : ver) ~ ".zip\""; 235 m_registry.downloadPackageZip(pname, ver.startsWith("~") ? ver : "v"~ver, (scope data) { 236 res.writeBody(data, "application/zip"); 237 }); 238 } 239 } else if (ext == "json") { 240 if (pname.canFind(":")) return; 241 res.writeJsonBody(_version.length ? versionInfo : packageInfo); 242 } else { 243 User user; 244 if (m_userman) { 245 try user = m_userman.getUser(User.ID.fromString(packageInfo["owner"].get!string)); 246 catch (Exception e) { 247 logDebug("Failed to get owner '%s' for %s %s: %s", 248 packageInfo["owner"].get!string, pname, ver, e.msg); 249 } 250 } 251 252 auto gitVer = verinfo.version_; 253 gitVer = gitVer.startsWith("~") ? gitVer[1 .. $] : "v"~gitVer; 254 string urlFilter(string url, bool is_image) 255 { 256 if (url.startsWith("http://") || url.startsWith("https://")) 257 return url; 258 259 if (auto pr = "repository" in packageInfo) { 260 auto owner = (*pr)["owner"].get!string; 261 auto project = (*pr)["project"].get!string; 262 switch ((*pr)["kind"].get!string) { 263 default: return url; 264 // TODO: BitBucket + GitLab 265 case "github": 266 if (is_image) return format("https://github.com/%s/%s/raw/%s/%s", owner, project, gitVer, url); 267 else return format("https://github.com/%s/%s/blob/%s/%s", owner, project, gitVer, url.startsWith("#") ? "README.md" ~ url : url); 268 } 269 } 270 271 return url; 272 } 273 274 auto packageName = pname; 275 auto registry = m_registry; 276 auto readmeContents = m_registry.getReadme(versionInfo, packageInfo["repository"].deserializeJson!DbRepository); 277 //auto sampleURLs = ["test1", "test2"]; /* TODO: actually make this array exist and embed samples generated from repository */ 278 string[] sampleURLs; 279 auto activeTab = req.query.get("tab", "info"); 280 render!("view_package.dt", packageName, user, packinfo, versionInfo, readmeContents, sampleURLs, urlFilter, registry, activeTab); 281 } 282 } 283 284 @path("/packages/:packname/versions") 285 void getAllPackageVersions(HTTPServerRequest req, HTTPServerResponse res, string _packname) { 286 import std.algorithm : canFind; 287 288 auto pname = _packname; 289 290 auto ppath = _packname.urlDecode().split(":"); 291 auto packageInfo = m_registry.getPackageInfo(ppath[0]); 292 293 auto packageName = pname; 294 auto registry = m_registry; 295 render!("view_package.versions.dt", 296 packageName, 297 packageInfo, 298 registry); 299 } 300 301 private bool getPackageInfo(string pack_name, string pack_version, out PackageInfo pkg_info, out PackageVersionInfo ver_info) { 302 import std.algorithm : map; 303 auto ppath = pack_name.urlDecode().split(":"); 304 305 pkg_info = m_registry.getPackageInfo(ppath[0]); 306 if (pkg_info.info.type == Json.Type.null_) return false; 307 308 if (pack_version.length) { 309 foreach (ref v; pkg_info.versions) { 310 if (v.version_ == pack_version) { 311 ver_info = v; 312 break; 313 } 314 } 315 if (ver_info.info.type != Json.Type.Object) return false; 316 } else { 317 import dubregistry.viewutils; 318 if (pkg_info.versions.length == 0) return false; 319 auto vidx = getBestVersionIndex(pkg_info.versions.map!(v => v.version_)); 320 ver_info = pkg_info.versions[vidx]; 321 } 322 323 foreach (i; 1 .. ppath.length) { 324 if ("subPackages" !in ver_info.info) return false; 325 bool found = false; 326 foreach (sp; ver_info.info["subPackages"]) { 327 if (sp["name"] == ppath[i]) { 328 Json newv = Json.emptyObject; 329 // inherit certain fields 330 foreach (field; ["version", "date", "license", "authors", "homepage"]) 331 if (auto pv = field in ver_info.info) newv[field] = *pv; 332 // copy/overwrite the rest frmo the sub package 333 foreach (string name, value; sp) newv[name] = value; 334 ver_info.info = newv; 335 found = true; 336 break; 337 } 338 } 339 if (!found) return false; 340 } 341 return true; 342 } 343 344 private void updateCategories() 345 { 346 auto catfile = openFile("categories.json"); 347 scope(exit) catfile.close(); 348 auto json = parseJsonString(catfile.readAllUTF8()); 349 350 Category[string] catmap; 351 352 Category processNode(Json node, string[] path) 353 { 354 path ~= node["name"].get!string; 355 auto cat = new Category; 356 cat.name = path.join("."); 357 cat.description = node["description"].get!string; 358 if (path.length > 2) 359 cat.indentedDescription = "\u00a0\u00a0\u00a0\u00a0".replicate(path.length-2) ~ "\u00a0└ " ~ cat.description; 360 else if (path.length == 2) 361 cat.indentedDescription = "\u00a0└ " ~ cat.description; 362 else cat.indentedDescription = cat.description; 363 Json icons = json["icons"]; 364 foreach_reverse (i; 0 .. path.length) { 365 string dotPath = path[0 .. i+1].join("."); 366 if (dotPath in icons) { 367 cat.imageName = icons[dotPath].get!string; 368 cat.imageDescription = path[0 .. i+1].join("/"); 369 break; 370 } 371 } 372 373 catmap[cat.name] = cat; 374 375 if ("categories" in node) 376 foreach (subcat; node["categories"]) 377 cat.subCategories ~= processNode(subcat, path); 378 379 return cat; 380 } 381 382 Category[] cats; 383 foreach (top_level_cat; json["list"]) 384 cats ~= processNode(top_level_cat, null); 385 386 m_categories = cats; 387 m_categoryMap = catmap; 388 } 389 390 private void updatePackageList() 391 { 392 import std.algorithm.iteration : map; 393 import core.memory : GC; 394 () @trusted { GC.collect(); } (); 395 396 397 // NOTE: all string/array data is reallocated to avoid pinning the 398 // underlying buffer that holds the MongoDB reply 399 PackedStringAllocator strings; 400 auto newpacks = appender!(CachedPackageSlot[]); 401 newpacks.reserve(m_packages.length); 402 foreach (p; m_registry.db.getPackageDump()) { 403 CachedPackageSlot cp; 404 cp.id = p._id; 405 cp.name = strings.alloc(p.name); 406 cp.stats = p.stats; 407 cp.categories = p.categories.map!(c => strings.alloc(c)).array; 408 cp.versions = p.versions 409 .map!(v => CachedPackageSlot.Version(strings.alloc(v.version_), v.date)) 410 .array; 411 newpacks ~= cp; 412 } 413 m_packages = newpacks.data; 414 } 415 } 416 417 class DubRegistryFullWebFrontend : DubRegistryWebFrontend { 418 private { 419 UserManWebAuthenticator m_usermanauth; 420 } 421 422 this(DubRegistry registry, UserManController userman) 423 { 424 super(registry, userman); 425 m_usermanauth = new UserManWebAuthenticator(userman); 426 } 427 428 void querySearch(string q = "") 429 { 430 auto results = m_registry.searchPackages(q); 431 auto queryString = q; 432 render!("search_results.dt", queryString, results); 433 } 434 435 void getGettingStarted() { render!("getting_started.dt"); } 436 void getAbout() { redirect("/getting_started"); } 437 void getUsage() { redirect("/getting_started"); } 438 void getAdvancedUsage() { render!("advanced_usage.dt"); } 439 440 void getPublish() { render!("publish.dt"); } 441 void getDevelop() { render!("develop.dt"); } 442 443 @path("/package-format") 444 void getPackageFormat(string lang = null) 445 { 446 switch (lang) { 447 default: redirect("package-format?lang=json"); break; 448 case "json": render!("package_format_json.dt"); break; 449 case "sdl": render!("package_format_sdl.dt"); break; 450 } 451 } 452 453 private auto downloadInfo() 454 { 455 static struct DownloadFile { 456 string fileName; 457 string platformCaption; 458 string typeCaption; 459 } 460 461 static struct DownloadVersion { 462 string id; 463 DownloadFile[][string] files; 464 } 465 466 static struct Info { 467 DownloadVersion[] versions; 468 string latest = ""; 469 470 void addFile(string ver, string platform, string filename) 471 { 472 473 auto df = DownloadFile(filename); 474 switch (platform) { 475 default: 476 auto pts = platform.split("-"); 477 df.platformCaption = format("%s%s (%s)", pts[0][0 .. 1].toUpper(), pts[0][1 .. $], pts[1].replace("_", "-").toUpper()); 478 break; 479 case "osx-x86": df.platformCaption = "OS X (X86)"; break; 480 case "osx-x86_64": df.platformCaption = "OS X (X86-64)"; break; 481 } 482 483 if (filename.endsWith(".tar.gz")) df.typeCaption = "binary tarball"; 484 else if (filename.endsWith(".zip")) df.typeCaption = "zipped binaries"; 485 else if (filename.endsWith(".rpm")) df.typeCaption = "binary RPM package"; 486 else if (filename.endsWith("setup.exe")) df.typeCaption = "installer"; 487 else df.typeCaption = "Unknown"; 488 489 foreach(ref v; versions) 490 if( v.id == ver ){ 491 v.files[platform] ~= df; 492 return; 493 } 494 DownloadVersion dv = DownloadVersion(ver); 495 dv.files[platform] = [df]; 496 versions ~= dv; 497 if (!isPreReleaseVersion(ver) && (latest.empty || compareVersions(ver, latest) > 0)) 498 latest = ver; 499 } 500 } 501 502 Info info; 503 504 if (!"public/files".exists || !"public/files".isDir) 505 return info; 506 507 import std.regex; 508 Regex!char[][string] platformPatterns; 509 platformPatterns["windows-x86"] = [ 510 regex("^dub-(?P<version>[^-]+)(?:-(?P<prerelease>.*))?(?:-setup\\.exe|-windows-x86\\.zip)$") 511 ]; 512 platformPatterns["linux-x86_64"] = [ 513 regex("^dub-(?P<version>[^-]+)(?:-(?P<prerelease>.+))?-linux-x86_64\\.tar\\.gz$"), 514 regex("^dub-(?P<version>[^-]+)-(?:0\\.(?P<prerelease>.+)|[^0].*)\\.x86_64\\.rpm$") 515 ]; 516 platformPatterns["linux-x86"] = [ 517 regex("^dub-(?P<version>[^-]+)(?:-(?P<prerelease>.+))?-linux-x86\\.tar\\.gz$"), 518 regex("^dub-(?P<version>[^-]+)-(?:0\\.(?P<prerelease>.+)|[^0].*)\\.x86\\.rpm$") 519 ]; 520 platformPatterns["linux-arm"] = [ 521 regex("^dub-(?P<version>[^-]+)(?:-(?P<prerelease>.+))?-linux-arm\\.tar\\.gz$"), 522 regex("^dub-(?P<version>[^-]+)-(?:0\\.(?P<prerelease>.+)|[^0].*)\\.arm\\.rpm$") 523 ]; 524 platformPatterns["osx-x86_64"] = [ 525 regex("^dub-(?P<version>[^-]+)(?:-(?P<prerelease>.+))?-osx-x86_64\\.tar\\.gz$"), 526 ]; 527 528 foreach(de; dirEntries("public/files", "*.*", SpanMode.shallow)) { 529 auto name = NativePath(de.name).head.name; 530 531 foreach (platform, rexes; platformPatterns) { 532 foreach (rex; rexes) { 533 auto match = match(name, rex).captures;//matchFirst(name, rex); 534 if (match.empty) continue; 535 auto ver = match["version"] ~ (match["prerelease"].length ? "-" ~ match["prerelease"] : ""); 536 if (!ver.isValidVersion()) continue; 537 info.addFile(ver, platform, name); 538 } 539 } 540 } 541 542 info.versions.sort!((a, b) => vcmp(a.id, b.id))(); 543 return info; 544 } 545 546 void getDownload() 547 { 548 auto info = downloadInfo(); 549 render!("download.dt", info); 550 } 551 552 @path("/download/LATEST") 553 void getLatest(HTTPServerResponse res) 554 { 555 auto info = downloadInfo(); 556 enforceHTTP(!info.latest.empty, HTTPStatus.notFound, "No version available."); 557 res.writeBody(info.latest); 558 } 559 560 561 @auth 562 void getMyPackages(User _user) 563 { 564 auto user = _user; 565 auto registry = m_registry; 566 render!("my_packages.dt", user, registry); 567 } 568 569 @auth @path("/register_package") 570 void getRegisterPackage(User _user, string url = null, string _error = null) 571 { 572 auto user = _user; 573 string error = _error; 574 auto registry = m_registry; 575 render!("my_packages.register.dt", user, url, error, registry); 576 } 577 578 @auth @path("/register_package") @errorDisplay!getRegisterPackage 579 void postRegisterPackage(string url, User _user, bool ignore_fork = false) 580 { 581 DbRepository rep; 582 if (!url.canFind("://")) 583 url = "https://" ~ url; 584 rep.parseURL(URL.fromString(url)); 585 586 string kind = rep.kind; 587 string owner = rep.owner; 588 string project = rep.project; 589 590 if (!ignore_fork) { 591 auto info = m_registry.getRepositoryInfo(rep); 592 if (info.isFork) { 593 render!("my_packages.register.warn_fork.dt", kind, owner, project); 594 return; 595 } 596 } 597 598 m_registry.addPackage(rep, _user.id); 599 redirect("/my_packages"); 600 } 601 602 @auth @path("/my_packages/:packname") 603 void getMyPackagesPackage(string _packname, User _user, string _error = null) 604 { 605 enforceUserPackage(_user, _packname); 606 auto packageName = _packname; 607 auto nfo = m_registry.getPackageInfo(packageName); 608 if (nfo.info.type == Json.Type.null_) return; 609 auto categories = m_categories; 610 auto registry = m_registry; 611 auto user = _user; 612 auto error = _error; 613 render!("my_packages.package.dt", packageName, categories, user, registry, error); 614 } 615 616 @auth @path("/my_packages/:packname/update") 617 void postUpdatePackage(string _packname, User _user) 618 { 619 enforceUserPackage(_user, _packname); 620 m_registry.triggerPackageUpdate(_packname); 621 redirect("/my_packages/"~_packname); 622 } 623 624 @auth @path("/my_packages/:packname/remove") 625 void postShowRemovePackage(string _packname, User _user) 626 { 627 auto packageName = _packname; 628 auto user = _user; 629 enforceUserPackage(user, packageName); 630 render!("my_packages.remove.dt", packageName, user); 631 } 632 633 @auth @path("/my_packages/:packname/remove_confirm") 634 void postRemovePackage(string _packname, User _user) 635 { 636 enforceUserPackage(_user, _packname); 637 m_registry.removePackage(_packname, _user.id); 638 redirect("/my_packages"); 639 } 640 641 @auth @path("/my_packages/:packname/set_categories") 642 void postSetPackageCategories(string[] categories, string _packname, User _user) 643 { 644 enforceUserPackage(_user, _packname); 645 string[] uniquecategories; 646 outer: foreach (cat; categories) { 647 if (!cat.length) continue; 648 foreach (j, ec; uniquecategories) { 649 if (cat.startsWith(ec)) continue outer; 650 if (ec.startsWith(cat)) { 651 uniquecategories[j] = cat; 652 continue outer; 653 } 654 } 655 uniquecategories ~= cat; 656 } 657 m_registry.setPackageCategories(_packname, uniquecategories); 658 659 redirect("/my_packages/"~_packname); 660 } 661 662 @auth @path("/my_packages/:packname/set_repository") @errorDisplay!getMyPackagesPackage 663 void postSetPackageRepository(string kind, string owner, string project, string _packname, User _user) 664 { 665 enforceUserPackage(_user, _packname); 666 667 DbRepository rep; 668 rep.kind = kind; 669 rep.owner = owner; 670 rep.project = project; 671 m_registry.setPackageRepository(_packname, rep); 672 673 redirect("/my_packages/"~_packname); 674 } 675 676 @auth @path("/my_packages/:packname/set_logo") @errorDisplay!getMyPackagesPackage 677 void postSetLogo(scope HTTPServerRequest request, string _packname, User _user) 678 { 679 enforceUserPackage(_user, _packname); 680 const FilePart logo = request.files.get("logo"); 681 enforceBadRequest(logo != FilePart.init); 682 auto info = getFileInfo(logo.tempPath); 683 enforceBadRequest(info.size < 1024 * 1024, "Logo too big, at most 1 MB"); 684 auto renamed = NativePath(logo.tempPath.toString ~ logo.filename.name.extension); 685 moveFile(logo.tempPath, renamed, true); 686 scope (exit) 687 removeFile(renamed); 688 m_registry.setPackageLogo(_packname, renamed); 689 690 redirect("/my_packages/"~_packname); 691 } 692 693 @auth @path("/my_packages/:packname/delete_logo") 694 void postDeleteLogo(scope HTTPServerRequest request, string _packname, User _user) 695 { 696 enforceUserPackage(_user, _packname); 697 m_registry.unsetPackageLogo(_packname); 698 699 redirect("/my_packages/"~_packname); 700 } 701 702 @path("/docs/commandline") 703 void getCommandLineDocs() 704 { 705 import dub.commandline; 706 auto commands = getCommands(); 707 render!("docs.commandline.dt", commands); 708 } 709 710 private void enforceUserPackage(User user, string package_name) 711 { 712 enforceHTTP(m_registry.isUserPackage(user.id, package_name), HTTPStatus.forbidden, "You don't have access rights for this package."); 713 } 714 715 // Attribute for authenticated routes 716 private enum auth = before!performAuth("_user"); 717 mixin PrivateAccessProxy; 718 719 private User performAuth(HTTPServerRequest req, HTTPServerResponse res) 720 { 721 return m_usermanauth.performAuth(req, res); 722 } 723 } 724 725 final class Category { 726 string name, description, indentedDescription, imageName, imageDescription; 727 Category[] subCategories; 728 }