1 /*
  2     Copyright 2008-2023
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true, escape:true, window:true, ActiveXObject:true, XMLHttpRequest:true*/
 33 /*jslint nomen: true, plusplus: true*/
 34 
 35 /**
 36  * @fileoverview The JXG.Server is a wrapper for a smoother integration of server side calculations. on the
 37  * server side a python plugin system is used.
 38  */
 39 
 40 import JXG from "../jxg";
 41 import Zip from "../utils/zip";
 42 import Base64 from "../utils/base64";
 43 import Type from "../utils/type";
 44 
 45 /**
 46  * @namespace
 47  * JXG.Server namespace holding functions to load JXG server modules.
 48  */
 49 JXG.Server = {
 50     /**
 51      * This is where all of a module's handlers are accessed from. If you're loading a module named JXGModule which
 52      * provides a handler called ImaHandler, then this handler can be called by invoking JXG.Server.modules.JXGModule.ImaHandler().
 53      * @namespace
 54      */
 55     modules: {},
 56 
 57     /**
 58      * Stores all asynchronous calls to server which aren't finished yet.
 59      * @private
 60      */
 61     runningCalls: {},
 62 
 63     /**
 64      * Handles errors, just a default implementation, can be overwritten by you, if you want to handle errors by yourself.
 65      * @param {object} data An object holding a field of type string named message handling the error described in the message string.
 66      */
 67     handleError: function (data) {
 68         JXG.debug("error occured, server says: " + data.message);
 69     },
 70 
 71     /**
 72      * The main method of JXG.Server. Actually makes the calls to the server and parses the feedback.
 73      * @param {String} action Can be 'load' or 'exec'.
 74      * @param {function} callback Function pointer or anonymous function which takes as it's only argument an
 75      * object containing the data from the server. The fields of this object depend on the reply of the server
 76      * module. See the correspondings server module readme.
 77      * @param {Object} data What is to be sent to the server.
 78      * @param {Boolean} sync If the call should be synchronous or not.
 79      */
 80     callServer: function (action, callback, data, sync) {
 81         var fileurl, passdata, AJAX,// params, k,
 82             id, dataJSONStr;
 83 
 84         sync = sync || false;
 85 
 86         // params = "";
 87         // for (k in data) {
 88         //     if (data.hasOwnProperty(k)) {
 89         //         params += "&" + escape(k) + "=" + escape(data[k]);
 90         //     }
 91         // }
 92 
 93         dataJSONStr = Type.toJSON(data);
 94 
 95         // generate id
 96         do {
 97             id = action + Math.floor(Math.random() * 4096);
 98         } while (Type.exists(this.runningCalls[id]));
 99 
100         // store information about the calls
101         this.runningCalls[id] = { action: action };
102         if (Type.exists(data.module)) {
103             this.runningCalls[id].module = data.module;
104         }
105 
106         fileurl = JXG.serverBase + "JXGServer.py";
107         passdata =
108             "action=" +
109             escape(action) +
110             "&id=" +
111             id +
112             "&dataJSON=" +
113             escape(Base64.encode(dataJSONStr));
114 
115         this.cbp = function (d) {
116             /*jslint evil:true*/
117             var str, data, tmp, inject, paramlist, id, i, j;
118 
119             str = new Zip.Unzip(Base64.decodeAsArray(d)).unzip();
120             if (Type.isArray(str) && str.length > 0) {
121                 str = str[0][0];
122             }
123 
124             if (!Type.exists(str)) {
125                 return;
126             }
127 
128             data =
129                 window.JSON && window.JSON.parse
130                     ? window.JSON.parse(str)
131                     : new Function("return " + str)();
132 
133             if (data.type === "error") {
134                 this.handleError(data);
135             } else if (data.type === "response") {
136                 id = data.id;
137 
138                 // inject fields
139                 for (i = 0; i < data.fields.length; i++) {
140                     tmp = data.fields[i];
141                     inject =
142                         tmp.namespace +
143                         (typeof new Function("return " + tmp.namespace)() === "object"
144                             ? "."
145                             : ".prototype.") +
146                         tmp.name +
147                         " = " +
148                         tmp.value;
149                     new Function(inject)();
150                 }
151 
152                 // inject handlers
153                 for (i = 0; i < data.handler.length; i++) {
154                     tmp = data.handler[i];
155                     paramlist = [];
156 
157                     for (j = 0; j < tmp.parameters.length; j++) {
158                         paramlist[j] = '"' + tmp.parameters[j] + '": ' + tmp.parameters[j];
159                     }
160                     // insert subnamespace named after module.
161                     inject =
162                         "if(typeof JXG.Server.modules." +
163                         this.runningCalls[id].module +
164                         ' == "undefined")' +
165                         "JXG.Server.modules." +
166                         this.runningCalls[id].module +
167                         " = {};";
168 
169                     // insert callback method which fetches and uses the server's data for calculation in JavaScript
170                     inject +=
171                         "JXG.Server.modules." +
172                         this.runningCalls[id].module +
173                         "." +
174                         tmp.name +
175                         "_cb = " +
176                         tmp.callback +
177                         ";";
178 
179                     // insert handler as JXG.Server.modules.<module name>.<handler name>
180                     inject +=
181                         "JXG.Server.modules." +
182                         this.runningCalls[id].module +
183                         "." +
184                         tmp.name +
185                         " = function (" +
186                         tmp.parameters.join(",") +
187                         ", __JXGSERVER_CB__, __JXGSERVER_SYNC) {" +
188                         'if(typeof __JXGSERVER_CB__ == "undefined") __JXGSERVER_CB__ = JXG.Server.modules.' +
189                         this.runningCalls[id].module +
190                         "." +
191                         tmp.name +
192                         "_cb;" +
193                         "var __JXGSERVER_PAR__ = {" +
194                         paramlist.join(",") +
195                         ', "module": "' +
196                         this.runningCalls[id].module +
197                         '", "handler": "' +
198                         tmp.name +
199                         '" };' +
200                         'JXG.Server.callServer("exec", __JXGSERVER_CB__, __JXGSERVER_PAR__, __JXGSERVER_SYNC);' +
201                         "};";
202                     new Function(inject)();
203                 }
204 
205                 delete this.runningCalls[id];
206 
207                 // handle data
208                 callback(data.data);
209             }
210         };
211 
212         // bind cbp callback method to JXG.Server to get access to JXG.Server fields from within cpb
213         this.cb = JXG.bind(this.cbp, this);
214 
215         // We are using our own XMLHttpRequest object in here because of a/sync and POST
216         if (window.XMLHttpRequest) {
217             AJAX = new XMLHttpRequest();
218             AJAX.overrideMimeType("text/plain; charset=iso-8859-1");
219         } else {
220             AJAX = new ActiveXObject("Microsoft.XMLHTTP");
221         }
222         if (AJAX) {
223             // POST is required if data sent to server is too long for a url.
224             // some browsers/http servers don't accept long urls.
225             AJAX.open("POST", fileurl, !sync);
226             AJAX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
227 
228             if (!sync) {
229                 // Define function to fetch data received from server
230                 // that function returning a function is required to make this.cb known to the function.
231                 AJAX.onreadystatechange = (function (cb) {
232                     return function () {
233                         if (AJAX.readyState === 4 && AJAX.status === 200) {
234                             cb(AJAX.responseText);
235                             return true;
236                         }
237                         return false;
238                     };
239                 })(this.cb);
240             }
241 
242             // send the data
243             AJAX.send(passdata);
244             if (sync) {
245                 this.cb(AJAX.responseText);
246                 return true;
247             }
248         }
249 
250         return false;
251     },
252 
253     /**
254      * Callback for the default action 'load'.
255      */
256     loadModule_cb: function (data) {
257         var i;
258         for (i = 0; i < data.length; i++) {
259             JXG.debug(data[i].name + ": " + data[i].value);
260         }
261     },
262 
263     /**
264      * Loads a module from the server.
265      * @param {string} module A string containing the module. Has to match the filename of the Python module on the server exactly including
266      * lower and upper case letters without the file ending .py.
267      */
268     loadModule: function (module) {
269         return JXG.Server.callServer(
270             "load",
271             JXG.Server.loadModule_cb,
272             { module: module },
273             true
274         );
275     }
276 };
277 
278 JXG.Server.load = JXG.Server.loadModule;
279 
280 export default JXG.Server;
281