/// <summary> /// Get a json of the book's settings. /// </summary> private void HandleBookSettings(ApiRequest request) { switch (request.HttpMethod) { case HttpMethods.Get: dynamic settings = new ExpandoObject(); settings.isRecordedAsLockedDown = _bookSelection.CurrentSelection.RecordedAsLockedDown; settings.unlockShellBook = _bookSelection.CurrentSelection.TemporarilyUnlocked; settings.currentToolBoxTool = _bookSelection.CurrentSelection.BookInfo.CurrentTool; settings.isTemplateBook = GetIsBookATemplate(); request.ReplyWithJson((object)settings); break; case HttpMethods.Post: //note: since we only have this one value, it's not clear yet whether the panel involved here will be more of a //an "edit settings", or a "book settings", or a combination of them. settings = DynamicJson.Parse(request.RequiredPostJson()); _bookSelection.CurrentSelection.TemporarilyUnlocked = settings["unlockShellBook"]; _pageRefreshEvent.Raise(PageRefreshEvent.SaveBehavior.SaveBeforeRefresh); if(((DynamicJson)settings).IsDefined("isTemplateBook")) { UpdateBookTemplateMode(settings.isTemplateBook); } request.Succeeded(); break; default: throw new ArgumentOutOfRangeException(); } }
public static bool Handle(EndpointRegistration endpointRegistration, IRequestInfo info, CollectionSettings collectionSettings, Book.Book currentBook) { var request = new ApiRequest(info, collectionSettings, currentBook); try { if(Program.RunningUnitTests) { endpointRegistration.Handler(request); } else { var formForSynchronizing = Application.OpenForms.Cast<Form>().Last(); if (endpointRegistration.HandleOnUIThread && formForSynchronizing.InvokeRequired) { formForSynchronizing.Invoke(endpointRegistration.Handler, request); } else { endpointRegistration.Handler(request); } } if(!info.HaveOutput) { throw new ApplicationException(string.Format("The EndpointHandler for {0} never called a Succeeded(), Failed(), or ReplyWith() Function.", info.RawUrl.ToString())); } } catch (Exception e) { SIL.Reporting.ErrorReport.ReportNonFatalExceptionWithMessage(e, info.RawUrl); return false; } return true; }
/// <summary> /// Get a json of stats about the image. It is used to populate a tooltip when you hover over an image container /// </summary> private void HandleImageInfo(ApiRequest request) { try { var fileName = request.RequiredFileNameOrPath("image"); Guard.AgainstNull(_bookSelection.CurrentSelection, "CurrentBook"); var plainfilename = fileName.NotEncoded; // The fileName might be URL encoded. See https://silbloom.myjetbrains.com/youtrack/issue/BL-3901. var path = UrlPathString.GetFullyDecodedPath(_bookSelection.CurrentSelection.FolderPath, ref plainfilename); RequireThat.File(path).Exists(); var fileInfo = new FileInfo(path); dynamic result = new ExpandoObject(); result.name = plainfilename; result.bytes = fileInfo.Length; // Using a stream this way, according to one source, // http://stackoverflow.com/questions/552467/how-do-i-reliably-get-an-image-dimensions-in-net-without-loading-the-image, // supposedly avoids loading the image into memory when we only want its dimensions using(var stream = RobustFile.OpenRead(path)) using(var img = Image.FromStream(stream, false, false)) { result.width = img.Width; result.height = img.Height; switch(img.PixelFormat) { case PixelFormat.Format32bppArgb: case PixelFormat.Format32bppRgb: case PixelFormat.Format32bppPArgb: result.bitDepth = "32"; break; case PixelFormat.Format24bppRgb: result.bitDepth = "24"; break; case PixelFormat.Format16bppArgb1555: case PixelFormat.Format16bppGrayScale: result.bitDepth = "16"; break; case PixelFormat.Format8bppIndexed: result.bitDepth = "8"; break; case PixelFormat.Format1bppIndexed: result.bitDepth = "1"; break; default: result.bitDepth = "unknown"; break; } } request.ReplyWithJson((object) result); } catch(Exception e) { Logger.WriteEvent("Error in server imageInfo/: url was " + request.LocalPath()); Logger.WriteEvent("Error in server imageInfo/: exception is " + e.Message); request.Failed(e.Message); NonFatalProblem.Report(ModalIf.None, PassiveIf.Alpha, "Request Error", request.LocalPath(), e); } }
private void HandleAddPage(ApiRequest request) { var page = GetPageTemplate(request); if (page != null) { _templateInsertionCommand.Insert(page as Page); request.Succeeded(); return; } }
/// <summary> /// Called by the server to handle API calls for page thumbnails. /// </summary> public void HandleThumbnailRequest(ApiRequest request) { var filePath = request.LocalPath().Replace("api/pageTemplateThumbnail/",""); var pathToExistingOrGeneratedThumbnail = FindOrGenerateThumbnail(filePath); if(string.IsNullOrEmpty(pathToExistingOrGeneratedThumbnail) || !File.Exists(pathToExistingOrGeneratedThumbnail)) { request.Failed("Could not make a page thumbnail for "+filePath); return; } request.ReplyWithImage(pathToExistingOrGeneratedThumbnail); }
/// <summary> /// Returns a json string for initializing the AddPage dialog. It gives paths to our current TemplateBook /// and specifies whether the dialog is to be used for adding pages or choosing a different layout. /// </summary> public void HandleTemplatesRequest(ApiRequest request) { dynamic addPageSettings = new ExpandoObject(); addPageSettings.defaultPageToSelect = _templateInsertionCommand.MostRecentInsertedTemplatePage == null ? "" : _templateInsertionCommand.MostRecentInsertedTemplatePage.Id; addPageSettings.orientation = _bookSelection.CurrentSelection.GetLayout().SizeAndOrientation.IsLandScape ? "landscape" : "portrait"; addPageSettings.groups = GetBookTemplatePaths(GetPathToCurrentTemplateHtml(), _sourceCollectionsList.GetSourceBookPaths()) .Select(bookTemplatePath => GetPageGroup(bookTemplatePath)); addPageSettings.currentLayout = _pageSelection.CurrentSelection.IdOfFirstAncestor; request.ReplyWithJson(JsonConvert.SerializeObject(addPageSettings)); }
private void HandleChangeLayout(ApiRequest request) { var templatePage = GetPageTemplate(request); if (templatePage != null) { var pageToChange = /*PageChangingLayout ??*/ _pageSelection.CurrentSelection; var book = _pageSelection.CurrentSelection.Book; book.UpdatePageToTemplate(book.OurHtmlDom, templatePage.GetDivNodeForThisPage(), pageToChange.Id); // The Page objects are cached in the page list and may be used if we issue another // change layout command. We must update their lineage so the right "current layout" // will be shown if the user changes the layout of the same page again. var pageChanged = pageToChange as Page; if (pageChanged != null) pageChanged.UpdateLineage(new[] { templatePage.Id }); _pageRefreshEvent.Raise(PageRefreshEvent.SaveBehavior.JustRedisplay); request.Succeeded(); } }
private IPage GetPageTemplate(ApiRequest request) { var requestData = DynamicJson.Parse(request.RequiredPostJson()); //var templateBookUrl = request.RequiredParam("templateBookUrl"); var templateBookPath = HttpUtility.HtmlDecode(requestData.templateBookPath); var templateBook = _sourceCollectionsList.FindAndCreateTemplateBookByFullPath(templateBookPath); if(templateBook == null) { request.Failed("Could not find template book " + requestData.templateBookUrl); return null; } var pageDictionary = templateBook.GetTemplatePagesIdDictionary(); IPage page = null; if(pageDictionary.TryGetValue(requestData.pageId, out page)) { return page; } else { request.Failed("Could not find the page " + requestData.pageId + " in the template book " + requestData.templateBookUrl); return null; } }
/// <summary> /// Delete a file (typically a recording, as requested by the Clear button in the talking book tool) /// </summary> /// <param name="fileUrl"></param> private void HandleDeleteSegment(ApiRequest request) { var path = GetPathToSegment(request.RequiredParam("id")); if(!RobustFile.Exists(path)) { request.Succeeded(); } else { try { RobustFile.Delete(path); request.Succeeded(); } catch(IOException e) { var msg = string.Format( LocalizationManager.GetString("Errors.ProblemDeletingFile", "Bloom had a problem deleting this file: {0}"), path); ErrorReport.NotifyUserOfProblem(e, msg + Environment.NewLine + e.Message); } } }
/// <summary> /// Get a json of stats about the image. It is used to populate a tooltip when you hover over an image container /// </summary> private void HandleImageInfo(ApiRequest request) { try { var fileName = request.RequiredParam("image"); Guard.AgainstNull(_bookSelection.CurrentSelection, "CurrentBook"); var path = Path.Combine(_bookSelection.CurrentSelection.FolderPath, fileName); if (!File.Exists(path)) { // We can be fed doubly-encoded filenames. So try to decode a second time and see if that works. // See https://silbloom.myjetbrains.com/youtrack/issue/BL-3749. fileName = System.Web.HttpUtility.UrlDecode(fileName); path = Path.Combine(_bookSelection.CurrentSelection.FolderPath, fileName); } RequireThat.File(path).Exists(); var fileInfo = new FileInfo(path); dynamic result = new ExpandoObject(); result.name = fileName; result.bytes = fileInfo.Length; // Using a stream this way, according to one source, // http://stackoverflow.com/questions/552467/how-do-i-reliably-get-an-image-dimensions-in-net-without-loading-the-image, // supposedly avoids loading the image into memory when we only want its dimensions using (var stream = File.OpenRead(path)) using (var img = Image.FromStream(stream, false, false)) { result.width = img.Width; result.height = img.Height; switch (img.PixelFormat) { case PixelFormat.Format32bppArgb: case PixelFormat.Format32bppRgb: case PixelFormat.Format32bppPArgb: result.bitDepth = "32"; break; case PixelFormat.Format24bppRgb: result.bitDepth = "24"; break; case PixelFormat.Format16bppArgb1555: case PixelFormat.Format16bppGrayScale: result.bitDepth = "16"; break; case PixelFormat.Format8bppIndexed: result.bitDepth = "8"; break; case PixelFormat.Format1bppIndexed: result.bitDepth = "1"; break; default: result.bitDepth = "unknown"; break; } } request.ReplyWithJson((object)result); } catch (Exception e) { Logger.WriteEvent("Error in server imageInfo/: url was " + request.LocalPath()); Logger.WriteEvent("Error in server imageInfo/: exception is " + e.Message); request.Failed(e.Message); } }
/// <summary> /// Handles a url starting with api/kPrefix by stripping off that prefix, searching for the file /// named in the remainder of the url, and opening it in some browser (passing on any anchor specified). /// </summary> public static void HandleRequest(ApiRequest request) { //NB: be careful not to lose case, as at least chrome is case-sensitive with anchors (e.g. #ChoiceOfTopic) var localPath = Regex.Replace(request.LocalPath(), "api/"+ kPrefix+"/", "", RegexOptions.IgnoreCase); var langCode = LocalizationManager.UILanguageId; var completeEnglishPath = FileLocator.GetFileDistributedWithApplication(localPath); var completeUiLangPath = completeEnglishPath.Replace("-en.htm", "-" + langCode + ".htm"); string url; if (langCode != "en" && RobustFile.Exists(completeUiLangPath)) url = completeUiLangPath; else url = completeEnglishPath; var cleanUrl = url.Replace("\\", "/"); // allows jump to file to work //we would like to get something like foo.htm#secondPart but the browser strips off that fragment part //so we require that to be written as foo.htm?fragment=secondPart so it gets to us, then we convert //it back to the normal format before sending it to a parser if (!string.IsNullOrEmpty(request.Parameters["fragment"])) { cleanUrl += "#" + request.Parameters["fragment"]; request.Parameters.Remove("fragment"); } string browser = string.Empty; if (SIL.PlatformUtilities.Platform.IsLinux) { // REVIEW: This opens HTML files in the browser. Do we have any non-html // files that this code needs to open in the browser? Currently they get // opened in whatever application the user has selected for that file type // which might well be an editor. browser = "xdg-open"; } else { // If we don't provide the path of the browser, i.e. Process.Start(url + queryPart), we get file not found exception. // If we prepend "file:///", the anchor part of the link (#xxx) is not sent unless we provide the browser path too. // This is the same behavior when simply typing a url into the Run command on Windows. // If we fail to get the browser path for some reason, we still load the page, just without navigating to the anchor. string defaultBrowserPath; if (TryGetDefaultBrowserPathWindowsOnly(out defaultBrowserPath)) { browser = defaultBrowserPath; } } //Note, we don't currently use this, since this is only used for our own html. I added it for completeness... maybe //someday when we are running the BloomLibrary locally for the user, we'll have links that require a query part. var queryPart = ""; if (request.Parameters.Count > 0) { //reconstruct the query part, this time minus any fragment parameter (which we removed previously, if it was there) queryPart = "?" + request.Parameters.AllKeys.Aggregate("", (total, key) => total + key + "=" + request.Parameters.Get(key) + "&"); queryPart = queryPart.TrimEnd(new[] { '&' }); } if (!string.IsNullOrEmpty(browser)) { try { Process.Start(browser, "\"file:///" + cleanUrl + queryPart + "\""); request.SucceededDoNotNavigate(); return; } catch (Exception) { Debug.Fail("Jumping to browser with anchor failed."); // Don't crash Bloom because we can't open an external file. } } // If the above failed, either for lack of default browser or exception, try this: Process.Start("\"" + cleanUrl + "\""); request.SucceededDoNotNavigate(); }
private static void HandleSettings(ApiRequest request) { if(request.HttpMethod != HttpMethods.Get) throw new ApplicationException(request.LocalPath()+" only implements 'get'"); var settings = new Dictionary<string, object> { {"current", request.CurrentBook.BookInfo.CurrentTool} }; foreach (var tool in GetToolsToDisplay(request.CurrentBook, IdsOfToolsThisVersionKnowsAbout)) { if (!String.IsNullOrEmpty(tool.State)) settings.Add(tool.StateName, tool.State); } request.ReplyWithJson(settings); }
private void HandleEndRecord(ApiRequest request) { #if __MonoCS__ #else if (Recorder.RecordingState != RecordingState.Recording) { //usually, this is a result of us getting the "end" before we actually started, because it was too quick if(TestForTooShortAndSendFailIfSo(request)) { _startRecordingTimer.Enabled = false;//we don't want it firing in a few milliseconds from now return; } //but this would handle it if there was some other reason request.Failed("Got endRecording, but was not recording"); return; } try { Debug.WriteLine("Stop recording"); Recorder.Stopped += Recorder_Stopped; //note, this doesn't actually stop... more like... starts the stopping. It does mark the time //we requested to stop. A few seconds later (2, looking at the library code today), it will //actually close the file and raise the Stopped event Recorder.Stop(); request.Succeeded(); //ReportSuccessfulRecordingAnalytics(); } catch (Exception) { //swallow it. One reason (based on HearThis comment) is that they didn't hold it down long enough, we detect this below. } TestForTooShortAndSendFailIfSo(request); #endif }
private bool TestForTooShortAndSendFailIfSo(ApiRequest request) { if ((DateTime.Now - _startRecording) < TimeSpan.FromSeconds(0.5)) { CleanUpAfterPressTooShort(); var msg = LocalizationManager.GetString("EditTab.Toolbox.TalkingBook.PleaseHoldMessage", "Please hold the button down until you have finished recording", "Appears when the speak/record button is pressed very briefly"); request.Failed(msg); return true; } return false; }
// Does this page have any audio at all? Used to enable 'Listen to the whole page'. private void HandleEnableListenButton(ApiRequest request) { var ids = request.RequiredParam("ids"); foreach (var id in ids.Split(',')) { if (RobustFile.Exists(GetPathToSegment(id))) { request.Succeeded(); return; } } request.Failed("no audio"); }
/// <summary> /// Returns a json string like {"devices":["microphone", "Logitech Headset"], "productName":"Logitech Headset", "genericName":"Headset"}, /// except that in practice currrently the generic and product names are the same and not as helpful as the above. /// Devices is a list of product names (of available recording devices), the productName and genericName refer to the /// current selection (or will be null, if no current device). /// </summary> public void HandleAudioDevices(ApiRequest request) { #if __MonoCS__ request.Failed("Not supported on Linux"); #else var sb = new StringBuilder("{\"devices\":["); sb.Append(string.Join(",", RecordingDevice.Devices.Select(d => "\""+d.ProductName+"\""))); sb.Append("],\"productName\":"); if (CurrentRecording.RecordingDevice != null) sb.Append("\"" + CurrentRecording.RecordingDevice.ProductName + "\""); else sb.Append("null"); sb.Append(",\"genericName\":"); if (CurrentRecording.RecordingDevice != null) sb.Append("\"" + CurrentRecording.RecordingDevice.GenericName + "\""); else sb.Append("null"); sb.Append("}"); request.ReplyWithJson(sb.ToString()); #endif }
private void HandleCheckForSegment(ApiRequest request) { var path = GetPathToSegment(request.RequiredParam("id")); request.ReplyWithText(RobustFile.Exists(path) ? "exists" : "not found"); }
/// <returns>true if the recording started successfully</returns> public void HandleStartRecording(ApiRequest request) { #if __MonoCS__ MessageBox.Show("Recording does not yet work on Linux", "Cannot record"); return; #else if(Recording) { request.Failed("Already recording"); return; } string segmentId = request.RequiredParam("id"); PathToCurrentAudioSegment = GetPathToSegment(segmentId); PathToTemporaryWav = Path.GetTempFileName(); if (Recorder.RecordingState == RecordingState.RequestedStop) { request.Failed(LocalizationManager.GetString("EditTab.Toolbox.TalkingBook.BadState", "Bloom recording is in an unusual state, possibly caused by unplugging a microphone. You will need to restart.","This is very low priority for translation.")); } // If someone unplugged the microphone we were planning to use switch to another. // This also triggers selecting the first one initially. if (!RecordingDevice.Devices.Contains(RecordingDevice)) { RecordingDevice = RecordingDevice.Devices.FirstOrDefault(); } if (RecordingDevice == null) { ReportNoMicrophone(); request.Failed("No Microphone"); return ; } if(Recording) { request.Failed( "Already Recording"); return; } if (RobustFile.Exists(PathToCurrentAudioSegment)) { //Try to deal with _backPath getting locked (BL-3160) try { RobustFile.Delete(_backupPath); } catch(IOException) { _backupPath = System.IO.Path.GetTempFileName(); } try { RobustFile.Copy(PathToCurrentAudioSegment, _backupPath, true); } catch (Exception err) { ErrorReport.NotifyUserOfProblem(err, "Bloom cold not copy "+PathToCurrentAudioSegment+" to "+_backupPath+" If things remains stuck, you may need to restart your computer."); request.Failed( "Problem with backup file"); return; } try { RobustFile.Delete(PathToCurrentAudioSegment); //DesktopAnalytics.Analytics.Track("Re-recorded a clip", ContextForAnalytics); } catch (Exception err) { ErrorReport.NotifyUserOfProblem(err, "The old copy of the recording at " + PathToCurrentAudioSegment + " is locked up, so Bloom can't record over it at the moment. If it remains stuck, you may need to restart your computer."); request.Failed( "Audio file locked"); return; } } else { RobustFile.Delete(_backupPath); //DesktopAnalytics.Analytics.Track("Recording clip", ContextForAnalytics); } _startRecording = DateTime.Now; _startRecordingTimer.Start(); request.ReplyWithText("starting record soon"); return; #endif }
public void HandleCurrentRecordingDevice(ApiRequest request) { #if __MonoCS__ #else if(request.HttpMethod == HttpMethods.Post) { var name = request.RequiredPostString(); foreach (var dev in RecordingDevice.Devices) { if(dev.ProductName == name) { RecordingDevice = dev; request.Succeeded(); return; } } request.Failed("Could not find the device named " + name); } else request.Failed("Only Post is currently supported"); #endif }