Skip to content
Permalink
Browse files

Migrate to newest wiki format

The new format no longer uses the page path and replaces it with an id instead.
  • Loading branch information...
kzu committed Oct 30, 2019
1 parent a43383b commit c1569c9a4520cab63e38cc9857fc839a23d08d8f
Showing with 101 additions and 92 deletions.
  1. +67 −27 functions/Wiki.linq
  2. +32 −63 linkinator/background.js
  3. +2 −2 linkinator/manifest.json
@@ -1,24 +1,26 @@
<Query Kind="Program">
<Reference>&lt;RuntimeDirectory&gt;\System.Web.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\Microsoft.Build.Framework.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\Microsoft.Build.Tasks.v4.0.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\Microsoft.Build.Utilities.v4.0.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Configuration.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Design.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.DirectoryServices.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.DirectoryServices.Protocols.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.EnterpriseServices.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Web.RegularExpressions.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Design.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Runtime.Caching.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Web.ApplicationServices.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.DirectoryServices.Protocols.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Web.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Web.RegularExpressions.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Web.Services.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\Microsoft.Build.Utilities.v4.0.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Runtime.Caching.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\Microsoft.Build.Framework.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\Microsoft.Build.Tasks.v4.0.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.Windows.Forms.dll</Reference>
<NuGetReference>Microsoft.ApplicationInsights</NuGetReference>
<NuGetReference>Microsoft.AspNetCore.Mvc</NuGetReference>
<NuGetReference>Microsoft.Extensions.Logging.Console</NuGetReference>
<NuGetReference>Microsoft.Extensions.Logging.Debug</NuGetReference>
<NuGetReference>xunit.assert</NuGetReference>
<Namespace>Microsoft.ApplicationInsights</Namespace>
<Namespace>Microsoft.ApplicationInsights.Channel</Namespace>
<Namespace>Microsoft.ApplicationInsights.Extensibility</Namespace>
<Namespace>Microsoft.AspNetCore.Http</Namespace>
<Namespace>Microsoft.AspNetCore.Http.Internal</Namespace>
<Namespace>Microsoft.AspNetCore.Mvc</Namespace>
@@ -31,8 +33,21 @@
<Namespace>System.Net</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
<Namespace>Xunit</Namespace>
<Namespace>Microsoft.ApplicationInsights.Extensibility</Namespace>
<Namespace>Microsoft.ApplicationInsights.Channel</Namespace>
<AppConfig>
<Content>
<configuration>
<runtime>
<loadFromRemoteSources enabled="true" />
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices.RuntimeInformation" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="4.0.0.0-4.0.2.0" newVersion="4.0.2.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
</Content>
</AppConfig>
<DisableMyExtensions>true</DisableMyExtensions>
</Query>

@@ -53,6 +68,10 @@ void Main()
// If project missing, go to docs
Assert.IsType<RedirectResult>(Run("http://wiki.azdo.io/", "foo"));
Assert.Equal("https://github.com/kzu/azdo#wiki", Run<RedirectResult>("http://wiki.azdo.io/", "foo").Url);

Assert.Equal(
"https://dev.azure.com/foo/oss/_wiki/wikis/oss.wiki/1/Hello-World",
Run<RedirectResult>("https://wiki.azdo.io/foo/oss/1/Hello-World", "foo", "oss").Url);
}

static TActionResult Run<TActionResult>(string url, string org = null, string project = null)
@@ -93,29 +112,50 @@ public static IActionResult Run(HttpRequest req, ILogger log, string org = null,
return new RedirectResult("https://github.com/kzu/azdo#wiki");
}

var path = req.Path.Value.Replace($"/{org}/{project}", "/");
if (!req.QueryString.HasValue)
{
// Default mode is to replace dashes with spaces
path = path.Replace('-', ' ');
}
else if (req.QueryString.Value == "?u")
var path = req.Path.Value.Replace($"/{org}/{project}/", "/");

// Detect new URL format for new Wiki ([project].wiki/[id]/[page])
var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (double.TryParse(parts[0], out var pageId))
{
path = path.Replace('_', ' ');
}

// Finally, URL-encode the path before making up the final URL
var escaped = Uri.EscapeDataString(path).Replace("-", "%252D");
var location = $"https://dev.azure.com/{org}/{project}/_wiki/wikis/{project}.wiki?pagePath={escaped}";
var location = $"https://dev.azure.com/{org}/{project}/_wiki/wikis/{project}.wiki{path}";

new TelemetryClient(TelemetryConfiguration.Active).TrackEvent(
"redirect", new Dictionary<string, string>
{
{ "url", req.Host + req.Path },
{ "redirect", location },
{ "org", org },
{ "project", project },
});

