We've got this rather large'ish and old'ish asp web application, that started to behave poorly because of (as i understand) XHR requests in beforeunload event handler. There is quite a logic tied to this, so i cannot just throw it away.
The code in beforeunload was like this:
$(window).on('beforeunload', function () {
$.ajax('/site/$hdr/unload', { async: true, cache: false });
});
and in server side something like this:
private class HttpModule : IHttpModule
{
private static void PostAcquireRequestState(object sender, EventArgs e)
{
var context = HttpContext.Current;
var path = context.Request.AppRelativeCurrentExecutionFilePath;
if (path.StartsWith("~/$hdr/unload"))
{
//do things
}
}
I'm trying to use the fetch api instead of synchronous ajax request used before. In chrome everything works fine, but FireFox 89.0 behaves strangely.
Code in beforeunload is like this:
let i;
let data = new Headers();
if (window.FMS && window.FMS.HttpHeaders) {
for (i in window.FMS.HttpHeaders) {
if (FMS.HttpHeaders.hasOwnProperty(i)) {
data.append('X-FMS-' + i, FMS.HttpHeaders[i]);
}
}
}
fetch(FMS.UnloadUrl + '?_=' + jQuery.now(), {
method: 'GET',
keepalive: true,
cache: 'no-cache',
headers: data }
).then(response => response.ok)
.then(IsOk => console.debug(IsOk.toString()))
.catch((error) => {console.error('Error:', error)});
If my understanding is correct - the server side now have to receive OPTIONS preflight request first? I never receive this. Sometimes i receive the GET request, but usually in the FireFox console i can see TypeError: NetworkError when attempting to fetch resource error or notification that the XHR request has been blocked by DevTools.
Related
I have this issue:
I am using .NET 6 with MVC and I am making use of FullcalendarIo. I have a controller for making free slots in the calendar. This is the code in the controller:
[Authorize(Roles = DoctorRoleName)]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task < ActionResult < AppointmentSlotInputModel >> GenerateSlots(AppointmentSlotInputModel model) {
//await this.appointmentService.GenerateSlots(model.Start, model.End, model.SlotDurationMinutes);
return Json("Hello");
}
And this is my JS Code that makes the POST request
const params = {
start: startDate,
end: endDate,
slotDurationMinutes: scale
};
const response = await fetch('/Appointment/GenerateSlots', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.getElementById("RequestVerificationToken").value
},
body: JSON.stringify(params)
});
In the network tab the request to /Appointment/GenerateSlots I get first code 302 - Redirect and then 404 Not Found. The request url seems correct - https://localhost:44376/Appointment/GenerateSlots.
f I change the method to GET and put the [HttpGet] attribute above the action in the controller I get the JSON result. In the Startup.cs I am using these:
services.AddAntiforgery(options => {
options.HeaderName = "X-CSRF-TOKEN";
});
services.Configure < CookiePolicyOptions > (options => {
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
But event if I comment them out the result is the same - first 302 then 404. I have tried a lot of the code that is written here on the site for people with similar issues, but it hasn't helped. Where am I mistaking?
I am using standard routing:
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Edit: I have found a similar thread here - ASP.NET controller, AJAX GET works, but POST does not (404)
But this doesn't seem to be the case for me.
So I found a my solution. In my Startup.cs I have this code:
services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
});
And the BadRequest from the Controller came because of the AntiForgeryToken validation. In my AJAX call my header for the AF Token was wrong: It was 'RequestVerificationToken', but it should have been 'X-CSRF-TOKEN' or the other way around. But still now I have the proper result and no more BadRequest.
I have a problem with events, when open pdf in external window. They are not triggered even with 'viewerId' attribute. Here is my code:
HTML
<a *ngIf="document.s3_link" class="document-title" (click)="openDocument(document)">{{ document.description }}</a
>
<ng2-pdfjs-viewer
#externalPdfViewer
viewerId="MyUniqueID"
[externalWindow]="true"
(onDocumentLoad)="highlightSearchTerm()"
></ng2-pdfjs-viewer>
TypeScript
openDocument(document): void {
this.getDocumentBlob(document.s3_link).subscribe(res => {
this.externalPdfViewer.pdfSrc = res
this.externalPdfViewer.downloadFileName = document.description
this.externalPdfViewer.refresh()
})
}
getDocumentBlob(link): Observable<any> {
let headers = new HttpHeaders()
headers = headers.set("Accept", "application/pdf")
return this.http.get(link, { headers: headers, responseType: "blob" })
}
highlightSearchTerm() {
this.externalPdfViewer.PDFViewerApplication.findController.executeCommand(
"find",
{
caseSensitive: false,
findPrevious: undefined,
highlightAll: true,
phraseSearch: true,
query: this.initQuery,
}
)
}
Found this in documentation in one of the issues.
When you are opening PDF in a new window, events cannot be emitted back to former window.
Please see this SO: Communication between tabs or windows
Documentation needs to be updated to reflect this. Using above techniques, it may be achieved, but that would require an improvement/implementation.
So I've got a custom .aspx page with pure JS/jQuery (so no Angular) that I upload to Sharepoint Online and can add as a webpart or iFrame to a site in Sharepoint Online. I want to display Outlook calendar events and I also use FullCalendar.io for the displaying part.
This also includes ADAL (Azure AD) security because it's needed for Outlook API.
So first I authenticate with Azure AD, get my acquired token and then pass that token to the function that builds my FullCalendar.io calendar on the page. This will try to get the Outlook Calendar events and display them nicely on the FullCalendar.
Here's the examples I followed:
https://codeatwork.wordpress.com/2017/04/16/using-outlook-rest-apis-in-sharepoint-online/
https://github.com/AzureAD/azure-activedirectory-library-for-js
https://www.paitgroup.com/blog/display-events-from-an-outlook-calendar-in-sharepoint-using-office-365-apis
I've also added the permissions from link 1 to my Azure AD registered App in the Azure portal!
Here's my code:
<script>
var $this = this;
$(document).ready(function() {
window.config = {
tenantId: '{tenant}',
clientId: '{clientId}',
popUp: true,
redirectUri: '{redirectURI}',
endpoints: {
"https://outlook.office.com/api/v2.0/me/events":"https://outlook.office.com/",
}
};
var authenticationContext = new AuthenticationContext(config);
authenticationContext.handleWindowCallback();
if (authenticationContext.getCachedUser()) {
authenticationContext.acquireToken(config.clientId, function (errorDesc, token, error) {
if (error) { //acquire token failure
if (config.popUp) {
// If using popup flows
authenticationContext.acquireTokenPopup(config.clientId, null, null, function (errorDesc, token, error) {});
}
else {
// In this case the callback passed in the Authentication request constructor will be called.
authenticationContext.acquireTokenRedirect(config.clientId, null, null);
}
}
else {
//acquired token successfully
$this.DisplayEvents(token);
}
});
}
else {
// Initiate login
authenticationContext.login();
}
});
function DisplayEvents(adalToken) {
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay,listWeek'
},
navLinks: true, // can click day/week names to navigate views
editable: true,
eventLimit: true, // allow "more" link when too many events
events: function(start, end, timezone, callback) {
var headers = new Headers();
var bearerToken = "Bearer " + adalToken;
headers.append('Authorization', bearerToken);
var options = {
method: 'GET',
headers: headers,
mode: 'no-cors'
};
var outlookEndpoint = 'https://outlook.office.com/api/v2.0/me/events?$select=Subject,Organizer,Start,End';
fetch(outlookEndpoint, options)
.then(function (response) {
console.log(response);
});
}
});
}
</script>
As you can see, the token is passed to the "fetch" call in the events function of the fullCalendar() function. I've tried doing a normal ajax 'GET' call, but I get CORS errors. Same when I leave out "mode: 'no-cors'" from my options. Fetch is being done because of the github example I've linked above!
Now I'm receiving a 401 Unauthorized error and response that's filled with status:0 and body: null etc.
Anyone else has had this issue or can see what I'm doing wrong to make the authentication fail?
You need to modify your acquireToken method as below. Just replace "config.clientId" with "https://outlook.office.com/".
if (authContext.getCachedUser()) {
authContext.acquireToken("https://outlook.office.com/", function (error, token) {
if (error) { //acquire token failure
if (config.popUp) {
// If using popup flows
authContext.acquireTokenPopup("https://outlook.office.com/", null, null, function (errorDesc, token, error) { });
}
else {
// In this case the callback passed in the Authentication request constructor will be called.
authContext.acquireTokenRedirect("https://outlook.office.com/", null, null);
}
}
else {
//acquired token successfully
// alert('token success');
DisplayEvents(token);
}
});
}
else {
// Initiate login
authContext.login();
}
Besides, you can find the 401 error details by using Rest Client or postman. For example, this is the response in Rest Client.
I'm trying the Wikipedia client login flow depicted in the API:Login docs, but something wrong happens:
1) I correctly get a token raised with the HTTP GET https://en.wikipedia.org/w/api.php?action=query&meta=tokens&type=login&format=json
and I get a valid logintoken string.
2.1) I then try the clientlogin like:
HTTP POST /w/api.php?action=clientlogin&format=json&lgname=xxxx&lgtoken=xxxx%2B%5C
and the POST BODY was
{
"lgpassword" : "xxxxx",
"lgtoken" : "xxxxx"
}
But I get an error:
{
"error": {
"code": "notoken",
"info": "The \"token\" parameter must be set."
},
"servedby": "mw1228"
}
If I try to change lgtoken to token I get the same result.
2.2) I have then tried the old method i.e. action=login and passing the body, but it does not work, since it gives me back another login token: HTTP POST https://en.wikipedia.org/w/api.php?action=login&format=json&lgname=xxxx
and the same POST BODY
I then get
{
"warnings": {}
},
"login": {
"result": "NeedToken",
"token": "xxxxx+\\"
}
where the docs here states that
NeedToken if the lgtoken parameter was not provided or no session was active (e.g. your cookie handling is broken).
but I have passed the lgtoken in the json body as showed.
I'm using Node.js and the built-in http module, that is supposed to pass and keep session Cookies in the right way (with other api it works ok).
I have found a similar issue on a the LrMediaWiki client here.
[UPDATE]
This is my current implementation:
Wikipedia.prototype.loginUser = function (username, password) {
var self = this;
return new Promise((resolve, reject) => {
var cookies = self.cookies({});
var headers = {
'Cookie': cookies.join(';'),
'Accept': '*/*',
'User-Agent': self.browser.userAgent()
};
// fetch login token
self.api.RequestGetP('/w/api.php', headers, {
action: 'query',
meta: 'tokens',
type: 'login',
format: 'json'
})
.then(response => { // success
if (response.query && response.query.tokens && response.query.tokens['logintoken']) {
self.login.logintoken = response.query.tokens['logintoken'];
self.logger.info("Wikipedia.login token:%s", self.login);
return self.api.RequestPostP('/w/api.php', headers, {
action: 'login',
format: 'json',
lgname: username
},
{
lgpassword: password,
lgtoken: self.login.logintoken
});
} else {
var error = new Error('no logintoken');
return reject(error);
}
})
.then(response => { // success
return resolve(response);
})
.catch(error => { // error
self.logger.error("Wikipedia.login error%s\n%#", error.message, error.stack);
return reject(error);
});
});
}//loginUser
where this.api is a simple wrapper of the Node.js http, the source code is available here and the api signatures are like:
Promise:API.RequestGetP(url,headers,querystring)
Promise:API.RequestPostP(url,headers,querystring,body)
If the currently accepted answer isn't working for someone, the following method will definitely work. I've used the axios library to send requests. Any library can be used but the key lies in formatting the body and headers correctly.
let url = "https://test.wikipedia.org/w/api.php";
let params = {
action: "query",
meta: "tokens",
type: "login",
format: "json"
};
axios.get(url, { params: params }).then(resp => {
let loginToken = resp.data.query.tokens.logintoken
let cookie = resp.headers["set-cookie"].join(';');
let body = {
action: 'login',
lgname: 'user_name',
lgpassword: 'password',
lgtoken: loginToken,
format: 'json'
}
let bodyData = new URLSearchParams(body).toString();
axios.post(url, bodyData, {
headers: {
Cookie: cookie,
}
}).then(resp => {
// You're now logged in!
// You'll have to add the following cookie in the headers again for any further requests that you might make
let cookie = resp.headers["set-cookie"].join(';')
console.log(resp.data)
})
})
And you should be seeing a response like
{
login: { result: 'Success', lguserid: 0000000, lgusername: 'Username' }
}
The second post request was where I got stuck for several hours, trying to figure out what was wrong. You need to send the data in an encoded form by using an API like URLSearchParams, or by just typing up the body as a string manually yourself.
I think from what you are saying you have lgtoken and lgname in the URL you are using, and then lgpassword and lgtoken (again!) in a JSON-encoded POST body.
This is not how the Mediawiki API works.
You submit it all as POST parameters. JSON is never involved, except when you ask for the result to come back in that format. I can't help you fix your code as you don't provide it, but that's what you need to do. (If you edit your question with your code, I'll do my best to help you.)
After seeing your code, I'll presume (without knowing the detail of your code) that you want something like this:
return self.api.RequestPostP('/w/api.php', headers, {
action: 'login',
format: 'json',
lgname: username,
lgpassword: password,
lgtoken: self.login.logintoken
});
I've got a small angulaerjs app that uses angularjs resource in one of it's controllers. Here is the code for resource usage
$scope.getFilteredTasks = function (filter) {
var resource = $resource('{0}/api/orderTasks/filterTasks'.format($scope.baseUrl), { status: '#status', type: '#type', createdDate: '#createdDate', agentType: '#agentType', page: '#page', pageSize: '#pageSize' }, {
'response': { method: 'GET', isArray: false }
});
$('#loading').show();
resource.response({
status: filter.status,
type: filter.type,
createdDate: filter.createdDate,
agentType: filter.agentType,
page: $scope.currentPage,
pageSize: $scope.pageSize
},
function (result) {
$scope.resultTasks = result.Items;
if ($scope.totalItems != result.TotalCount)
$scope.totalItems = result.TotalCount;
$('#loading').hide();
},
function (result) {
$('#loading').hide();
alert('Error in request!')
});
};
When the page first load everything is fine and i get all data that i need. But when I make second request (for example push the button on page that calls getFilteredTask(filter) function). I've got an result.status = -1. According to Fiddler and Chrome Network tab in dev tools request has status 200 but was cancelled. I've also checked the backend and found no problems, request was succsessfully handled and server returned all data that I need but I get cannceled request on client side.
UPDATE
It loooks like this problem appears only in Chrome. In IE 11 for example everything is ok
the issue might be that you were making the ajax request from a link, and not preventing the link from being followed. So if you are doing this in an onclick attribute, make sure to return false; as well.