Example #1
0
        public void ClipboardRoundTripWorks_Bmp()
        {
            var imagePath = GetPathToImage("PasteHS.bmp");

            using (var image = PalasoImage.FromFileRobustly(imagePath))
            {
                PortableClipboard.CopyImageToClipboard(image);
                using (var resultingImage = PortableClipboard.GetImageFromClipboard())
                {
                    // There is no working PalasoImage.Equals(), so just try a few properties
                    Assert.AreEqual(image.FileName, resultingImage.FileName);
                    Assert.AreEqual(image.Image.Size, resultingImage.Image.Size);
                    Assert.AreEqual(image.Image.Flags, resultingImage.Image.Flags);
                }
            }
        }
Example #2
0
        public void ClipboardRoundTripWorks_GetsExistingMetadata()
        {
            var imagePath = GetPathToImage("AOR_EAG00864.png");

            using (var image = PalasoImage.FromFileRobustly(imagePath))
            {
                var preCopyLicense       = image.Metadata.License.Token;
                var preCopyCollectionUri = image.Metadata.CollectionUri;
                PortableClipboard.CopyImageToClipboard(image);
                using (var resultingImage = PortableClipboard.GetImageFromClipboard())
                {
                    // Test that the same metadata came through
                    Assert.IsTrue(resultingImage.Metadata.IsMinimallyComplete);
                    Assert.AreEqual(preCopyLicense, resultingImage.Metadata.License.Token);
                    Assert.AreEqual(preCopyCollectionUri, resultingImage.Metadata.CollectionUri);
                    Assert.AreEqual(image.Image.Flags, resultingImage.Image.Flags);
                }
            }
        }
Example #3
0
        // Answer true if the current clipboard contents are something that makes sense to paste into the href
        // of a hyperlink in a Bloom Book. Currently we allow all http(s) and mailto links, plus internal links
        // (starting with #) provided they are to a non-xmatter page that is present in the book.
        private void HandleIsClipboardBookHyperlink(ApiRequest request)
        {
            string clipContent = "";             // initial value is not used, delegate will set it.

            Program.MainContext.Send(o =>
            {
                try
                {
                    clipContent = PortableClipboard.GetText();
                }
                catch (Exception e)
                {
                    // Need to make sure to handle exceptions.
                    // If the worker thread dies with an unhandled exception,
                    // it causes the whole program to immediately crash without opportunity for error reporting
                    Bloom.Utils.MiscUtils.SuppressUnusedExceptionVarWarning(e);

                    request.ReplyWithBoolean(false);
                }
            }, null);

            request.ReplyWithBoolean(IsBloomHyperlink(clipContent, request.CurrentBook));
        }
        public void RegisterWithApiHandler(BloomApiHandler apiHandler)
        {
            // This is just for storing the user preference of method
            // If we had a couple of these, we could just have a generic preferences api
            // that browser-side code could use.
            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "method", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    var method = Settings.Default.PublishAndroidMethod;
                    if (!new string[] { "wifi", "usb", "file" }.Contains(method))
                    {
                        method = "wifi";
                    }
                    request.ReplyWithText(method);
                }
                else                 // post
                {
                    Settings.Default.PublishAndroidMethod = request.RequiredPostString();
#if __MonoCS__
                    if (Settings.Default.PublishAndroidMethod == "usb")
                    {
                        _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet.");
                    }
#endif
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "backColor", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    if (request.CurrentBook != _coverColorSourceBook)
                    {
                        _coverColorSourceBook = request.CurrentBook;
                        ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor);
                    }
                    request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor));
                }
                else                 // post
                {
                    // ignore invalid colors (very common while user is editing hex)
                    Color newColor;
                    var newColorAsString = request.RequiredPostString();
                    if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor))
                    {
                        _thumbnailBackgroundColor = newColor;
                        request.CurrentBook.SetCoverColor(newColorAsString);
                    }
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode",
                                                      readRequest =>
            {
                // If the user has taken off all possible motion, force not having motion in the
                // Bloom Reader book.  See https://issues.bloomlibrary.org/youtrack/issue/BL-7680.
                if (!readRequest.CurrentBook.HasMotionPages)
                {
                    readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = false;
                }
                return(readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion);
            },
                                                      (writeRequest, value) =>
            {
                writeRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = value;
                writeRequest.CurrentBook.BookInfo.SavePublishSettings();
                _webSocketServer.SendEvent("publish", "motionChanged");
            }
                                                      , true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request =>
            {
                MakeBloompubPreview(request, false);
            }, false);


            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "thumbnail", request =>
            {
                var coverImage = request.CurrentBook.GetCoverImagePath();
                if (coverImage == null)
                {
                    request.Failed("no cover image");
                }
                else
                {
                    // We don't care as much about making it resized as making its background transparent.
                    using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile())
                    {
                        if (_thumbnailBackgroundColor == Color.Transparent)
                        {
                            ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor);
                        }
                        RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor);
                        request.ReplyWithImage(thumbnail.Path);
                    }
                }
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request =>
            {
#if !__MonoCS__
                SetState("UsbStarted");
                UpdatePreviewIfNeeded(request);
                _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings());
