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, id, dataJSONStr, k;
 82 
 83         sync = sync || false;
 84 
 85         params = "";
 86         for (k in data) {
 87             if (data.hasOwnProperty(k)) {
 88                 params += "&" + escape(k) + "=" + escape(data[k]);
 89             }
 90         }
 91 
 92         dataJSONStr = Type.toJSON(data);
 93 
 94         // generate id
 95         do {
 96             id = action + Math.floor(Math.random() * 4096);
 97         } while (Type.exists(this.runningCalls[id]));
 98 
 99         // store information about the calls
100         this.runningCalls[id] = { action: action };
101         if (Type.exists(data.module)) {
102             this.runningCalls[id].module = data.module;
103         }
104 
105         fileurl = JXG.serverBase + "JXGServer.py";
106         passdata =
107             "action=" +
108             escape(action) +
109             "&id=" +
110             id +
111             "&dataJSON=" +
112             escape(Base64.encode(dataJSONStr));
113 
114         this.cbp = function (d) {
115             /*jslint evil:true*/
116             var str, data, tmp, inject, paramlist, id, i, j;
117 
118             str = new Zip.Unzip(Base64.decodeAsArray(d)).unzip();
119             if (Type.isArray(str) && str.length > 0) {
120                 str = str[0][0];
121             }
122 
123             if (!Type.exists(str)) {
124                 return;
125             }
126 
127             data =
128                 window.JSON && window.JSON.parse
129                     ? window.JSON.parse(str)
130                     : new Function("return " + str)();
131 
132             if (data.type === "error") {
133                 this.handleError(data);
134             } else if (data.type === "response") {
135                 id = data.id;
136 
137                 // inject fields
138                 for (i = 0; i < data.fields.length; i++) {
139                     tmp = data.fields[i];
140                     inject =
141                         tmp.namespace +
142                         (typeof new Function("return " + tmp.namespace)() === "object"
143                             ? "."
144                             : ".prototype.") +
145                         tmp.name +
146                         " = " +
147                         tmp.value;
148                     new Function(inject)();
149                 }
150 
151                 // inject handlers
152                 for (i = 0; i < data.handler.length; i++) {
153                     tmp = data.handler[i];
154                     paramlist = [];
155 
156                     for (j = 0; j < tmp.parameters.length; j++) {
157                         paramlist[j] = '"' + tmp.parameters[j] + '": ' + tmp.parameters[j];
158                     }
159                     // insert subnamespace named after module.
160                     inject =
161                         "if(typeof JXG.Server.modules." +
162                         this.runningCalls[id].module +
163                         ' == "undefined")' +
164                         "JXG.Server.modules." +
165                         this.runningCalls[id].module +
166                         " = {};";
167 
168                     // insert callback method which fetches and uses the server's data for calculation in JavaScript
169                     inject +=
170                         "JXG.Server.modules." +
171                         this.runningCalls[id].module +
172                         "." +
173                         tmp.name +
174                         "_cb = " +
175                         tmp.callback +
176                         ";";
177 
178                     // insert handler as JXG.Server.modules.<module name>.<handler name>
179                     inject +=
180                         "JXG.Server.modules." +
181                         this.runningCalls[id].module +
182                         "." +
183                         tmp.name +
184                         " = function (" +
185                         tmp.parameters.join(",") +
186                         ", __JXGSERVER_CB__, __JXGSERVER_SYNC) {" +
187                         'if(typeof __JXGSERVER_CB__ == "undefined") __JXGSERVER_CB__ = JXG.Server.modules.' +
188                         this.runningCalls[id].module +
189                         "." +
190                         tmp.name +
191                         "_cb;" +
192                         "var __JXGSERVER_PAR__ = {" +
193                         paramlist.join(",") +
194                         ', "module": "' +
195                         this.runningCalls[id].module +
196                         '", "handler": "' +
197                         tmp.name +
198                         '" };' +
199                         'JXG.Server.callServer("exec", __JXGSERVER_CB__, __JXGSERVER_PAR__, __JXGSERVER_SYNC);' +
200                         "};";
201                     new Function(inject)();
202                 }
203 
204                 delete this.runningCalls[id];
205 
206                 // handle data
207                 callback(data.data);
208             }
209         };
210 
211         // bind cbp callback method to JXG.Server to get access to JXG.Server fields from within cpb
212         this.cb = JXG.bind(this.cbp, this);
213 
214         // We are using our own XMLHttpRequest object in here because of a/sync and POST
215         if (window.XMLHttpRequest) {
216             AJAX = new XMLHttpRequest();
217             AJAX.overrideMimeType("text/plain; charset=iso-8859-1");
218         } else {
219             AJAX = new ActiveXObject("Microsoft.XMLHTTP");
220         }
221         if (AJAX) {
222             // POST is required if data sent to server is too long for a url.
223             // some browsers/http servers don't accept long urls.
224             AJAX.open("POST", fileurl, !sync);
225             AJAX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
226 
227             if (!sync) {
228                 // Define function to fetch data received from server
229                 // that function returning a function is required to make this.cb known to the function.
230                 AJAX.onreadystatechange = (function (cb) {
231                     return function () {
232                         if (AJAX.readyState === 4 && AJAX.status === 200) {
233                             cb(AJAX.responseText);
234                             return true;
235                         }
236                         return false;
237                     };
238                 })(this.cb);
239             }
240 
241             // send the data
242             AJAX.send(passdata);
243             if (sync) {
244                 this.cb(AJAX.responseText);
245                 return true;
246             }
247         }
248 
249         return false;
250     },
251 
252     /**
253      * Callback for the default action 'load'.
254      */
255     loadModule_cb: function (data) {
256         var i;
257         for (i = 0; i < data.length; i++) {
258             JXG.debug(data[i].name + ": " + data[i].value);
259         }
260     },
261 
262     /**
263      * Loads a module from the server.
264      * @param {string} module A string containing the module. Has to match the filename of the Python module on the server exactly including
265      * lower and upper case letters without the file ending .py.
266      */
267     loadModule: function (module) {
268         return JXG.Server.callServer(
269             "load",
270             JXG.Server.loadModule_cb,
271             { module: module },
272             true
273         );
274     }
275 };
276 
277 JXG.Server.load = JXG.Server.loadModule;
278 
279 export default JXG.Server;
280