/// <summary> /// Check whether the new image file is the same as the one we already have chosen. /// (or at least the same pathname in the filesystem) /// </summary> /// <remarks> /// See https://silbloom.myjetbrains.com/youtrack/issue/BL-2776. /// If the user goes out of his way to choose the exact same picture file from the /// original location again, a copy will still be created with a slightly revised /// name. Cropping a picture also results in a new copy of the file with the /// revised name. We still need a tool to remove unused picture files from a /// book's folder. (ie, BL-2351) /// </remarks> private bool IsSameFilePath(string bookFolderPath, UrlPathString src, PalasoImage imageInfo) { if (src!=null) { var path = Path.Combine(bookFolderPath, src.NotEncoded); if (path == imageInfo.OriginalFilePath) return true; } return false; }
public void Encoded_RoundTripTortureTest() { var url = "bread%20%2b%20cinnamon%20%26%20sugar%20%3d%20100%25%20yum.jpg"; Assert.AreEqual(url, UrlPathString.CreateFromUrlEncodedString(url).UrlEncoded); }
public void CreateFromHtmlXmlEncodedString_WithAmpersand_UnencodedAsExpected() { Assert.AreEqual("one&two", UrlPathString.CreateFromHtmlXmlEncodedString("one&two").NotEncoded); }
public void EqualityOperator_OneIsNull_False() { Assert.IsFalse(UrlPathString.CreateFromUrlEncodedString("test me") == null); }
public void EqualityOperator_AreEqual_True() { Assert.IsTrue(UrlPathString.CreateFromUrlEncodedString("test me") == UrlPathString.CreateFromUrlEncodedString("test " + "me")); }
public void Equals_AreEqual_True() { Assert.IsTrue(UrlPathString.CreateFromUrlEncodedString("test me").Equals(UrlPathString.CreateFromUrlEncodedString("test " + "me"))); }
public void UnencodedWithPlusAndSpace_RoundTripable() { Assert.AreEqual("test + me", UrlPathString.CreateFromUnencodedString("test + me").NotEncoded); }
public void CreateFromUnencodedString_ObviousStringWasAlreadyEncoded_Adapts() { Assert.AreEqual("test me", UrlPathString.CreateFromUnencodedString("test%20me").NotEncoded); Assert.AreEqual("test%me", UrlPathString.CreateFromUnencodedString("test%25me").NotEncoded); Assert.AreEqual("John&John", UrlPathString.CreateFromUnencodedString("John%26John").NotEncoded); }
public void Unencoded_toUnencoded_Correct() { Assert.AreEqual("test me", UrlPathString.CreateFromUnencodedString("test me").NotEncoded); }
public void UrlEncoded_withSpace_toUrlEncoded_SpaceEntityRetained() { Assert.AreEqual("test%20me", UrlPathString.CreateFromUrlEncodedString("test%20me").UrlEncoded); }
public void UrlEncoded_withPercent_toNotEncoded_PercentRetained() { Assert.AreEqual("OneHundred%", UrlPathString.CreateFromUrlEncodedString("OneHundred%25").NotEncoded); }
public void UrlEncodedWithPlusToMeanPlus_UnEncoded_PlusRetained() { //sometimes things encode a space a '+' instead of %20 Assert.AreEqual("one+one = two", UrlPathString.CreateFromUrlEncodedString("one+one%20=%20two").NotEncoded); }
/// <summary> /// When publishing videos in any form but PDF, we want to trim the actual video to just the part that /// the user wants to see and add the controls attribute, so that the video controls are visible. /// </summary> /// <param name="videoContainerElement">bloom-videoContainer element from copied DOM</param> /// <param name="sourceBookFolder">This is assumed to be a staging folder, we may replace videos here!</param> /// <returns>the new filepath if a video file exists and was copied, empty string if no video file was found</returns> public static string PrepareVideoForPublishing(XmlElement videoContainerElement, string sourceBookFolder, bool videoControls) { var videoFolder = Path.Combine(sourceBookFolder, "video"); var videoElement = videoContainerElement.SelectSingleNode("video") as XmlElement; if (videoElement == null) { return(string.Empty); } // In each valid video element, we remove any timings in the 'src' attribute of the source element. var sourceElement = videoElement.SelectSingleNode("source") as XmlElement; var srcAttrVal = sourceElement?.Attributes["src"]?.Value; if (srcAttrVal == null) { return(string.Empty); } string timings; var videoUrl = StripTimingFromVideoUrl(srcAttrVal, out timings); // Check for valid video file to match url var urlWithoutPrefix = UrlPathString.CreateFromUrlEncodedString(videoUrl.Substring(6)); // grab everything after 'video/' var originalVideoFilePath = Path.Combine(videoFolder, urlWithoutPrefix.NotEncoded); // any query already removed if (!RobustFile.Exists(originalVideoFilePath)) { return(string.Empty); } var tempName = originalVideoFilePath; if (!string.IsNullOrEmpty(FfmpegProgram) && !string.IsNullOrEmpty(timings) && IsVideoMarkedForTrimming(sourceBookFolder, videoUrl, timings)) { tempName = Path.Combine(videoFolder, GetNewVideoFileName()); var successful = TrimVideoUsingFfmpeg(originalVideoFilePath, tempName, timings); if (successful) { RobustFile.Delete(originalVideoFilePath); var trimmedFileName = BookStorage.GetVideoFolderName + Path.GetFileName(tempName); HtmlDom.SetVideoElementUrl(new ElementProxy(videoContainerElement), UrlPathString.CreateFromUnencodedString(trimmedFileName, true), false); } else { // probably doesn't exist, but if it does we don't need it. // File.Delete (underneath RobustFile.Delete) does not throw if the file doesn't exist. RobustFile.Delete(tempName); tempName = originalVideoFilePath; } } if (videoControls) { // Add playback controls needed for videos to work in Readium and possibly other epub readers. videoElement.SetAttribute("controls", string.Empty); } return(tempName); }
/// <summary> /// In case the user has been editing video files outside of Bloom, for example, /// after using the Show In Folder command in the sign language toolbox, /// we want to update the pages that show those videos to use the new versions. /// Unfortunately something, probably the browser itself, seems to be very /// persistent about caching videos, even though our server tells it not to cache /// things in the book folder (and debug builds don't ever tell it to cache anything). /// We prevent cache hits on modified files by adding a fake param to the URI. /// (Also we will remove any trimming...see comment below.) /// This function is called whenever Bloom is activated in Edit mode. /// </summary> /// <param name="sender"></param> /// <param name="eventArgs"></param> public void CheckForChangedVideoOnActivate(object sender, EventArgs eventArgs) { // We're only going to check for video changes on the current book, if any. // It's not inconceivable that whatever caches videos will keep a cached one // for another book, but I think trying to modify books we don't even have open // is to dangerous, as well as quite difficult. if (CurrentBook == null) { return; } // These two mechanisms are in danger of fighting over the change. If we're in the // middle of editing a single file from the Bloom command, don't do anything here. // Note: this means we _could_ miss another edit the user did while he was off doing // the edit outside. But the race condition between the event handlers for Bloom activated // and the end of the edit-outside process is a real one (different threads) and they // did really overlap before I put this in. The edit-outside process is looking for the most recently // modified video to replace the current one, so things are likely to get confused // anyway if the user is trying to use both mechanisms at once. This mechanism can't // just replace that one (at least as it stands), because we're expecting the outside // program to make a new file, allowing Bloom to save the old one as an original, while // this is looking for in-place changes. // A downside is that if the user never closes the edit-outside program, this mechanism // will stay disabled. But I don't see a better answer, at least if we keep both commands. if (_doingEditOutsideBloom) { return; } // On Linux, this method interferes with successfully referencing a newly imported video file. // See https://issues.bloomlibrary.org/youtrack/issue/BL-6723. Ignoring just the one call to // this method suffices for things to work. (On Windows, the sequence of events differs, but // this change is safe.) if (_importedVideoIntoBloom) { _importedVideoIntoBloom = false; return; } var videoFolderPath = BookStorage.GetVideoDirectoryAndEnsureExistence(CurrentBook.FolderPath); var filesModifiedSinceDeactivate = new DirectoryInfo(videoFolderPath) .GetFiles("*.mp4") .Where(f => GetRealLastModifiedTime(f) > DeactivateTime) .Select(f => f.FullName) .ToList(); if (!filesModifiedSinceDeactivate.Any()) { return; } // We might modify the current page, but the user may also have modified it // without doing anything to cause a Save before the deactivate. So save their // changes before we go to work on it. Model.SaveNow(); foreach (var videoPath in filesModifiedSinceDeactivate) { var expectedSrcAttr = UrlPathString.CreateFromUnencodedString(BookStorage.GetVideoFolderName + Path.GetFileName(videoPath)); var videoElts = CurrentBook.RawDom.SafeSelectNodes($"//video/source[contains(@src,'{expectedSrcAttr.UrlEncodedForHttpPath}')]"); if (videoElts.Count == 0) { continue; // not used in book, ignore } // OK, the user has modified the file outside of Bloom. Something is determined to cache video. // Defeat it by setting a fake param. // Note that doing this will discard any fragment in the existing URL, typically trimming. // I think this is good...if the user has edited the video, we should start over assuming he // wants all of it. var newSrcAttr = UrlPathString.CreateFromUnencodedString(BookStorage.GetVideoFolderName + Path.GetFileName(videoPath)); HtmlDom.SetSrcOfVideoElement(newSrcAttr, new ElementProxy((XmlElement)videoElts[0]), true, "?now=" + DateTime.Now.Ticks); } // We could try to figure out whether one of the modified videos is on the current page. // But that's the most likely video to be modified, and it doesn't take long to reload, // and this only happens in the very special case that the user has modified a video outside // of Bloom. View.UpdateSingleDisplayedPage(_pageSelection.CurrentSelection); // Likewise, this is probably overkill, but it's a probably-rare case. View.UpdateAllThumbnails(); }
private bool ProcessAnyFileContent(IRequestInfo info, string localPath) { string modPath = localPath; string path = null; var urlPath = UrlPathString.CreateFromUrlEncodedString(modPath); var tempPath = urlPath.NotEncoded; if (RobustFile.Exists(tempPath)) { modPath = tempPath; } try { if (localPath.Contains("favicon.ico")) //need something to pacify Chrome { path = FileLocator.GetFileDistributedWithApplication("BloomPack.ico"); } // Is this request the full path to an image file? For most images, we just have the filename. However, in at // least one use case, the image we want isn't in the folder of the PDF we're looking at. That case is when // we are looking at a "folio", a book that gathers up other books into one big PDF. In that case, we want // to find the image in the correct book folder. See AddChildBookContentsToFolio(); var possibleFullImagePath = localPath; // "OriginalImages/" at the beginning means we're generating a pdf and want full images, // but it has nothing to do with the actual file location. if (localPath.StartsWith(OriginalImageMarker + "/")) { possibleFullImagePath = localPath.Substring(15); } if (RobustFile.Exists(possibleFullImagePath) && Path.IsPathRooted(possibleFullImagePath)) { path = possibleFullImagePath; } else { // Surprisingly, this method will return localPath unmodified if it is a fully rooted path // (like C:\... or \\localhost\C$\...) to a file that exists. So this execution path // can return contents of any file that exists if the URL gives its full path...even ones that // are generated temp files most certainly NOT distributed with the application. path = FileLocator.GetFileDistributedWithApplication(BloomFileLocator.BrowserRoot, modPath); } } catch (ApplicationException e) { // Might be from GetFileDistributedWithApplication above, but we could be checking templates that // are NOT distributed with the application. // Otherwise ignore. Assume this means that this class/method cannot serve that request, // but something else may. if (e.Message.StartsWith("Could not locate the required file")) { // LocateFile includes userInstalledSearchPaths (e.g. a shortcut to a collection in a non-standard location) path = BloomFileLocator.sTheMostRecentBloomFileLocator.LocateFile(localPath); } } //There's probably a eventual way to make this problem go away, // but at the moment FF, looking for source maps to go with css, is // looking for those maps where we said the css was, which is in the actual // book folders. So instead redirect to our browser file folder. if (string.IsNullOrEmpty(path) || !RobustFile.Exists(path)) { var startOfBookLayout = localPath.IndexOf("bookLayout"); if (startOfBookLayout > 0) { path = BloomFileLocator.GetBrowserFile(false, localPath.Substring(startOfBookLayout)); } var startOfBookEdit = localPath.IndexOf("bookEdit"); if (startOfBookEdit > 0) { path = BloomFileLocator.GetBrowserFile(false, localPath.Substring(startOfBookEdit)); } } if (!RobustFile.Exists(path) && localPath.StartsWith("pageChooser/") && IsImageTypeThatCanBeReturned(localPath)) { // if we're in the page chooser dialog and looking for a thumbnail representing an image in a // template page, look for that thumbnail in the book that is the template source, // rather than in the folder that stores the page choose dialog HTML and code. var templateBook = _bookSelection.CurrentSelection.FindTemplateBook(); if (templateBook != null) { var pathMinusPrefix = localPath.Substring("pageChooser/".Length); var templatePath = Path.Combine(templateBook.FolderPath, pathMinusPrefix); if (RobustFile.Exists(templatePath)) { info.ReplyWithImage(templatePath); return(true); } // Might be a page from a different template than the one we based this book on path = BloomFileLocator.sTheMostRecentBloomFileLocator.LocateFile(pathMinusPrefix); if (!string.IsNullOrEmpty(path)) { info.ReplyWithImage(path); return(true); } } } // Use '%25' to detect that the % in a Url encoded character (for example space encoded as %20) was encoded as %25. // In this example we would have %2520 in info.RawUrl and %20 in localPath instead of a space. Note that if an // image has a % in the filename, like 'The other 50%', and it isn't doubly encoded, then this shouldn't be a // problem because we're triggering here only if the file isn't found. if (!RobustFile.Exists(localPath) && info.RawUrl.Contains("%25")) { // possibly doubly encoded? decode one more time and try. See https://silbloom.myjetbrains.com/youtrack/issue/BL-3835. // Some existing books have somehow acquired Url encoded coverImage data like the following: // <div data-book="coverImage" lang="*"> // The%20Moon%20and%20The%20Cap_Cover.png // </div> // This leads to data being stored doubly encoded in the program's run-time data. The coverImage data is supposed to be // Html/Xml encoded (using &), not Url encoded (using %). path = System.Web.HttpUtility.UrlDecode(localPath); } if (!RobustFile.Exists(path) && IsImageTypeThatCanBeReturned(localPath) && _bookSelection.CurrentSelection != null) { // last resort...maybe we are in the process of renaming a book (BL-3345) and something mysteriously is still using // the old path. For example, I can't figure out what hangs on to the old path when an image is changed after // altering the main book title. var currentFolderPath = Path.Combine(_bookSelection.CurrentSelection.FolderPath, Path.GetFileName(localPath)); if (RobustFile.Exists(currentFolderPath)) { info.ReplyWithImage(currentFolderPath); return(true); } } if (!RobustFile.Exists(path)) { // On developer machines, we can lose part of path earlier. Try one more thing. path = info.LocalPathWithoutQuery.Substring(7); // skip leading "/bloom/"); } if (!RobustFile.Exists(path)) { if (ShouldReportFailedRequest(info, CurrentBook?.FolderPath)) { ReportMissingFile(localPath, path); } return(false); // from here we head off to ServerBase.MakeReply() which now uses the same ShouldReportFailedRequest() method. } info.ContentType = GetContentType(Path.GetExtension(modPath)); info.ReplyWithFileContent(path); return(true); }
public void PathOnly_HasQuery_StripsQuery() { Assert.AreEqual("test me", UrlPathString.CreateFromUnencodedString("test%20me?12345").PathOnly.NotEncoded); }
public void CreateFromUnencodedString_LooksEncodedButSetStrictlyTreatAsEncodedTrue_RoundTrips() { Assert.AreEqual("test%20me", UrlPathString.CreateFromUnencodedString("test%20me", true).NotEncoded); }
public void PathOnly_HasNoQuery_ReturnsAll() { Assert.AreEqual("test me", UrlPathString.CreateFromUnencodedString("test%20me").PathOnly.NotEncoded); }
public void UrlEncodedWithPlusToMeanSpace_toUrlEncoded_Retained() { //sometimes things encode a space a '+' instead of %20 Assert.AreEqual("test%20me", UrlPathString.CreateFromUrlEncodedString("test+me").UrlEncoded); }
public void PathOnly_AmbiguousInput_RoundTrips() { Assert.AreEqual("test+me", UrlPathString.CreateFromUnencodedString("test+me").PathOnly.NotEncoded); }
public void UnencodedWithAmpersand_RoundTripable() { Assert.AreEqual("test&me", UrlPathString.CreateFromUnencodedString("test&me").NotEncoded); Assert.AreEqual("test & me", UrlPathString.CreateFromUnencodedString("test & me").NotEncoded); }
public void HandleI18nRequest(ApiRequest request) { var lastSegment = request.LocalPath().Split(new char[] { '/' }).Last(); switch (lastSegment) { case "loadStrings": var d = new Dictionary <string, string>(); var post = request.GetPostDataWhenFormEncoded(); if (post != null) { foreach (string key in post.Keys) { try { if (d.ContainsKey(key)) { continue; } var translation = GetTranslationDefaultMayNotBeEnglish(key, post[key]); d.Add(key, translation); } catch (Exception error) { Debug.Fail("Debug Only:" + error.Message + Environment.NewLine + "A bug reported at this location is BL-923"); //Until BL-923 is fixed (hard... it's a race condition, it's better to swallow this for users } } } request.ReplyWithJson(JsonConvert.SerializeObject(d)); break; case "translate": var parameters = request.Parameters; string id = parameters["key"]; string englishText = parameters["englishText"]; string langId = parameters["langId"]; string dontWarnIfMissing = parameters["dontWarnIfMissing"]; bool isDontWarnIfMissing = dontWarnIfMissing != null && dontWarnIfMissing.Equals("true"); string isoCode1; string isoCodeM1; string isoCodeM2; if (request.CurrentBook != null) { isoCode1 = request.CurrentBook.BookData.Language1IsoCode; isoCodeM1 = request.CurrentBook.BookData.MetadataLanguage1IsoCode; isoCodeM2 = request.CurrentBook.BookData.MetadataLanguage2IsoCode; } else { isoCode1 = request.CurrentCollectionSettings.Language1.Iso639Code; isoCodeM1 = request.CurrentCollectionSettings.Language2.Iso639Code; isoCodeM2 = request.CurrentCollectionSettings.Language3?.Iso639Code ?? ""; } langId = langId.Replace("V", isoCode1); langId = langId.Replace("N1", isoCodeM1); langId = langId.Replace("N2", isoCodeM2); langId = langId.Replace("UI", LocalizationManager.UILanguageId); if (GetSomeTranslation(id, langId, out var localizedString)) { // Ensure that we actually have a value for localized string. (This should already be true, but I'm paranoid.) if (localizedString == null) { localizedString = englishText; } request.ReplyWithJson(new { text = localizedString, success = true }); } else { var idFound = true; // Don't report missing strings if they are numbers // Enhance: We might get the Javascript to do locale specific numbers someday // The C# side doesn't currently have the smarts to do DigitSubstitution // See Remark at https://msdn.microsoft.com/en-us/library/system.globalization.numberformatinfo.digitsubstitution(v=vs.110).aspx if (IsInteger(id)) { englishText = id; } else { // Now that end users can create templates, it's annoying to report that their names, // page labels, and page descriptions don't have localizations. if (IsTemplateBookKey(id)) { englishText = englishText.Trim(); } else { // it's ok if we don't have a translation, but if the string isn't even in the list of things that need translating, // then we want to remind the developer to add it to the english xlf file. if (!LocalizationManager.GetIsStringAvailableForLangId(id, "en")) { if (!isDontWarnIfMissing) { ReportL10NMissingString(id, englishText, UrlPathString.CreateFromUrlEncodedString(parameters["comment"] ?? "").NotEncoded); } idFound = false; } else { //ok, so we don't have it translated yet. Make sure it's at least listed in the things that can be translated. // And return the English string, which is what we would do the next time anyway. (BL-3374) LocalizationManager.GetDynamicString("Bloom", id, englishText); } } } request.ReplyWithJson(new { text = englishText, success = idFound }); } break; case "uilang": request.ReplyWithText(LocalizationManager.UILanguageId); break; default: request.Failed(); break; } }
public void Equals_AreNotEqual_False() { Assert.IsFalse(UrlPathString.CreateFromUrlEncodedString("test me").Equals(UrlPathString.CreateFromUrlEncodedString("test him"))); }
public static bool HandleRequest(string localPath, IRequestInfo info, CollectionSettings currentCollectionSettings) { var lastSep = localPath.IndexOf("/", System.StringComparison.Ordinal); var lastSegment = (lastSep > -1) ? localPath.Substring(lastSep + 1) : localPath; switch (lastSegment) { case "loadStrings": while (_localizing) { Thread.Sleep(0); } try { _localizing = true; var d = new Dictionary <string, string>(); var post = info.GetPostDataWhenFormEncoded(); if (post != null) { foreach (string key in post.Keys) { try { if (d.ContainsKey(key)) { continue; } // Now that end users can create templates, it's annoying to report that their names, // page labels, and page descriptions don't have localizations. if (IsTemplateBookKey(key)) { continue; } var translation = GetTranslationDefaultMayNotBeEnglish(key, post[key]); d.Add(key, translation); } catch (Exception error) { Debug.Fail("Debug Only:" + error.Message + Environment.NewLine + "A bug reported at this location is BL-923"); //Until BL-923 is fixed (hard... it's a race condition, it's better to swallow this for users } } } info.ContentType = "application/json"; info.WriteCompleteOutput(JsonConvert.SerializeObject(d)); return(true); } finally { _localizing = false; } break; case "translate": var parameters = info.GetQueryParameters(); string id = parameters["key"]; string englishText = parameters["englishText"]; string langId = parameters["langId"]; langId = langId.Replace("V", currentCollectionSettings.Language1Iso639Code); langId = langId.Replace("N1", currentCollectionSettings.Language2Iso639Code); langId = langId.Replace("N2", currentCollectionSettings.Language3Iso639Code); langId = langId.Replace("UI", LocalizationManager.UILanguageId); if (LocalizationManager.GetIsStringAvailableForLangId(id, langId)) { info.ContentType = "text/plain"; info.WriteCompleteOutput(LocalizationManager.GetDynamicStringOrEnglish("Bloom", id, englishText, null, langId)); return(true); } else { // Don't report missing strings if they are numbers // Enhance: We might get the Javascript to do locale specific numbers someday // The C# side doesn't currently have the smarts to do DigitSubstitution // See Remark at https://msdn.microsoft.com/en-us/library/system.globalization.numberformatinfo.digitsubstitution(v=vs.110).aspx if (IsInteger(id)) { englishText = id; } else { // Now that end users can create templates, it's annoying to report that their names, // page labels, and page descriptions don't have localizations. if (IsTemplateBookKey(id)) { englishText = englishText.Trim(); } else { // it's ok if we don't have a translation, but if the string isn't even in the list of things that need translating, // then we want to remind the developer to add it to the english xlf file. if (!LocalizationManager.GetIsStringAvailableForLangId(id, "en")) { ReportL10NMissingString(id, englishText, UrlPathString.CreateFromUrlEncodedString(parameters["comment"] ?? "").NotEncoded); } else { //ok, so we don't have it translated yet. Make sure it's at least listed in the things that can be translated. // And return the English string, which is what we would do the next time anyway. (BL-3374) LocalizationManager.GetDynamicString("Bloom", id, englishText); } } } info.ContentType = "text/plain"; info.WriteCompleteOutput(englishText); return(true); } break; } return(false); }
public void EqualityOperator_AreNotEqual_False() { Assert.IsFalse(UrlPathString.CreateFromUrlEncodedString("test me") == UrlPathString.CreateFromUrlEncodedString("different")); }
public void PathOnly_LooksEncodedButSetStrictlyTreatAsEncodedTrue_RoundTrips() { //this checks that PathOnly doesn't do processing in ambiguous mode, undoing the information we gave it to be strict Assert.AreEqual("test%20me", UrlPathString.CreateFromUnencodedString("test%20me", true).PathOnly.NotEncoded); }
public void HtmlEncodedWithAmpersand_RoundTripable() { var s = "one&two"; Assert.AreEqual(s, UrlPathString.CreateFromHtmlXmlEncodedString(s).HtmlXmlEncoded); }
public void QueryOnly_HasQuery_ReturnsIt() { Assert.That(UrlPathString.CreateFromUnencodedString("test%20me?12345").QueryOnly.NotEncoded, Is.EqualTo("?12345")); }
public void Unencoded_RoundTripTortureTest() { var fileName = "bread + cinnamon & sugar = 100% yum.JPG"; Assert.AreEqual(fileName, UrlPathString.CreateFromUnencodedString(fileName).NotEncoded); }
public void QueryOnly_NoQuery_ReturnsEmpty() { Assert.That(UrlPathString.CreateFromUnencodedString("test%20me").QueryOnly.NotEncoded, Is.EqualTo("")); }
/// <summary> /// Sets the url attribute either of an img (the src attribute) /// or a div with an inline style with an background-image rule /// </summary> public static void SetImageElementUrl(ElementProxy imgOrDivWithBackgroundImage, UrlPathString url) { if(imgOrDivWithBackgroundImage.Name.ToLower() == "img") { imgOrDivWithBackgroundImage.SetAttribute("src", url.UrlEncoded); } else { imgOrDivWithBackgroundImage.SetAttribute("style", string.Format("background-image:url('{0}')", url.UrlEncoded)); } }
// Needs to be thread-safe private string GetBookStatusJson(string bookFolderName, Book.Book book) { string whoHasBookLocked = null; DateTime whenLocked = DateTime.MaxValue; bool problem = false; // bookFolderName may be null when no book is selected, e.g., after deleting one. var status = bookFolderName == null ? null :_tcManager.CurrentCollection?.GetStatus(bookFolderName); // At this level, we know this is the path to the .bloom file in the repo // (though if we implement another backend, we'll have to generalize the notion somehow). // For the Javascript, it's just an argument to pass to // CommonMessages.GetPleaseClickHereForHelpMessage(). It's only used if hasInvalidRepoData is non-empty. string clickHereArg = ""; var folderTC = _tcManager.CurrentCollection as FolderTeamCollection; if (folderTC != null && bookFolderName != null) { clickHereArg = UrlPathString.CreateFromUnencodedString(folderTC.GetPathToBookFileInRepo(bookFolderName)) .UrlEncoded; } string hasInvalidRepoData = (status?.hasInvalidRepoData ?? false) ? (folderTC)?.GetCouldNotOpenCorruptZipMessage() : ""; if (bookFolderName == null) { return(JsonConvert.SerializeObject( new { // Keep this in sync with IBookTeamCollectionStatus defined in TeamCollectionApi.tsx who = "", whoFirstName = "", whoSurname = "", when = DateTime.Now.ToShortDateString(), where = "", currentUser = CurrentUser, currentUserName = TeamCollectionManager.CurrentUserFirstName, currentMachine = TeamCollectionManager.CurrentMachine, problem = "", hasInvalidRepoData = false, clickHereArg = "", changedRemotely = false, disconnected = false, newLocalBook = true, checkinMessage = "", isUserAdmin = _tcManager.OkToEditCollectionSettings })); } bool newLocalBook = false; try { whoHasBookLocked = _tcManager.CurrentCollectionEvenIfDisconnected?.WhoHasBookLocked(bookFolderName); // It's debatable whether to use CurrentCollectionEvenIfDisconnected everywhere. For now, I've only changed // it for the two bits of information actually needed by the status panel when disconnected. whenLocked = _tcManager.CurrentCollection?.WhenWasBookLocked(bookFolderName) ?? DateTime.MaxValue; if (whoHasBookLocked == TeamCollection.FakeUserIndicatingNewBook) { // This situation comes about from two different scenarios: // 1) The user is creating a new book and TeamCollection status doesn't matter // 2) The user is trying to check out an existing book and TeamCollectionManager // discovers [through CheckConnection()] that it is suddenly in a disconnected // state. // In both cases, the current selected book is in view. The only way to tell // these two situations apart is that in (1) book.IsSaveable is true // and in (2) it is not. // Or, book may be null because we're just getting a status to show in the list // of all books. In that case, book is null, but it's fairly safe to assume it's a new local book. if (book?.IsSaveable ?? true) { whoHasBookLocked = CurrentUser; newLocalBook = true; } else { whoHasBookLocked = null; } } problem = _tcManager.CurrentCollection?.HasLocalChangesThatMustBeClobbered(bookFolderName) ?? false; } catch (Exception e) when(e is ICSharpCode.SharpZipLib.Zip.ZipException || e is IOException) { hasInvalidRepoData = (_tcManager.CurrentCollection as FolderTeamCollection)?.GetCouldNotOpenCorruptZipMessage(); } // If the request asked for the book by name, we don't have an actual Book object. // However, it happens that those requests don't need the checkinMessage. var checkinMessage = book == null ? "" : BookHistory.GetPendingCheckinMessage(book); return(JsonConvert.SerializeObject( new { // Keep this in sync with IBookTeamCollectionStatus defined in TeamCollectionApi.tsx who = whoHasBookLocked, whoFirstName = _tcManager.CurrentCollection?.WhoHasBookLockedFirstName(bookFolderName), whoSurname = _tcManager.CurrentCollection?.WhoHasBookLockedSurname(bookFolderName), when = whenLocked.ToLocalTime().ToShortDateString(), where = _tcManager.CurrentCollectionEvenIfDisconnected?.WhatComputerHasBookLocked(bookFolderName), currentUser = CurrentUser, currentUserName = TeamCollectionManager.CurrentUserFirstName, currentMachine = TeamCollectionManager.CurrentMachine, problem, hasInvalidRepoData, clickHereArg, changedRemotely = _tcManager.CurrentCollection?.HasBeenChangedRemotely(bookFolderName), disconnected = _tcManager.CurrentCollectionEvenIfDisconnected?.IsDisconnected, newLocalBook, checkinMessage, isUserAdmin = _tcManager.OkToEditCollectionSettings })); }