new TelemetryClient(TelemetryConfiguration.Active).TrackEvent(
"redirect", new Dictionary<string, string>
return new RedirectResult(location);
}
else
{
if (!req.QueryString.HasValue)
{
// Default mode is to replace dashes with spaces
path = path.Replace('-', ' ');
}
else if (req.QueryString.Value == "?u")
{
path = path.Replace('_', ' ');
}

// Finally, URL-encode the path before making up the final URL
var escaped = Uri.EscapeDataString(path).Replace("-", "%252D");
var location = $"https://dev.azure.com/{org}/{project}/_wiki/wikis/{project}.wiki?pagePath={escaped}";

new TelemetryClient(TelemetryConfiguration.Active).TrackEvent(
"redirect", new Dictionary<string, string>
{
{ "url", req.Host + req.Path + req.QueryString },
{ "redirect", location },
{ "org", org },
{ "project", project },
});
});

return new RedirectResult(location);
return new RedirectResult(location);
}
}
@@ -12,10 +12,10 @@ function copy(text) {
function onClicked(tab) {
var parser = document.createElement('a');
parser.href = tab.url.toString();
var shortUrl = parser.pathname.substring(1) + parser.search;
var relativeUrl = parser.pathname.substring(1) + parser.search;

console.info('Processing ' + shortUrl);
var newUrl = shortenUrl(parser.hostname, shortUrl);
console.info('Processing ' + relativeUrl);
var newUrl = shortenUrl(parser.hostname, relativeUrl);
// If no replacement was made, copy again the original url.
if (newUrl.skipped)
newUrl = tab.url;
@@ -47,8 +47,8 @@ function findSelectedUrl(querySelector) {
});
}

