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