/// <summary> /// Asynchronously uploads a file in this title. /// </summary> /// <param name="site"></param> /// <param name="title"></param> /// <param name="source">Source of the file.</param> /// <param name="comment">Comment of the upload, as well as the page content if it doesn't exist.</param> /// <param name="ignoreWarnings">Ignore any warnings. This must be set to upload a new version of an existing image.</param> /// <param name="watch">Whether to add the file into your watchlist.</param> /// <param name="cancellationToken">The cancellation token that will be checked prior to completing the returned task.</param> /// <exception cref="UnauthorizedAccessException">You do not have the permission to upload the file.</exception> /// <exception cref="OperationFailedException"> /// There's an general failure while uploading the file. /// - or - /// Since MW 1.31, if you are uploading the exactly same content to the same title /// with <paramref name="ignoreWarnings"/> set to <c>true</c>, /// you will reveive this exception with <see cref="OperationFailedException.ErrorCode"/> /// set to <c>fileexists-no-change</c>. See https://gerrit.wikimedia.org/r/378702 . /// </exception> /// <exception cref="TimeoutException">Timeout specified in <see cref="WikiClient.Timeout"/> has been reached.</exception> /// <returns>An <see cref="UploadResult"/>. You need to check <see cref="UploadResult.ResultCode"/> for further action.</returns> public static async Task <UploadResult> UploadAsync(this WikiSite site, string title, WikiUploadSource source, string comment, bool ignoreWarnings, AutoWatchBehavior watch, CancellationToken cancellationToken) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Debug.Assert(source != null); var link = WikiLink.Parse(site, title, BuiltInNamespaces.File); using (site.BeginActionScope(null, title, source)) { var requestFields = new Dictionary <string, object> { { "action", "upload" }, { "watchlist", watch }, { "token", WikiSiteToken.Edit }, { "filename", link.Title }, { "comment", comment }, { "ignorewarnings", ignoreWarnings }, }; foreach (var p in source.GetUploadParameters(site.SiteInfo)) { requestFields[p.Key] = p.Value; } var request = new MediaWikiFormRequestMessage(requestFields, true); site.Logger.LogDebug("Start uploading."); var jresult = await site.InvokeMediaWikiApiAsync(request, cancellationToken); var result = jresult["upload"].ToObject <UploadResult>(Utility.WikiJsonSerializer); site.Logger.LogInformation("Uploaded. Result={Result}.", result.ResultCode); return(result); } }
public static string?ToWikiQueryValue(object?value) { return(value switch { null => null, string _ => (string)value, bool b => b ? "" : null, AutoWatchBehavior awb => awb switch { AutoWatchBehavior.Default => "preferences", AutoWatchBehavior.None => "nochange", AutoWatchBehavior.Watch => "watch", AutoWatchBehavior.Unwatch => "unwatch", _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) },
/// <summary> /// Deletes the current page. /// </summary> public async Task <bool> DeleteAsync(string reason, AutoWatchBehavior watch, CancellationToken cancellationToken) { using (Site.BeginActionScope(this)) using (await Site.ModificationThrottler.QueueWorkAsync("Delete: " + this, cancellationToken)) { JToken jresult; try { jresult = await Site.InvokeMediaWikiApiAsync(new MediaWikiFormRequestMessage(new { action = "delete", token = WikiSiteToken.Delete, title = PageStub.HasTitle ? PageStub.Title : null, pageid = PageStub.HasTitle ? null : (int?)PageStub.Id, maxlag = 5, watchlist = watch, reason = reason, }), cancellationToken); } catch (OperationFailedException ex) { switch (ex.ErrorCode) { case "cantdelete": // Couldn't delete "title". Maybe it was deleted already by someone else case "missingtitle": return(false); case "permissiondenied": throw new UnauthorizedOperationException(ex); } throw; } var title = (string)jresult["delete"]["title"]; PageStub = new WikiPageStub(WikiPageStub.MissingPageIdMask, PageStub.Title, PageStub.NamespaceId); LastRevision = null; LastRevisionId = 0; Site.Logger.LogInformation("[[{Page}]] has been deleted.", title); return(true); } }
/// <summary> /// Deletes the current page. /// </summary> public async Task <bool> DeleteAsync(string reason, AutoWatchBehavior watch, CancellationToken cancellationToken) { using (await Site.ModificationThrottler.QueueWorkAsync("Delete: " + this, cancellationToken)) { var token = await Site.GetTokenAsync("delete", cancellationToken); JToken jresult; try { jresult = await Site.PostValuesAsync(new { action = "delete", title = Title, maxlag = 5, watchlist = watch, reason = reason, token = token, }, cancellationToken); } catch (OperationFailedException ex) { switch (ex.ErrorCode) { case "cantdelete": // Couldn't delete "title". Maybe it was deleted already by someone else case "missingtitle": return(false); case "permissiondenied": throw new UnauthorizedOperationException(ex); } throw; } var title = (string)jresult["delete"]["title"]; Exists = false; LastRevision = null; LastRevisionId = 0; Site.Logger?.Info(this, $"Page {title} has been deleted."); return(true); } }
//content can be // Stream file content // string url to fetch // UploadResult the previous failed upload private static async Task <UploadResult> UploadAsyncInternal(WikiSite site, object content, string title, string comment, bool ignoreWarnings, AutoWatchBehavior watch, CancellationToken cancellationToken) { if (site == null) { throw new ArgumentNullException(nameof(site)); } if (content == null) { throw new ArgumentNullException(nameof(content)); } if (title == null) { throw new ArgumentNullException(nameof(title)); } var link = WikiLink.Parse(site, title); if (link.Namespace.Id != BuiltInNamespaces.File) { throw new ArgumentException($"Invalid namespace for file title: {title} .", nameof(title)); } var token = await site.GetTokenAsync("edit"); long?streamPosition = null; HttpContent RequestFactory() { var requestContent = new MultipartFormDataContent { { new StringContent("json"), "format" }, { new StringContent("upload"), "action" }, { new StringContent(Utility.ToWikiQueryValue(watch)), "watchlist" }, { new StringContent(token), "token" }, { new StringContent(link.Title), "filename" }, { new StringContent(comment), "comment" }, }; if (content is Stream streamContent) { if (streamPosition < 0) { return(null); } // Memorize/reset the stream position. if (streamContent.CanSeek) { if (streamPosition == null) { streamPosition = streamContent.Position; } else { streamContent.Position = streamPosition.Value; } Debug.Assert(streamPosition >= 0); } else { // Mark for do-not-retry. streamPosition = -1; } requestContent.Add(new KeepAlivingStreamContent(streamContent), "file", title); } else if (content is string stringContent) { requestContent.Add(new StringContent(stringContent), "url"); } else if (content is UploadResult resultContent) { var key = (resultContent).FileKey; if (string.IsNullOrEmpty(key)) { throw new InvalidOperationException("The specified UploadResult has no valid FileKey."); } // sessionkey: Same as filekey, maintained for backward compatibility (deprecated in 1.18) requestContent.Add(new StringContent(key), site.SiteInfo.Version >= new Version(1, 18) ? "filekey" : "sessionkey"); } else { Debug.Assert(false, "Unrecognized content argument type."); } if (ignoreWarnings) { requestContent.Add(new StringContent(""), "ignorewarnings"); } return(requestContent); } site.Logger?.Info(site, $"Uploading: {link.Title} ."); var jresult = await site.PostContentAsync(RequestFactory, cancellationToken); var result = jresult["upload"].ToObject <UploadResult>(Utility.WikiJsonSerializer); site.Logger?.Info(site, $"Upload[{link.Title}]: {result}."); switch (result.ResultCode) { case UploadResultCode.Warning: throw new UploadException(result); default: // UploadResult.Result setter should have thrown an exception. Debug.Assert(result.ResultCode == UploadResultCode.Success || result.ResultCode == UploadResultCode.Continue); break; } return(result); }
/// <summary> /// Asynchronously uploads a file. /// </summary> /// <param name="site">MedaiWiki site.</param> /// <param name="content">Content of the file.</param> /// <param name="title">Title of the file, with or without File: prefix.</param> /// <param name="comment">Comment of the upload, as well as the page content if it doesn't exist.</param> /// <param name="ignoreWarnings">Ignore any warnings. This must be set to upload a new version of an existing image.</param> /// <param name="watch">Whether to add the file into your watchlist.</param> /// <param name="cancellationToken">The cancellation token that will be checked prior to completing the returned task.</param> /// <exception cref="UploadException"> /// There's warning from server, and <paramref name="ignoreWarnings"/> is <c>false</c>. /// Check <see cref="UploadException.UploadResult"/> for the warning message or continuing the upload. /// </exception> /// <exception cref="OperationFailedException"> There's an error while uploading the file. </exception> /// <exception cref="TimeoutException"> /// Timeout specified in <see cref="WikiClientBase.Timeout"/> has been reached. Note in this /// invocation, there will be no retries. /// </exception> /// <returns>An <see cref="UploadResult"/>.</returns> public static Task <UploadResult> UploadAsync(WikiSite site, Stream content, string title, string comment, bool ignoreWarnings, AutoWatchBehavior watch, CancellationToken cancellationToken) { return(UploadAsyncInternal(site, content, title, comment, ignoreWarnings, watch, cancellationToken)); }
/// <inheritdoc cref="UploadAsync(WikiSite,string,WikiUploadSource,string,bool,AutoWatchBehavior,CancellationToken)"/> public static Task <UploadResult> UploadAsync(this WikiSite site, string title, WikiUploadSource source, string comment, bool ignoreWarnings, AutoWatchBehavior watch) { return(UploadAsync(site, title, source, comment, ignoreWarnings, watch, CancellationToken.None)); }
/// <summary> /// Deletes the current page. /// </summary> public Task <bool> DeleteAsync(string reason, AutoWatchBehavior watch) { return(DeleteAsync(reason, watch, CancellationToken.None)); }
/// <summary> /// Moves (renames) a page. (MediaWiki 1.12) /// </summary> public async Task MoveAsync(string newTitle, string reason, PageMovingOptions options, AutoWatchBehavior watch, CancellationToken cancellationToken) { if (newTitle == null) { throw new ArgumentNullException(nameof(newTitle)); } if (newTitle == Title) { return; } using (Site.BeginActionScope(this)) using (await Site.ModificationThrottler.QueueWorkAsync("Move: " + this, cancellationToken)) { // When passing this to the Edit API, always pass the token parameter last // (or at least after the text parameter). That way, if the edit gets interrupted, // the token won't be passed and the edit will fail. // This is done automatically by mw.Api. JToken jresult; try { jresult = await Site.InvokeMediaWikiApiAsync(new MediaWikiFormRequestMessage(new { action = "move", token = WikiSiteToken.Move, from = PageStub.HasTitle ? PageStub.Title : null, fromid = PageStub.HasTitle ? null : (int?)PageStub.Id, to = newTitle, maxlag = 5, movetalk = (options & PageMovingOptions.LeaveTalk) != PageMovingOptions.LeaveTalk, movesubpages = (options & PageMovingOptions.MoveSubpages) == PageMovingOptions.MoveSubpages, noredirect = (options & PageMovingOptions.NoRedirect) == PageMovingOptions.NoRedirect, ignorewarnings = (options & PageMovingOptions.IgnoreWarnings) == PageMovingOptions.IgnoreWarnings, watchlist = watch, reason = reason, }), cancellationToken); } catch (OperationFailedException ex) { switch (ex.ErrorCode) { case "cantmove": case "protectedpage": case "protectedtitle": throw new UnauthorizedOperationException(ex); default: if (ex.ErrorCode.StartsWith("cantmove")) { throw new UnauthorizedOperationException(ex); } throw; } } var fromTitle = (string)jresult["move"]["from"]; var toTitle = (string)jresult["move"]["to"]; Site.Logger.LogInformation("Page [[{fromTitle}]] has been moved to [[{toTitle}]].", fromTitle, toTitle); var link = WikiLink.Parse(Site, toTitle); PageStub = new WikiPageStub(PageStub.Id, toTitle, link.Namespace.Id); } }
/// <summary> /// Submits content contained in <see cref="Content"/>, making edit to the page. /// (MediaWiki 1.16) /// </summary> /// <param name="summary">The edit summary. Leave it blank to use the default edit summary.</param> /// <param name="minor">Whether the edit is a minor edit. (See <a href="https://meta.wikimedia.org/wiki/Help:Minor_edit">m:Help:Minor Edit</a>)</param> /// <param name="bot">Whether to mark the edit as bot; even if you are using a bot account the edits will not be marked unless you set this flag.</param> /// <param name="watch">Specify how the watchlist is affected by this edit.</param> /// <param name="cancellationToken">A token used to cancel the operation.</param> /// <returns><c>true</c> if page content has been changed; <c>false</c> otherwise.</returns> /// <remarks> /// This action will refill <see cref="Id" />, <see cref="Title"/>, /// <see cref="ContentModel"/>, <see cref="LastRevisionId"/>, and invalidate /// <see cref="ContentLength"/>, <see cref="LastRevision"/>, and <see cref="LastTouched"/>. /// You should call <see cref="RefreshAsync()"/> again if you're interested in them. /// </remarks> /// <exception cref="InvalidOperationException">Cannot create actual page in the specified namespace.</exception> /// <exception cref="OperationConflictException">Edit conflict detected.</exception> /// <exception cref="UnauthorizedOperationException">You have no rights to edit the page.</exception> public async Task <bool> UpdateContentAsync(string summary, bool minor, bool bot, AutoWatchBehavior watch, CancellationToken cancellationToken) { using (Site.BeginActionScope(this)) using (await Site.ModificationThrottler.QueueWorkAsync("Edit: " + this, cancellationToken)) { // When passing this to the Edit API, always pass the token parameter last // (or at least after the text parameter). That way, if the edit gets interrupted, // the token won't be passed and the edit will fail. // This is done automatically by mw.Api. JToken jresult; try { jresult = await Site.InvokeMediaWikiApiAsync(new MediaWikiFormRequestMessage(new { action = "edit", token = WikiSiteToken.Edit, title = PageStub.HasTitle ? PageStub.Title : null, pageid = PageStub.HasTitle ? null : (int?)PageStub.Id, minor = minor, bot = bot, recreate = true, maxlag = 5, basetimestamp = LastRevision?.TimeStamp, watchlist = watch, summary = summary, text = Content, }), cancellationToken); } catch (OperationFailedException ex) { switch (ex.ErrorCode) { case "protectedpage": throw new UnauthorizedOperationException(ex); case "pagecannotexist": throw new InvalidOperationException(ex.ErrorMessage, ex); default: throw; } } var jedit = jresult["edit"]; var result = (string)jedit["result"]; if (result == "Success") { if (jedit["nochange"] != null) { Site.Logger.LogInformation("Submitted empty edit to page."); return(false); } ContentModel = (string)jedit["contentmodel"]; LastRevisionId = (int)jedit["newrevid"]; // jedit["ns"] == null PageStub = new WikiPageStub((int)jedit["pageid"], (string)jedit["title"], PageStub.NamespaceId); Site.Logger.LogInformation("Edited page. New revid={RevisionId}.", LastRevisionId); return(true); } // No "errors" in json result but result is not Success. throw new OperationFailedException(result, (string)null); } }
/// <inheritdoc cref="UpdateContentAsync(string,bool,bool,AutoWatchBehavior,CancellationToken)"/> public Task <bool> UpdateContentAsync(string summary, bool minor, bool bot, AutoWatchBehavior watch) { return(UpdateContentAsync(summary, minor, bot, watch, new CancellationToken())); }
/// <summary> /// Moves (renames) a page. (MediaWiki 1.12) /// </summary> public async Task MoveAsync(string newTitle, string reason, PageMovingOptions options, AutoWatchBehavior watch, CancellationToken cancellationToken) { if (newTitle == null) { throw new ArgumentNullException(nameof(newTitle)); } if (newTitle == Title) { return; } using (await Site.ModificationThrottler.QueueWorkAsync("Move: " + this, cancellationToken)) { var token = await Site.GetTokenAsync("move", cancellationToken); // When passing this to the Edit API, always pass the token parameter last // (or at least after the text parameter). That way, if the edit gets interrupted, // the token won't be passed and the edit will fail. // This is done automatically by mw.Api. JToken jresult; try { jresult = await Site.PostValuesAsync(new { action = "move", from = Title, to = newTitle, maxlag = 5, movetalk = (options & PageMovingOptions.LeaveTalk) != PageMovingOptions.LeaveTalk, movesubpages = (options & PageMovingOptions.MoveSubpages) == PageMovingOptions.MoveSubpages, noredirect = (options & PageMovingOptions.NoRedirect) == PageMovingOptions.NoRedirect, ignorewarnings = (options & PageMovingOptions.IgnoreWarnings) == PageMovingOptions.IgnoreWarnings, watchlist = watch, reason = reason, token = token, }, cancellationToken); } catch (OperationFailedException ex) { switch (ex.ErrorCode) { case "cantmove": case "protectedpage": case "protectedtitle": throw new UnauthorizedOperationException(ex); default: if (ex.ErrorCode.StartsWith("cantmove")) { throw new UnauthorizedOperationException(ex); } throw; } } var fromTitle = (string)jresult["move"]["from"]; var toTitle = (string)jresult["move"]["to"]; Site.Logger?.Info(this, $"Page {fromTitle} has been moved to {toTitle} ."); Title = toTitle; } }
/// <summary> /// Submits content contained in <see cref="Content"/>, making edit to the page. /// (MediaWiki 1.16) /// </summary> /// <returns><c>true</c> if page content has been changed; <c>false</c> otherwise.</returns> /// <remarks> /// This action will refill <see cref="Id" />, <see cref="Title"/>, /// <see cref="ContentModel"/>, <see cref="LastRevisionId"/>, and invalidate /// <see cref="ContentLength"/>, <see cref="LastRevision"/>, and <see cref="LastTouched"/>. /// You should call <see cref="RefreshAsync()"/> again /// if you're interested in them. /// </remarks> /// <exception cref="InvalidOperationException">Cannot create actual page in the specified namespace.</exception> /// <exception cref="OperationConflictException">Edit conflict detected.</exception> /// <exception cref="UnauthorizedOperationException">You have no rights to edit the page.</exception> public async Task <bool> UpdateContentAsync(string summary, bool minor, bool bot, AutoWatchBehavior watch, CancellationToken cancellationToken) { using (await Site.ModificationThrottler.QueueWorkAsync("Edit: " + this, cancellationToken)) { var token = await Site.GetTokenAsync("edit", cancellationToken); // When passing this to the Edit API, always pass the token parameter last // (or at least after the text parameter). That way, if the edit gets interrupted, // the token won't be passed and the edit will fail. // This is done automatically by mw.Api. JToken jresult; try { jresult = await Site.PostValuesAsync(new { action = "edit", title = Title, minor = minor, bot = bot, recreate = true, maxlag = 5, basetimestamp = LastRevision?.TimeStamp, watchlist = watch, summary = summary, text = Content, token = token, }, cancellationToken); } catch (OperationFailedException ex) { switch (ex.ErrorCode) { case "protectedpage": throw new UnauthorizedOperationException(ex); case "pagecannotexist": throw new InvalidOperationException(ex.ErrorMessage, ex); default: throw; } } var jedit = jresult["edit"]; var result = (string)jedit["result"]; if (result == "Success") { if (jedit["nochange"] != null) { return(false); } ContentModel = (string)jedit["contentmodel"]; LastRevisionId = (int)jedit["newrevid"]; Id = (int)jedit["pageid"]; Title = (string)jedit["title"]; return(true); } // No "errors" in json result but result is not Success. throw new OperationFailedException(result, (string)null); } }