function shortenUrl(hostname, shortUrl) {
var segments = shortUrl.split('/');
function shortenUrl(hostname, relativeUrl) {
var segments = relativeUrl.split('/');
var org = segments[0];
var project = segments[1];
if (hostname == 'devdiv.visualstudio.com')
@@ -61,64 +61,33 @@ function shortenUrl(hostname, shortUrl) {
}

// ================== Wiki ======================
if (shortUrl.includes('/_wiki/wikis/')) {
var indexOfPagePath = shortUrl.indexOf('pagePath=') + 9;
var indexOfEndPath = shortUrl.indexOf('&', indexOfPagePath);
if (relativeUrl.includes('/_wiki/wikis/')) {
// Special case DevDiv: we make it even shorter, and switch domains to
// place /DevDiv/DevDiv back server-side
var domain = shortUrl.includes('DevDiv/_wiki/wikis/') ? 'http://wiki.devdiv.io/' : 'http://wiki.azdo.io/';
var pagePath = indexOfEndPath == -1 ? shortUrl.substring(indexOfPagePath) : shortUrl.substring(indexOfPagePath, indexOfEndPath);
// The first decode gives us a slash-separated path, which we need to decode individually to decode double encoded chars (i.e. & and -)
pagePath = decodeURIComponent(pagePath).split('/').map(x => decodeURIComponent(x)).join('/').replace(/^\//, "");
// page path query string processing hint for azure function:
// [none] = replace dashes with spaces. Most common case, made short and nice
// ?u = replace underscores with spaces
// ?b = bare, do not replace anything (either there were no spaces, which is uncommon in wikis,
// or there were, but there were also both `-` and `_` so no replacement could be provided)
if (pagePath.indexOf(' ') != -1) {
// Improve URI when it has spaces
if (pagePath.indexOf('-') == -1) {
pagePath = pagePath.replace(/ /g, '-');
} else if (pagePath.indexOf('_') == -1) {
pagePath = pagePath.replace(/ /g, '_') + "?u";
} else {
// Can't replace spaces, bare treatment to preserve path intact.
pagePath = pagePath.split('/').map(x => encodeURIComponent(x)).join('/') + "?b";
}
} else {
// No spaces, force bare treatment to preserve path intact.
pagePath = pagePath.split('/').map(x => encodeURIComponent(x)).join('/') + "?b";
var domain = relativeUrl.includes('DevDiv/_wiki/wikis/') ? 'http://wiki.devdiv.io/' : 'http://wiki.azdo.io/';
var match = /_wiki\/wikis\/.*\.wiki\/(\d+)\/(.*)/.exec(relativeUrl);
if (match) {
// New short format does not encode the page path but rather uses the page id + its name
if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv")
return domain + match[1] + '/' + match[2];
else
return domain + org + '/' + project + '/' + match[1] + '/' + match[2];
}

// This would be the safest way, but it would also URL-encode characters that are
// usable in a copy-pasted URL, such as & and - in the URL (browser know how to encode/decode)
// pagePath = pagePath.split('/').map(x => encodeURIComponent(x)).join('/');

if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv")
return domain + pagePath;
else
return domain + org + '/' + project + '/' + pagePath;
}

// We could make wiki pages way shorter by just using the id instead...
// if (shortUrl.includes('/_wiki/wikis/DevDiv.wiki/') && shortUrl.includes('pageId=')) {
// var pageId = /pageId=(\d+)/.exec(shortUrl);
// return 'http://wiki.devdiv.io/' + pageId;
// }

// ================== WorkItems ======================
if (shortUrl.includes('/_workitems/edit/'))
return 'http://work.azdo.io/' + shortUrl.substring(shortUrl.indexOf('/_workitems/edit/') + 17);
if (relativeUrl.includes('/_workitems/edit/'))
return 'http://work.azdo.io/' + relativeUrl.substring(relativeUrl.indexOf('/_workitems/edit/') + 17);

if (shortUrl.includes('workitem=')) {
var id = /workitem=(\d+)/.exec(shortUrl);
if (relativeUrl.includes('workitem=')) {
var id = /workitem=(\d+)/.exec(relativeUrl);
return 'http://work.azdo.io/' + id[1];
}

// ================== Build ======================
if (shortUrl.includes('/_build')) {
var buildId = /buildId=(\d+)/.exec(shortUrl);
var definitionId = /definitionId=(\d+)/.exec(shortUrl);
if (relativeUrl.includes('/_build')) {
var buildId = /buildId=(\d+)/.exec(relativeUrl);
var definitionId = /definitionId=(\d+)/.exec(relativeUrl);

var id = buildId ? parseInt(buildId[1]) : parseInt(definitionId[1]);
var suffix = '';
@@ -136,8 +105,8 @@ function shortenUrl(hostname, shortUrl) {
return 'https://build.azdo.io/' + org + '/' + project + '/' + id + suffix;
}

if (shortUrl.includes('edit-build-definition&id=')) {
var buildId = /id=(\d+)/.exec(shortUrl);
if (relativeUrl.includes('edit-build-definition&id=')) {
var buildId = /id=(\d+)/.exec(relativeUrl);
var id = parseInt(buildId[1]);

if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv") {
@@ -150,10 +119,10 @@ function shortenUrl(hostname, shortUrl) {
}

// ================== Release ======================
if (shortUrl.includes('/_releaseDefinition?definitionId=') ||
(shortUrl.includes('/_release') && shortUrl.includes('definitionId='))) {
if (relativeUrl.includes('/_releaseDefinition?definitionId=') ||
(relativeUrl.includes('/_release') && relativeUrl.includes('definitionId='))) {
// New release pipeline
var definitionId = /definitionId=(\d+)/.exec(shortUrl);
var definitionId = /definitionId=(\d+)/.exec(relativeUrl);
var id = parseInt(definitionId[1]);

if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv") {
@@ -165,9 +134,9 @@ function shortenUrl(hostname, shortUrl) {
}
}

if ((shortUrl.includes('/_releaseProgress?') || shortUrl.includes('release-pipeline-progress')) && shortUrl.includes('releaseId=')) {
if ((relativeUrl.includes('/_releaseProgress?') || relativeUrl.includes('release-pipeline-progress')) && relativeUrl.includes('releaseId=')) {
// New release pipeline
var releaseId = /releaseId=(\d+)/.exec(shortUrl);
var releaseId = /releaseId=(\d+)/.exec(relativeUrl);
var id = parseInt(releaseId[1]);

if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv") {
@@ -180,17 +149,17 @@ function shortenUrl(hostname, shortUrl) {
}

// ================== PullRequest ======================
if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv" && shortUrl.includes('/pullrequest/')) {
var match = /_git\/(.+)\/pullrequest\/(\d+)/.exec(shortUrl);
if (org.toLowerCase() == "devdiv" && project.toLowerCase() == "devdiv" && relativeUrl.includes('/pullrequest/')) {
var match = /_git\/(.+)\/pullrequest\/(\d+)/.exec(relativeUrl);
if (match[1] == 'VS')
// Make the default project VS, to make it even shorter
return 'http://pr.devdiv.io/' + match[2];
else
return 'http://pr.devdiv.io/' + match[1] + '/' + match[2];
}

if (shortUrl.includes('content/problem/')) {
var problemId = /problem\/(\d+)\//.exec(shortUrl);
if (relativeUrl.includes('content/problem/')) {
var problemId = /problem\/(\d+)\//.exec(relativeUrl);
return 'http://feedback.devdiv.io/' + problemId[1];
}

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "AzDO Linkinator",
"author": "Daniel Cazzulino",
"version": "0.5.16",
"version": "0.5.17",
"background": {
"scripts": ["background.js"],
"persistent": false
@@ -45,4 +45,4 @@
"webNavigation",
"https://dev.azure.com/*"
]
}
}

0 comments on commit c1569c9

Please sign in to comment.
You can’t perform that action at this time.