#endif
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request =>
            {
#if !__MonoCS__
                _usbPublisher.Stop(disposing: false);
                SetState("stopped");
#endif
                request.PostSucceeded();
            }, true);
            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/start", request =>
            {
                SetState("ServingOnWifi");
                UpdatePreviewIfNeeded(request);
                _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings());

                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/stop", request =>
            {
                _wifiPublisher.Stop();
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/save", request =>
            {
                UpdatePreviewIfNeeded(request);
                FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings());
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubsParams", request =>
            {
                request.ReplyWithJson(JsonConvert.SerializeObject(_collectionSettings.BulkPublishBloomPubSettings));
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubs", request =>
            {
                // update what's in the collection so that we remember for next time
                _collectionSettings.BulkPublishBloomPubSettings = request.RequiredPostObject <BulkBloomPubPublishSettings>();
                _collectionSettings.Save();

                _bulkBloomPubCreator.PublishAllBooks(_collectionSettings.BulkPublishBloomPubSettings);
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "textToClipboard", request =>
            {
                PortableClipboard.SetText(request.RequiredPostString());
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode",
                                                      request =>
            {
                return(request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate",
                                                      request =>
            {
                return(request.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion && request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape",
                                                      request =>
            {
                return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default
            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "languagesInBook", request =>
            {
                try
                {
                    InitializeLanguagesInBook(request);

                    Dictionary <string, InclusionSetting> textLangsToPublish  = request.CurrentBook.BookInfo.PublishSettings.BloomPub.TextLangs;
                    Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.PublishSettings.BloomPub.AudioLangs;

                    var result = "[" + string.Join(",", _allLanguages.Select(kvp =>
                    {
                        string langCode = kvp.Key;

                        bool includeText = false;
                        if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting))
                        {
                            includeText = includeTextSetting.IsIncluded();
                        }

                        bool includeAudio = false;
                        if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting))
                        {
                            includeAudio = includeAudioSetting.IsIncluded();
                        }

                        var value = new LanguagePublishInfo()
                        {
                            code             = kvp.Key,
                            name             = request.CurrentBook.PrettyPrintLanguage(langCode),
                            complete         = kvp.Value,
                            includeText      = includeText,
                            containsAnyAudio = _languagesWithAudio.Contains(langCode),
                            includeAudio     = includeAudio
                        };
                        var json = JsonConvert.SerializeObject(value);
                        return(json);
                    })) + "]";

                    request.ReplyWithText(result);
                }
Example #5
0
        public void RegisterWithApiHandler(BloomApiHandler apiHandler)
        {
            apiHandler.RegisterEndpointLegacy("uiLanguages", HandleUiLanguages, false);                               // App
            apiHandler.RegisterEndpointLegacy("currentUiLanguage", HandleCurrentUiLanguage, false);                   // App
            apiHandler.RegisterEndpointLegacy("bubbleLanguages", HandleBubbleLanguages, false);                       // Move to EditingViewApi
            apiHandler.RegisterEndpointLegacy("authorMode", HandleAuthorMode, false);                                 // Move to EditingViewApi
            apiHandler.RegisterEndpointLegacy("topics", HandleTopics, false);                                         // Move to EditingViewApi
            apiHandler.RegisterEndpointLegacy("common/error", HandleJavascriptError, false);                          // Common
            apiHandler.RegisterEndpointLegacy("common/preliminaryError", HandlePreliminaryJavascriptError, false);    // Common
            apiHandler.RegisterEndpointLegacy("common/saveChangesAndRethinkPageEvent", RethinkPageAndReloadIt, true); // Move to EditingViewApi
            apiHandler.RegisterEndpointLegacy("common/chooseFolder", HandleChooseFolder, true);
            apiHandler.RegisterEndpointLegacy("common/showInFolder", HandleShowInFolderRequest, true);                // Common
            apiHandler.RegisterEndpointLegacy("common/canModifyCurrentBook", HandleCanModifyCurrentBook, true);
            apiHandler.RegisterEndpointLegacy("common/showSettingsDialog", HandleShowSettingsDialog, false);          // Common
            apiHandler.RegisterEndpointLegacy("common/problemWithBookMessage", request =>
            {
                request.ReplyWithText(CommonMessages.GetProblemWithBookMessage(Path.GetFileName(_bookSelection.CurrentSelection?.FolderPath)));
            }, false);
            apiHandler.RegisterEndpointLegacy("common/clickHereForHelp", request =>
            {
                var problemFilePath = UrlPathString.CreateFromUrlEncodedString(request.RequiredParam("problem")).NotEncoded;
                request.ReplyWithText(CommonMessages.GetPleaseClickHereForHelpMessage(problemFilePath));
            }, false);
            // Used when something in JS land wants to copy text to or from the clipboard. For POST, the text to be put on the
            // clipboard is passed as the 'text' property of a JSON requestData.
            // Somehow the get version of this fires while initializing a page (probably hooking up CkEditor, an unwanted
            // invocation of the code that decides whether to enable the paste hyperlink button). This causes a deadlock
            // unless we make this endpoint requiresSync:false. I think this is safe as it doesn't interact with any other
            // Bloom objects.
            apiHandler.RegisterEndpointLegacy("common/clipboardText",
                                              request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    string result = "";                             // initial value is not used, delegate will set it.
                    Program.MainContext.Send(o =>
                    {
                        try
                        {
                            result = PortableClipboard.GetText();
                        }
                        catch (Exception e)
                        {
                            // Need to make sure to handle exceptions.
                            // If the worker thread dies with an unhandled exception,
                            // it causes the whole program to immediately crash without opportunity for error reporting
                            NonFatalProblem.Report(ModalIf.All, PassiveIf.None, "Error pasting text", exception: e);
                        }
                    }, null);
                    request.ReplyWithText(result);
                }
                else
                {
                    // post
                    var requestData = DynamicJson.Parse(request.RequiredPostJson());
                    string content  = requestData.text;
                    if (!string.IsNullOrEmpty(content))
                    {
                        Program.MainContext.Post(o =>
                        {
                            try
                            {
                                PortableClipboard.SetText(content);
                            }
                            catch (Exception e)
                            {
                                // Need to make sure to handle exceptions.
                                // If the worker thread dies with an unhandled exception,
                                // it causes the whole program to immediately crash without opportunity for error reporting
                                NonFatalProblem.Report(ModalIf.All, PassiveIf.None, "Error copying text", exception: e);
                            }
                        }, null);
                    }
                    request.PostSucceeded();
                }
            }, false, false);
            apiHandler.RegisterEndpointLegacy("common/checkForUpdates",
                                              request =>
            {
                WorkspaceView.CheckForUpdates();
                request.PostSucceeded();
            }, false);
            apiHandler.RegisterEndpointLegacy("common/channel",
                                              request =>
            {
                request.ReplyWithText(ApplicationUpdateSupport.ChannelName);
            }, false);
            // This is useful for debugging TypeScript code, especially on Linux.  I wouldn't necessarily expect
            // to see it used anywhere in code that gets submitted and merged.
            apiHandler.RegisterEndpointLegacy("common/debugMessage",
                                              request =>
            {
                var message = request.RequiredPostString();
                Debug.WriteLine("FROM JS: " + message);
                request.PostSucceeded();
            }, false);

            apiHandler.RegisterEndpointLegacy("common/loginData",
                                              request =>
            {
                var requestData = DynamicJson.Parse(request.RequiredPostJson());
                string token    = requestData.sessionToken;
                string email    = requestData.email;
                string userId   = requestData.userId;
                //Debug.WriteLine("Got login data " + email + " with token " + token + " and id " + userId);
                _parseClient.SetLoginData(email, userId, token, BookUpload.Destination);
                _doWhenLoggedIn?.Invoke();
                request.PostSucceeded();
            }, false);

            // At this point we open dialogs from c# code; if we opened dialogs from javascript, we wouldn't need this
            // api to do it. We just need a way to close a c#-opened dialog from javascript (e.g. the Close button of the dialog).
            //
            // This must set requiresSync:false because the API call which opened the dialog may already have
            // the lock in which case we would be deadlocked.
            // ErrorReport.NotifyUserOfProblem is a particularly problematic case. We tried to come up with some
            // other solutions for that including opening the dialog on Application.Idle. But the dialog needs
            // to give a real-time result so callers can know what do with button presses. Since some of those
            // callers are in libpalaso, we can't just ignore the result and handle the actions ourselves.
            apiHandler.RegisterEndpointLegacy("common/closeReactDialog", request =>
            {
                ReactDialog.CloseCurrentModal(request.GetPostStringOrNull());
                request.PostSucceeded();
            }, true, requiresSync: false);

            // TODO: move to the new App API (BL-9635)
            apiHandler.RegisterEndpointLegacy("common/reloadCollection", HandleReloadCollection, true);
        }
Example #6
0
        public void HandleRequest(ApiRequest request)
        {
            if (CurrentBook == null)
            {
                Debug.Fail("BL-836 reproduction?");
                // ReSharper disable once HeuristicUnreachableCode
                request.Failed("CurrentBook is null");
                return;
            }
            if (request.CurrentCollectionSettings == null)
            {
                Debug.Fail("BL-836 reproduction?");
                // ReSharper disable once HeuristicUnreachableCode
                request.Failed("CurrentBook.CollectionSettings is null");
                return;
            }

            var lastSegment = request.LocalPath().Split(new char[] { '/' }).Last();

            switch (lastSegment)
            {
            case "test":
                request.PostSucceeded();
                break;

            case "readerSettingsEditForbidden":
                request.ReplyWithText(_tcManager.OkToEditCollectionSettings
                                                ? ""
                                                : WorkspaceView.MustBeAdminMessage);
                break;

            case "readerToolSettings":
                if (request.HttpMethod == HttpMethods.Get)
                {
                    request.ReplyWithJson(GetReaderSettings(request.CurrentBook.BookData));
                }
                else
                {
                    var path    = DecodableReaderToolSettings.GetReaderToolsSettingsFilePath(request.CurrentCollectionSettings);
                    var content = request.RequiredPostJson();
                    RobustFile.WriteAllText(path, content, Encoding.UTF8);
                    request.PostSucceeded();
                }
                break;


            //note, this endpoint is confusing because it appears that ultimately we only use the word list out of this file (see "sampleTextsList").
            //This ends up being written to a ReaderToolsWords-xyz.json (matching its use, if not it contents).
            case "synphonyLanguageData":
                //This is the "post". There is no direct "get", but the name of the file is given in the "sampleTextList" reply, below.
                // We've had situations (BL-4313 and friends) where reading the posted data fails. This seems to be due to situations
                // where we have a very large block of data and are rapidly switching between books. But as far as I can tell, the only
                // case where it's at all important to capture the new language data is if the user has been changing settings and
                // in particular editing the word list. Timing out the save in that situation seems very unlikely to fail.
                // So, in the interests of preventing the crash when switching books fast, we will ignore failure to read all the
                // json, and just not update the file. We would in any case keep only the version of the data sent to us by
                // the last book which sends it, and that one is unlikely to get interrupted.
                string langdata;
                try
                {
                    langdata = request.RequiredPostJson();
                }
                catch (IOException e)
                {
                    SIL.Reporting.Logger.WriteError("Saving synphonyLanguageData failed to get Json", e);
                    break;
                }

                SaveSynphonyLanguageData(langdata);
                request.PostSucceeded();
                break;

            case "sampleTextsList":
                //note, as part of this reply, we send the path of the "ReaderToolsWords-xyz.json" which is *written* by the "synphonyLanguageData" endpoint above
                request.ReplyWithText(GetSampleTextsList(request.CurrentCollectionSettings.SettingsFilePath));
                break;

            case "sampleFileContents":
                request.ReplyWithText(GetTextFileContents(request.RequiredParam("fileName"), WordFileType.SampleFile));
                break;

            case "textOfContentPages":
                request.ReplyWithText(GetTextOfContentPagesAsJson());
                break;

            case "makeLetterAndWordList":
                MakeLetterAndWordList(request.RequiredPostString("settings"), request.RequiredPostString("allWords"));
                request.PostSucceeded();
                break;

            case "openTextsFolder":
                OpenTextsFolder();
                request.PostSucceeded();
                break;

            case "chooseAllowedWordsListFile":
                lock (request)
                {
                    request.ReplyWithText(ShowSelectAllowedWordsFileDialog());
                }
                break;

            case "allowedWordsList":
                switch (request.HttpMethod)
                {
                case HttpMethods.Delete:
                    RecycleAllowedWordListFile(request.RequiredParam("fileName"));
                    request.PostSucceeded();
                    break;

                case HttpMethods.Get:
                    var fileName = request.RequiredParam("fileName");
                    request.ReplyWithText(RemoveEmptyAndDupes(GetTextFileContents(fileName, WordFileType.AllowedWordsFile)));
                    break;

                default:
                    request.Failed("Http verb not handled");
                    break;
                }
                break;

            case "defaultLevel":
                if (request.HttpMethod == HttpMethods.Get)
                {
                    request.ReplyWithText(Settings.Default.CurrentLevel.ToString());
                }
                else
                {
                    int level;
                    if (int.TryParse(request.RequiredParam("level"), out level))
                    {
                        Settings.Default.CurrentLevel = level;
                        Settings.Default.Save();
                    }
                    else
                    {
                        // Don't think any sort of runtime failure is worthwhile here.
                        Debug.Fail("could not parse level number");
                    }
                    request.PostSucceeded();                             // technically it didn't if we didn't parse the number
                }
                break;

            case "defaultStage":
                if (request.HttpMethod == HttpMethods.Get)
                {
                    request.ReplyWithText(Settings.Default.CurrentStage.ToString());
                }
                else
                {
                    int stage;
                    if (int.TryParse(request.RequiredParam("stage"), out stage))
                    {
                        Settings.Default.CurrentStage = stage;
                        Settings.Default.Save();
                    }
                    else
                    {
                        // Don't think any sort of runtime failure is worthwhile here.
                        Debug.Fail("could not parse stage number");
                    }
                    request.PostSucceeded();                             // technically it didn't if we didn't parse the number
                }
                break;

            case "copyBookStatsToClipboard":
                // See https://issues.bloomlibrary.org/youtrack/issue/BL-10018.
                string bookStatsString;
                try
                {
                    bookStatsString = request.RequiredPostJson();
                    dynamic bookStats  = DynamicJson.Parse(bookStatsString);
                    var     headerBldr = new StringBuilder();
                    var     dataBldr   = new StringBuilder();
                    headerBldr.Append("Book Title");
                    var title = _bookSelection.CurrentSelection.Title;
                    title = title.Replace("\"", "\"\"");                                // Double double quotes to get Excel to recognize them.
                    dataBldr.AppendFormat("\"{0}\"", title);
                    headerBldr.Append("\tLevel");
                    dataBldr.AppendFormat("\t\"Level {0}\"", bookStats["levelNumber"]);
                    headerBldr.Append("\tNumber of Pages with Text");
                    dataBldr.AppendFormat("\t{0}", bookStats["pageCount"]);
                    headerBldr.Append("\tTotal Number of Words");
                    dataBldr.AppendFormat("\t{0}", bookStats["actualWordCount"]);
                    headerBldr.Append("\tTotal Number of Sentences");
                    dataBldr.AppendFormat("\t{0}", bookStats["actualSentenceCount"]);
                    headerBldr.Append("\tAverage No of Words per Page with Text");
                    dataBldr.AppendFormat("\t{0:0.#}", bookStats["actualAverageWordsPerPage"]);
                    headerBldr.Append("\tAverage No of Sentences per Page with Text");
                    dataBldr.AppendFormat("\t{0:0.#}", bookStats["actualAverageSentencesPerPage"]);
                    headerBldr.Append("\tNumber of Unique Words");
                    dataBldr.AppendFormat("\t{0}", bookStats["actualUniqueWords"]);
                    headerBldr.Append("\tAverage Word Length");
                    dataBldr.AppendFormat("\t{0:0.#}", bookStats["actualAverageGlyphsPerWord"]);
                    headerBldr.Append("\tAverage Sentence Length");
                    dataBldr.AppendFormat("\t{0:0.#}", bookStats["actualAverageWordsPerSentence"]);
                    headerBldr.Append("\tMaximum Word Length");
                    dataBldr.AppendFormat("\t{0}", bookStats["actualMaxGlyphsPerWord"]);
                    headerBldr.Append("\tMaximum Sentence Length");
                    dataBldr.AppendFormat("\t{0}", bookStats["actualMaxWordsPerSentence"]);
                    // "actualWordsPerPageBook" is the maximum number of words on a page in the book
                    // It's in the json data, but not asked for in the clipboard copying.
                    var stringToSave = headerBldr.ToString() + Environment.NewLine + dataBldr.ToString();
                    PortableClipboard.SetText(stringToSave);
                }
                catch (IOException e)
                {
                    SIL.Reporting.Logger.WriteError("Copying book statistics to clipboard failed to get Json", e);
                    break;
                }
                request.PostSucceeded();
                break;

            default:
                request.Failed("Don't understand '" + lastSegment + "' in " + request.LocalPath());
                break;
            }
        }
        public void RegisterWithApiHandler(BloomApiHandler apiHandler)
        {
            // This is just for storing the user preference of method
            // If we had a couple of these, we could just have a generic preferences api
            // that browser-side code could use.
            apiHandler.RegisterEndpointHandler(kApiUrlPart + "method", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    var method = Settings.Default.PublishAndroidMethod;
                    if (!new string[] { "wifi", "usb", "file" }.Contains(method))
                    {
                        method = "wifi";
                    }
                    request.ReplyWithText(method);
                }
                else                 // post
                {
                    Settings.Default.PublishAndroidMethod = request.RequiredPostString();
#if __MonoCS__
                    if (Settings.Default.PublishAndroidMethod == "usb")
                    {
                        _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet.");
                    }
#endif
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "backColor", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    if (request.CurrentBook != _coverColorSourceBook)
                    {
                        _coverColorSourceBook = request.CurrentBook;
                        ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor);
                    }
                    request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor));
                }
                else                 // post
                {
                    // ignore invalid colors (very common while user is editing hex)
                    Color newColor;
                    var newColorAsString = request.RequiredPostString();
                    if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor))
                    {
                        _thumbnailBackgroundColor = newColor;
                        request.CurrentBook.SetCoverColor(newColorAsString);
                    }
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode",
                                                      readRequest =>
            {
                // If the user has taken off all possible motion, force not having motion in the
                // Bloom Reader book.  See https://issues.bloomlibrary.org/youtrack/issue/BL-7680.
                if (!readRequest.CurrentBook.HasMotionPages)
                {
                    readRequest.CurrentBook.MotionMode = false;
                }
                return(readRequest.CurrentBook.MotionMode);
            },
                                                      (writeRequest, value) =>
            {
                writeRequest.CurrentBook.MotionMode = value;
            }
                                                      , true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request =>
            {
                if (request.HttpMethod == HttpMethods.Post)
                {
                    // This is already running on a server thread, so there doesn't seem to be any need to kick off
                    // another background one and return before the preview is ready. But in case something in C#
                    // might one day kick of a new preview, or we find we do need a background thread,
                    // I've made it a websocket broadcast when it is ready.
                    // If we've already left the publish tab...we can get a few of these requests queued up when
                    // a tester rapidly toggles between views...abandon the attempt
                    if (!PublishHelper.InPublishTab)
                    {
                        request.Failed("aborted, no longer in publish tab");
                        return;
                    }
                    try
                    {
                        UpdatePreview(request);
                        request.PostSucceeded();
                    }
                    catch (Exception e)
                    {
                        request.Failed("Error while updating preview. Message: " + e.Message);
                        NonFatalProblem.Report(ModalIf.Alpha, PassiveIf.All, "Error while updating preview.", null, e, true);
                    }
                }
            }, false);


            apiHandler.RegisterEndpointHandler(kApiUrlPart + "thumbnail", request =>
            {
                var coverImage = request.CurrentBook.GetCoverImagePath();
                if (coverImage == null)
                {
                    request.Failed("no cover image");
                }
                else
                {
                    // We don't care as much about making it resized as making its background transparent.
                    using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile())
                    {
                        if (_thumbnailBackgroundColor == Color.Transparent)
                        {
                            ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor);
                        }
                        RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor);
                        request.ReplyWithImage(thumbnail.Path);
                    }
                }
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request =>
            {
#if !__MonoCS__
                SetState("UsbStarted");
                UpdatePreviewIfNeeded(request);
                _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings());
