From 84599105af51cf1daa8120639df9b7b78d9dee8c Mon Sep 17 00:00:00 2001 From: nsantacruz Date: Thu, 20 Jun 2024 13:53:36 +0300 Subject: [PATCH 1/3] fix(topic): encode ref in case it has question marks or other problematic characters for a URI --- static/js/Story.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/Story.jsx b/static/js/Story.jsx index 12bf3df64c..5814879375 100644 --- a/static/js/Story.jsx +++ b/static/js/Story.jsx @@ -177,7 +177,7 @@ const markReviewedPostRequest = (lang, topic, topicLink) => { 'interface_lang': lang === 'en' ? 'english' : 'hebrew', 'description' : {...topicLink.descriptions[lang], 'review_state': 'reviewed'} }; - return Sefaria.postToApi(`/api/ref-topic-links/${topicLink.ref}`, {}, postData); + return Sefaria.postToApi(`/api/ref-topic-links/${encodeURIComponent(topicLink.ref)}`, {}, postData); } const useReviewState = (topic, topicLink) => { From eb396e21dfe2cbe3290f9a32091302105a5b4cff Mon Sep 17 00:00:00 2001 From: nsantacruz Date: Mon, 24 Jun 2024 11:37:39 +0300 Subject: [PATCH 2/3] fix(admin editor): refactor usages of requestWithCallback() to use Sefaria.adminEditorApiRequest. This fixes some issues with encoding refs properly in the URL because now all logic goes through the same function. --- sefaria/model/trend.py | 2 -- static/js/BookPage.jsx | 5 +++-- static/js/CategoryEditor.jsx | 13 +++++++++---- static/js/Misc.jsx | 26 +------------------------ static/js/SourceEditor.jsx | 21 +++++++++++++------- static/js/Story.jsx | 4 ++-- static/js/TopicEditor.jsx | 8 +++++--- static/js/TopicPage.jsx | 4 ++-- static/js/TopicSearch.jsx | 18 ++++++------------ static/js/sefaria/sefaria.js | 37 ++++++++++++++++++++++++++++++------ 10 files changed, 73 insertions(+), 65 deletions(-) diff --git a/sefaria/model/trend.py b/sefaria/model/trend.py index ed8518287a..c2d9b8306d 100644 --- a/sefaria/model/trend.py +++ b/sefaria/model/trend.py @@ -7,8 +7,6 @@ import time from datetime import datetime, date, timedelta -from py import process - from . import abstract as abst from . import user_profile from . import text diff --git a/static/js/BookPage.jsx b/static/js/BookPage.jsx index be769caa79..3730d19961 100644 --- a/static/js/BookPage.jsx +++ b/static/js/BookPage.jsx @@ -11,7 +11,7 @@ import { AdminToolHeader, CategoryChooser, TitleVariants, - CategoryHeader, requestWithCallBack + CategoryHeader } from './Misc'; import {ContentText} from "./ContentText"; import {validateMarkdownLinks} from "./AdminEditor"; @@ -1293,7 +1293,8 @@ const EditTextInfo = function({initTitle, close}) { const deleteObj = () => { setSavingStatus(true); const url = `/api/v2/index/${enTitle}`; - requestWithCallBack({url, type: "DELETE", redirect: () => window.location.href = `/texts`}); + Sefaria.adminEditorApiRequest(url, null, null, "DELETE") + .then(() => window.location.href = '/texts'); } const renderCollectiveTitle = () => { if (!creatingCollectiveTitle) { diff --git a/static/js/CategoryEditor.jsx b/static/js/CategoryEditor.jsx index 61f5b2c80e..f7636786aa 100644 --- a/static/js/CategoryEditor.jsx +++ b/static/js/CategoryEditor.jsx @@ -2,7 +2,7 @@ import {CategoryChooser, InterfaceText, ToggleSet} from "./Misc"; import Sefaria from "./sefaria/sefaria"; import $ from "./sefaria/sefariaJquery"; import {AdminEditor} from "./AdminEditor"; -import {requestWithCallBack, AdminToolHeader} from "./Misc"; +import {AdminToolHeader} from "./Misc"; import React, {useState, useRef} from "react"; const displayOptionForSources = (child) => { @@ -84,7 +84,9 @@ const ReorderEditor = ({close, type="", postURL="", redirect="", origItems = []} else if (type === 'sources') { postCategoryData = {sources: tocItems}; } - requestWithCallBack({url: postURL, data: postCategoryData, setSavingStatus, redirect: () => window.location.href = redirect}) + Sefaria.adminEditorApiRequest(postURL, null, postCategoryData) + .then(() => window.location.href = redirect) + .finally(() => setSavingStatus(false)); } return
@@ -187,7 +189,9 @@ const CategoryEditor = ({origData={}, close, origPath=[]}) => { if (urlParams.length > 0) { url += `?${urlParams.join('&')}`; } - requestWithCallBack({url, data: postCategoryData, setSavingStatus, redirect: () => window.location.href = "/texts/"+fullPath}); + Sefaria.adminEditorApiRequest(url, null, postCategoryData) + .then(() => window.location.href = "/texts/"+fullPath) + .finally(() => setSavingStatus(false)); } @@ -197,7 +201,8 @@ const CategoryEditor = ({origData={}, close, origPath=[]}) => { return; } const url = `/api/category/${origPath.concat(origData.origEn).join("/")}`; - requestWithCallBack({url, type: "DELETE", redirect: () => window.location.href = `/texts`}); + Sefaria.adminEditorApiRequest(url, null, null, "DELETE") + .then(() => window.location.href = `/texts`); } const primaryOptions = [ {name: "true", content: Sefaria._("True"), role: "radio", ariaLabel: Sefaria._("Set Primary Status to True") }, diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index f5645ee130..e0b43c77a0 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -1045,29 +1045,6 @@ class ToggleOption extends Component { } } - //style={this.props.style} - -const requestWithCallBack = ({url, setSavingStatus, redirect, type="POST", data={}, redirect_params}) => { - let ajaxPayload = {url, type}; - if (type === "POST") { - ajaxPayload.data = {json: JSON.stringify(data)}; - } - $.ajax({ - ...ajaxPayload, - success: function(result) { - if ("error" in result) { - if (setSavingStatus) { - setSavingStatus(false); - } - alert(result.error); - } else { - redirect(); - } - } - }).fail(function() { - alert(Sefaria._("Something went wrong. Sorry!")); - }); -} const TopicToCategorySlug = function(topic, category=null) { //helper function for AdminEditor @@ -1676,7 +1653,7 @@ const TopicPictureUploader = ({slug, callback, old_filename, caption}) => { const deleteImage = () => { const old_filename_wout_url = old_filename.split("/").slice(-1); const url = `${Sefaria.apiHost}/api/topics/images/${slug}?old_filename=${old_filename_wout_url}`; - requestWithCallBack({url, type: "DELETE", redirect: () => alert("Deleted image.")}); + Sefaria.adminEditorApiRequest(url, null, null, "DELETE").then(() => alert("Deleted image.")); callback(""); fileInput.current.value = ""; } @@ -3385,7 +3362,6 @@ export { AdminToolHeader, CategoryChooser, TitleVariants, - requestWithCallBack, OnInView, TopicPictureUploader, ImageWithCaption diff --git a/static/js/SourceEditor.jsx b/static/js/SourceEditor.jsx index e95cef8588..c4be41720d 100644 --- a/static/js/SourceEditor.jsx +++ b/static/js/SourceEditor.jsx @@ -1,7 +1,7 @@ import Sefaria from "./sefaria/sefaria"; import $ from "./sefaria/sefariaJquery"; import {AdminEditor} from "./AdminEditor"; -import {requestWithCallBack, Autocompleter, InterfaceText} from "./Misc"; +import {Autocompleter, InterfaceText} from "./Misc"; import React, {useState} from "react"; import {useRef} from "react"; @@ -46,10 +46,16 @@ const SourceEditor = ({topic, close, origData={}}) => { const save = async function () { setSavingStatus(true); let refInUrl = isNew ? displayRef : origData.ref; - let url = `/api/ref-topic-links/${Sefaria.normRef(refInUrl)}`; - let postData = {"topic": topic, "is_new": isNew, 'new_ref': displayRef, 'interface_lang': Sefaria.interfaceLang}; - postData['description'] = {"title": data.enTitle, "prompt": data.prompt, "ai_context": data.ai_context, "review_state": "edited"}; - requestWithCallBack({url, data: postData, setSavingStatus, redirect: () => window.location.href = "/topics/"+topic}); + const payload = { + new_ref: displayRef, + topic, + is_new: isNew, + interface_lang: Sefaria.interfaceLang, + description: {"title": data.enTitle, "prompt": data.prompt, "ai_context": data.ai_context, "review_state": "edited"}, + } + Sefaria.postRefTopicLink(refInUrl, payload) + .then(() => window.location.href = `/topics/${topic}`) + .finally(() => setSavingStatus(false)); } const handleChange = (x) => { @@ -86,8 +92,9 @@ const SourceEditor = ({topic, close, origData={}}) => { } const deleteTopicSource = function() { - const url = `/api/ref-topic-links/${origData.ref}?topic=${topic}&interface_lang=${Sefaria.interfaceLang}`; - requestWithCallBack({url, type: "DELETE", redirect: () => window.location.href = `/topics/${topic}`}); + const url = `/api/ref-topic-links/${encodeURIComponent(origData.ref)}?topic=${topic}&interface_lang=${Sefaria.interfaceLang}`; + Sefaria.adminEditorApiRequest(url, null, null, "DELETE") + .then(() => window.location.href = `/topics/${topic}`); } const previousTitleItemRef = useRef(data.enTitle ? "Previous Title" : null); //use useRef to make value null even if component re-renders const previousPromptItemRef = useRef(data.prompt ? "Previous Prompt" : null); diff --git a/static/js/Story.jsx b/static/js/Story.jsx index 5814879375..8aaacc0d89 100644 --- a/static/js/Story.jsx +++ b/static/js/Story.jsx @@ -170,14 +170,14 @@ const ReviewStateIndicatorLang = ({reviewState, markReviewed}) => { } const markReviewedPostRequest = (lang, topic, topicLink) => { - const postData = { + const payload = { "topic": topic, "is_new": false, 'new_ref': topicLink.ref, 'interface_lang': lang === 'en' ? 'english' : 'hebrew', 'description' : {...topicLink.descriptions[lang], 'review_state': 'reviewed'} }; - return Sefaria.postToApi(`/api/ref-topic-links/${encodeURIComponent(topicLink.ref)}`, {}, postData); + return Sefaria.postRefTopicLink(topicLink.ref, payload); } const useReviewState = (topic, topicLink) => { diff --git a/static/js/TopicEditor.jsx b/static/js/TopicEditor.jsx index 88fc3f1092..2d279ebb76 100644 --- a/static/js/TopicEditor.jsx +++ b/static/js/TopicEditor.jsx @@ -1,5 +1,5 @@ import Sefaria from "./sefaria/sefaria"; -import {InterfaceText, requestWithCallBack, TopicPictureUploader} from "./Misc"; +import {InterfaceText, TopicPictureUploader} from "./Misc"; import $ from "./sefaria/sefariaJquery"; import {AdminEditor} from "./AdminEditor"; import {Reorder} from "./CategoryEditor"; @@ -109,7 +109,9 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => { const saveReorderedSubtopics = function () { const url = `/api/topic/reorder`; const postCategoryData = {topics: sortedSubtopics}; - requestWithCallBack({url, data: postCategoryData, setSavingStatus, redirect: () => window.location.href = "/topics"}); + Sefaria.adminEditorApiRequest(url, null, postCategoryData) + .then(() => window.location.href = "/topics") + .finally(() => setSavingStatus(false)); } const prepData = () => { @@ -189,7 +191,7 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => { const deleteObj = function() { const url = `/api/topic/delete/${data.origSlug}`; - requestWithCallBack({url, type: "DELETE", redirect: () => window.location.href = "/topics"}); + Sefaria.adminEditorApiRequest(url, null, null, "DELETE").then(() => window.location.href = "/topics"); } let items = ["Title", "Hebrew Title", "English Description", "Hebrew Description", "Category Menu", "English Alternate Titles", "Hebrew Alternate Titles",]; if (isCategory) { diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx index 44c25fc2b0..0d25a0dac8 100644 --- a/static/js/TopicPage.jsx +++ b/static/js/TopicPage.jsx @@ -344,7 +344,7 @@ const generatePrompts = async(topicSlug, linksToGenerate) => { }); const payload = {ref_topic_links: linksToGenerate}; try { - await Sefaria.postToApi(`/api/topics/generate-prompts/${topicSlug}`, {}, payload); + await Sefaria.apiRequestWithBody(`/api/topics/generate-prompts/${topicSlug}`, {}, payload); const refValues = linksToGenerate.map(item => item.ref).join(", "); alert("The following prompts are generating: " + refValues); } catch (error) { @@ -359,7 +359,7 @@ const publishPrompts = async (topicSlug, linksToPublish) => { ref.descriptions[lang]["published"] = true; }); try { - const response = await Sefaria.postToApi(`/api/ref-topic-links/bulk`, {}, linksToPublish); + const response = await Sefaria.apiRequestWithBody(`/api/ref-topic-links/bulk`, {}, linksToPublish); const refValues = response.map(item => item.anchorRef).join(", "); const shouldRefresh = confirm("The following prompts have been published: " + refValues + ". Refresh page to see results?"); if (shouldRefresh) { diff --git a/static/js/TopicSearch.jsx b/static/js/TopicSearch.jsx index c736cc72cf..60e098b1cd 100644 --- a/static/js/TopicSearch.jsx +++ b/static/js/TopicSearch.jsx @@ -68,27 +68,21 @@ class TopicSearch extends Component { const srefs = this.props.srefs; const update = this.props.update; const reset = this.reset; - $.post("/api/ref-topic-links/" + Sefaria.normRef(this.props.srefs), {"json": postJSON}, async function (data) { - if (data.error) { - alert(data.error); - } else { + Sefaria.postRefTopicLink(Sefaria.normRef(this.props.srefs), postJSON).then(async () => { const sectionRef = await Sefaria.getRef(Sefaria.normRef(srefs)).sectionRef; srefs.map(sref => { - if (!Sefaria._refTopicLinks[sref]) { - Sefaria._refTopicLinks[sref] = []; - } - Sefaria._refTopicLinks[sref].push(data); + if (!Sefaria._refTopicLinks[sref]) { + Sefaria._refTopicLinks[sref] = []; + } + Sefaria._refTopicLinks[sref].push(data); }); if (!Sefaria._refTopicLinks[sectionRef]) { - Sefaria._refTopicLinks[sectionRef] = []; + Sefaria._refTopicLinks[sectionRef] = []; } Sefaria._refTopicLinks[sectionRef].push(data); update(); reset(); alert("Topic added."); - } - }).fail(function (xhr, status, errorThrown) { - alert("Unfortunately, there may have been an error saving this topic information: " + errorThrown); }); } diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index ecffc50cd5..611ad3a685 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -623,27 +623,52 @@ Sefaria = extend(Sefaria, { firstName: firstName, lastName: lastName, }; - return await Sefaria.postToApi(`/api/subscribe/${email}`, null, payload); + return await Sefaria.apiRequestWithBody(`/api/subscribe/${email}`, null, payload); }, subscribeSteinsaltzNewsletter: async function(firstName, lastName, email) { const payload = {firstName, lastName}; - return await Sefaria.postToApi(`/api/subscribe/steinsaltz/${email}`, null, payload); + return await Sefaria.apiRequestWithBody(`/api/subscribe/steinsaltz/${email}`, null, payload); }, - - postToApi: async function(url, urlParams, payload) { + postRefTopicLink: function(refInUrl, payload) { + const url = `/api/ref-topic-links/${encodeURIComponent(Sefaria.normRef(refInUrl))}`; + // payload will need to be refactored once /api/ref-topic-links takes a more standard input + return Sefaria.adminEditorApiRequest(url, null, payload); + }, + adminEditorApiRequest: async function(url, urlParams, payload, method="POST") { + /** + * Wraps apiRequestWithBody() with basic alerting if response has an error + */ + let result; + try { + result = await Sefaria.apiRequestWithBody(url, urlParams, payload, method); + } catch (e) { + alert(Sefaria._("Something went wrong. Sorry!")); + throw e; + } + if (result.error) { + alert(result.error); + throw result.error; + } else { + return result; + } + }, + apiRequestWithBody: async function(url, urlParams, payload, method="POST") { + /** + * Generic function for performing an API request with a payload. Payload and urlParams are optional and will not be used if falsy. + */ let apiUrl = this.apiHost + url; if (urlParams) { apiUrl += '?' + new URLSearchParams(urlParams).toString(); } const response = await fetch(apiUrl, { - method: "POST", + method, mode: 'same-origin', headers: { 'X-CSRFToken': Cookies.get('csrftoken'), 'Content-Type': 'application/json' }, credentials: 'same-origin', - body: JSON.stringify(payload) + body: payload && JSON.stringify(payload) }); if (!response.ok) { From c4b08ce0f70828cf5da95bfbc676eee95df9d6d1 Mon Sep 17 00:00:00 2001 From: nsantacruz Date: Mon, 24 Jun 2024 11:48:35 +0300 Subject: [PATCH 3/3] fix(admin editor): only use Sefaria.normRef() which also does encodeURIComponent() --- static/js/SourceEditor.jsx | 2 +- static/js/sefaria/sefaria.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/SourceEditor.jsx b/static/js/SourceEditor.jsx index c4be41720d..1755c74efe 100644 --- a/static/js/SourceEditor.jsx +++ b/static/js/SourceEditor.jsx @@ -92,7 +92,7 @@ const SourceEditor = ({topic, close, origData={}}) => { } const deleteTopicSource = function() { - const url = `/api/ref-topic-links/${encodeURIComponent(origData.ref)}?topic=${topic}&interface_lang=${Sefaria.interfaceLang}`; + const url = `/api/ref-topic-links/${Sefaria.normRef(origData.ref)}?topic=${topic}&interface_lang=${Sefaria.interfaceLang}`; Sefaria.adminEditorApiRequest(url, null, null, "DELETE") .then(() => window.location.href = `/topics/${topic}`); } diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index 611ad3a685..231e3e9e37 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -630,7 +630,7 @@ Sefaria = extend(Sefaria, { return await Sefaria.apiRequestWithBody(`/api/subscribe/steinsaltz/${email}`, null, payload); }, postRefTopicLink: function(refInUrl, payload) { - const url = `/api/ref-topic-links/${encodeURIComponent(Sefaria.normRef(refInUrl))}`; + const url = `/api/ref-topic-links/${Sefaria.normRef(refInUrl)}`; // payload will need to be refactored once /api/ref-topic-links takes a more standard input return Sefaria.adminEditorApiRequest(url, null, payload); },