1 /*
2  * ZoteroBetterBibTeX.cpp
3  *
4  * Copyright (C) 2009-20 by RStudio, Inc.
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include "ZoteroBetterBibTeX.hpp"
17 
18 #include <shared_core/json/Json.hpp>
19 #include <shared_core/json/Json.hpp>
20 
21 #include <core/http/TcpIpBlockingClient.hpp>
22 #include <core/http/SocketUtils.hpp>
23 
24 #include <session/prefs/UserPrefs.hpp>
25 #include <session/prefs/UserState.hpp>
26 
27 #include <session/SessionModuleContext.hpp>
28 
29 #include "ZoteroCollections.hpp"
30 #include "ZoteroCollectionsLocal.hpp"
31 
32 namespace rstudio {
33 
34 using namespace core;
35 
36 namespace session {
37 namespace modules {
38 namespace zotero {
39 
40 using namespace collections;
41 
42 namespace  {
43 
44 template<typename T>
betterBibtexJsonRpcRequest(const std::string & method,const json::Array & params,T * pResponse,std::string * pWarning)45 bool betterBibtexJsonRpcRequest(const std::string& method, const json::Array& params, T* pResponse, std::string* pWarning)
46 {
47    // build request
48    http::Request request;
49    request.setMethod("POST");
50    request.setContentType("application/json");
51    request.setHeader("Accept", "application/json");
52    request.setUri("/better-bibtex/json-rpc");
53    json::Object rpcRequest;
54    rpcRequest["jsonrpc"] = "2.0";
55    rpcRequest["method"] = method;
56    rpcRequest["params"] = params;
57    request.setBody(rpcRequest.writeFormatted());
58 
59    http::Response response;
60    Error error = http::sendRequest("localhost", "23119", boost::posix_time::milliseconds(10000), request, &response);
61    if (!error)
62    {
63       if (response.statusCode() == http::status::Ok)
64       {
65          json::Value responseValue;
66          Error error = responseValue.parse(response.body());
67          if (!error)
68          {
69             if (responseValue.isObject() && responseValue.getObject().hasMember("result"))
70             {
71                json::Value resultValue = responseValue.getObject()["result"];
72                if (json::isType<T>(resultValue))
73                {
74                   *pResponse = resultValue.getValue<T>();
75                   return true;
76                }
77             }
78 
79          }
80 
81          *pWarning = "Unexpected data format provided by Better BibTeX";
82          LOG_ERROR_MESSAGE(*pWarning + " : " + response.body());
83 
84       }
85       else
86       {
87          *pWarning = "Unexpected status " +
88                      safe_convert::numberToString(response.statusCode()) + " from Better BibTeX";
89          LOG_ERROR_MESSAGE(*pWarning);
90       }
91 
92    }
93    else if (http::isConnectionUnavailableError(error) ||
94             (error = systemError(boost::system::errc::timed_out, ErrorLocation())))
95    {
96       *pWarning = "Unable to connect to Better BibTeX. Please ensure that Zotero is running.";
97    }
98    else
99    {
100       *pWarning = "Unexpected error communicating with Better BibTex";
101       LOG_ERROR(error);
102    }
103 
104    return false;
105 }
106 
107 
108 } // anonymous namespace
109 
110 
betterBibtexInConfig(const std::string & config)111 bool betterBibtexInConfig(const std::string& config)
112 {
113    return config.find_first_of("extensions.zotero.translators.better-bibtex") != std::string::npos;
114 }
115 
betterBibtexEnabled()116 bool betterBibtexEnabled()
117 {
118    return session::prefs::userState().zoteroUseBetterBibtex();
119 }
120 
betterBibtexProvideIds(const collections::ZoteroCollections & collections,collections::ZoteroCollectionsHandler handler)121 void betterBibtexProvideIds(const collections::ZoteroCollections& collections,
122                             collections::ZoteroCollectionsHandler handler)
123 {
124    // get zotero key for each item in all of the collections
125    std::vector<std::string> zoteroKeys;
126    for (auto collection : collections)
127    {
128       std::transform(collection.items.begin(), collection.items.end(), std::back_inserter(zoteroKeys), [](const json::Value& itemJson) {
129          return itemJson.getObject()["key"].getString();
130       });
131    }
132 
133    // call better bibtex to create a map of zotero keys to bbt citation ids
134    std::string warning;
135    std::map<std::string,std::string> keyMap;
136    json::Object keyMapJson;
137    json::Array params;
138    params.push_back(json::toJsonArray(zoteroKeys));
139    if (betterBibtexJsonRpcRequest("item.citationkey", params, &keyMapJson, &warning))
140    {
141       for (auto member : keyMapJson)
142       {
143          if (member.getValue().isString())
144             keyMap[member.getName()] = member.getValue().getString();
145       }
146    }
147 
148    // new set of collections with updated ids
149    collections::ZoteroCollections updatedCollections;
150    std::transform(collections.begin(), collections.end(), std::back_inserter(updatedCollections),
151                   [&keyMap](const collections::ZoteroCollection& collection) {
152       json::Array updatedItems;
153       std::transform(collection.items.begin(), collection.items.end(), std::back_inserter(updatedItems),
154                      [&keyMap](const json::Value& itemJson) {
155           json::Object itemObject = itemJson.getObject();
156           if (itemObject.hasMember("key"))
157           {
158              std::string zoteroKey = itemObject["key"].getString();
159              std::map<std::string,std::string>::const_iterator it = keyMap.find(zoteroKey);
160              if (it != keyMap.end())
161              {
162                 itemObject["id"] = it->second;
163              }
164           }
165           return itemObject;
166       });
167       collections::ZoteroCollection updatedCollection(collections::ZoteroCollectionSpec(collection.name, collection.key, collection.parentKey, collection.version));
168       updatedCollection.items = updatedItems;
169       return updatedCollection;
170    });
171 
172    // return
173    handler(Success(), updatedCollections, warning);
174 }
175 
setBetterBibtexNotFoundResult(const std::string & warning,json::JsonRpcResponse * pResponse)176 void setBetterBibtexNotFoundResult(const std::string& warning, json::JsonRpcResponse* pResponse)
177 {
178    json::Object resultJson;
179    resultJson["status"] = "nohost";
180    resultJson["warning"] = warning;
181    pResponse->setResult(resultJson);
182 }
183 
betterBibtexExport(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)184 Error betterBibtexExport(const json::JsonRpcRequest& request,
185                          json::JsonRpcResponse* pResponse)
186 {
187    // extract params
188    json::Array itemKeysJson;
189    std::string translatorId;
190    int libraryId;
191    Error error = json::readParams(request.params, &itemKeysJson, &translatorId, &libraryId);
192    if (error)
193       return error;
194 
195    // include library in item keys
196    boost::format fmt("%1%:%2%");
197    for (std::size_t i=0; i<itemKeysJson.getSize(); i++)
198       itemKeysJson[i] = boost::str(fmt % libraryId % itemKeysJson[i].getString());
199 
200    // get citation keys
201    std::string warning;
202    json::Object keyMapJson;
203    json::Array params;
204    params.push_back(itemKeysJson);
205    if (betterBibtexJsonRpcRequest("item.citationkey", params, &keyMapJson, &warning))
206    {
207       // extract keys
208       json::Array citekeysJson;
209       std::transform(keyMapJson.begin(), keyMapJson.end(), std::back_inserter(citekeysJson),
210                      [](const json::Object::Member& member) {
211                         return member.getValue();
212                      });
213 
214       // perform export
215       params.clear();
216       params.push_back(citekeysJson);
217       params.push_back(translatorId);
218       params.push_back(libraryId);
219       json::Array exportJson;
220       if (betterBibtexJsonRpcRequest("item.export", params, &exportJson, &warning))
221       {
222          if (exportJson.getSize() >= 3 &&
223              exportJson[0].isInt() && exportJson[0].getInt() == 200 &&
224              exportJson[2].isString())
225          {
226             json::Object jsonResult;
227             jsonResult["status"] = "ok";
228             jsonResult["message"] = exportJson[2].getString();
229             pResponse->setResult(jsonResult);
230          }
231          else
232          {
233             std::string warning = "Unexpected response from Better BibTeX";
234             LOG_ERROR_MESSAGE(warning + " : " + exportJson.write());
235             setBetterBibtexNotFoundResult(warning, pResponse);
236          }
237       }
238       else
239       {
240          setBetterBibtexNotFoundResult(warning, pResponse);
241       }
242    }
243    else
244    {
245       setBetterBibtexNotFoundResult(warning, pResponse);
246    }
247 
248    return Success();
249 }
250 
betterBibtexInit()251 Error betterBibtexInit()
252 {
253    // force better bibtex pref off if the config isn't found
254    if (collections::localZoteroAvailable())
255    {
256       collections::DetectedLocalZoteroConfig config = collections::detectedLocalZoteroConfig();
257       if (prefs::userState().zoteroUseBetterBibtex() && !config.betterBibtex)
258          prefs::userState().setZoteroUseBetterBibtex(false);
259    }
260 
261    return Success();
262 }
263 
264 
265 } // end namespace zotero
266 } // end namespace modules
267 } // end namespace session
268 } // end namespace rstudio
269