#endif
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request =>
            {
#if !__MonoCS__
                _usbPublisher.Stop();
                SetState("stopped");
#endif
                request.PostSucceeded();
            }, true);
            apiHandler.RegisterEndpointHandler(kApiUrlPart + "wifi/start", request =>
            {
                SetState("ServingOnWifi");
                UpdatePreviewIfNeeded(request);
                _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings());

                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "wifi/stop", request =>
            {
                _wifiPublisher.Stop();
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "file/save", request =>
            {
                UpdatePreviewIfNeeded(request);
                FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings());
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "cleanup", request =>
            {
                Stop();
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "textToClipboard", request =>
            {
                PortableClipboard.SetText(request.RequiredPostString());
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode",
                                                      request =>
            {
                return(request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate",
                                                      request =>
            {
                return(request.CurrentBook.MotionMode && request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape",
                                                      request =>
            {
                return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default
            apiHandler.RegisterEndpointHandler(kApiUrlPart + "languagesInBook", request =>
            {
                try
                {
                    InitializeLanguagesInBook(request);

                    Dictionary <string, InclusionSetting> textLangsToPublish  = request.CurrentBook.BookInfo.MetaData.TextLangsToPublish.ForBloomPUB;
                    Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.MetaData.AudioLangsToPublish.ForBloomPUB;

                    var result = "[" + string.Join(",", _allLanguages.Select(kvp =>
                    {
                        string langCode = kvp.Key;

                        bool includeText = false;
                        if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting))
                        {
                            includeText = includeTextSetting.IsIncluded();
                        }

                        bool includeAudio = false;
                        if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting))
                        {
                            includeAudio = includeAudioSetting.IsIncluded();
                        }

                        var value = new LanguagePublishInfo()
                        {
                            code             = kvp.Key,
                            name             = request.CurrentBook.PrettyPrintLanguage(langCode),
                            complete         = kvp.Value,
                            includeText      = includeText,
                            containsAnyAudio = _languagesWithAudio.Contains(langCode),
                            includeAudio     = includeAudio
                        };
                        var json = JsonConvert.SerializeObject(value);
                        return(json);
                    })) + "]";

                    request.ReplyWithText(result);
                }
        public void RegisterWithServer(EnhancedImageServer server)
        {
            // This is just for storing the user preference of method
            // If we had a couple of these, we could just have a generic preferences api
            // that browser-side code could use.
            server.RegisterEndpointHandler(kApiUrlPart + "method", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    var method = Settings.Default.PublishAndroidMethod;
                    if (!new string[] { "wifi", "usb", "file" }.Contains(method))
                    {
                        method = "wifi";
                    }
                    request.ReplyWithText(method);
                }
                else                 // post
                {
                    Settings.Default.PublishAndroidMethod = request.RequiredPostString();
#if __MonoCS__
                    if (Settings.Default.PublishAndroidMethod == "usb")
                    {
                        _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet.");
                    }
#endif
                    request.PostSucceeded();
                }
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "backColor", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    if (request.CurrentBook != _coverColorSourceBook)
                    {
                        _coverColorSourceBook = request.CurrentBook;
                        TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor);
                    }
                    request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor));
                }
                else                 // post
                {
                    // ignore invalid colors (very common while user is editing hex)
                    Color newColor;
                    var newColorAsString = request.RequiredPostString();
                    if (TryCssColorFromString(newColorAsString, out newColor))
                    {
                        _thumbnailBackgroundColor = newColor;
                        request.CurrentBook.SetCoverColor(newColorAsString);
                    }
                    request.PostSucceeded();
                }
            }, true);


            server.RegisterEndpointHandler(kApiUrlPart + "photoStoryMode", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    // this is temporary, just trying to get support for full screen pan & zoom out quickly in 4.2
                    request.ReplyWithText(request.CurrentBook.UsePhotoStoryModeInBloomReader.ToString()
                                          .ToLowerInvariant()); // "false", not "False"
                }
                else                                            // post
                {
                    request.CurrentBook.UsePhotoStoryModeInBloomReader = bool.Parse(request.RequiredPostString());
                    request.PostSucceeded();
                }
            }, true);


            server.RegisterEndpointHandler(kApiUrlPart + "thumbnail", request =>
            {
                var coverImage = request.CurrentBook.GetCoverImagePath();
                if (coverImage == null)
                {
                    request.Failed("no cover image");
                }
                else
                {
                    // We don't care as much about making it resized as making its background transparent.
                    using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile())
                    {
                        if (_thumbnailBackgroundColor == Color.Transparent)
                        {
                            TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor);
                        }
                        RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor);
                        request.ReplyWithImage(thumbnail.Path);
                    }
                }
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "usb/start", request =>
            {
#if !__MonoCS__
                SetState("UsbStarted");
                _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor);
#endif
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request =>
            {
#if !__MonoCS__
                _usbPublisher.Stop();
                SetState("stopped");
#endif
                request.PostSucceeded();
            }, true);
            server.RegisterEndpointHandler(kApiUrlPart + "wifi/start", request =>
            {
                _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor);
                SetState("ServingOnWifi");
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "wifi/stop", request =>
            {
                _wifiPublisher.Stop();
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "file/save", request =>
            {
                FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress);
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "cleanup", request =>
            {
                Stop();
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "textToClipboard", request =>
            {
                PortableClipboard.SetText(request.RequiredPostString());
                request.PostSucceeded();
            }, true);
        }