File indexing completed on 2024-11-24 04:54:31

0001 /*
0002     SPDX-License-Identifier: LGPL-3.0-only OR GPL-2.0-or-later OR GPL-3.0-or-later
0003 */
0004 
0005 /****************************************************************************
0006 **
0007 ** Copyright (C) 2016 The Qt Company Ltd.
0008 ** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
0009 ** Contact: https://www.qt.io/licensing/
0010 **
0011 ** This file is part of the QtWebChannel module of the Qt Toolkit.
0012 **
0013 ** $QT_BEGIN_LICENSE:LGPL$
0014 ** Commercial License Usage
0015 ** Licensees holding valid commercial Qt licenses may use this file in
0016 ** accordance with the commercial license agreement provided with the
0017 ** Software or, alternatively, in accordance with the terms contained in
0018 ** a written agreement between you and The Qt Company. For licensing terms
0019 ** and conditions see https://www.qt.io/terms-conditions. For further
0020 ** information use the contact form at https://www.qt.io/contact-us.
0021 **
0022 ** GNU Lesser General Public License Usage
0023 ** Alternatively, this file may be used under the terms of the GNU Lesser
0024 ** General Public License version 3 as published by the Free Software
0025 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
0026 ** packaging of this file. Please review the following information to
0027 ** ensure the GNU Lesser General Public License version 3 requirements
0028 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
0029 **
0030 ** GNU General Public License Usage
0031 ** Alternatively, this file may be used under the terms of the GNU
0032 ** General Public License version 2.0 or (at your option) the GNU General
0033 ** Public license version 3 or any later version approved by the KDE Free
0034 ** Qt Foundation. The licenses are as published by the Free Software
0035 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
0036 ** included in the packaging of this file. Please review the following
0037 ** information to ensure the GNU General Public License requirements will
0038 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
0039 ** https://www.gnu.org/licenses/gpl-3.0.html.
0040 **
0041 ** $QT_END_LICENSE$
0042 **
0043 ****************************************************************************/
0044 
0045 "use strict";
0046 
0047 var QWebChannelMessageTypes = {
0048     signal: 1,
0049     propertyUpdate: 2,
0050     init: 3,
0051     idle: 4,
0052     debug: 5,
0053     invokeMethod: 6,
0054     connectToSignal: 7,
0055     disconnectFromSignal: 8,
0056     setProperty: 9,
0057     response: 10,
0058 };
0059 
0060 var QWebChannel = function(transport, initCallback)
0061 {
0062     if (typeof transport !== "object" || typeof transport.send !== "function") {
0063         console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
0064                       " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
0065         return;
0066     }
0067 
0068     var channel = this;
0069     this.transport = transport;
0070 
0071     this.send = function(data)
0072     {
0073         if (typeof(data) !== "string") {
0074             data = JSON.stringify(data);
0075         }
0076         channel.transport.send(data);
0077     }
0078 
0079     this.transport.onmessage = function(message)
0080     {
0081         var data = message.data;
0082         if (typeof data === "string") {
0083             data = JSON.parse(data);
0084         }
0085         switch (data.type) {
0086             case QWebChannelMessageTypes.signal:
0087                 channel.handleSignal(data);
0088                 break;
0089             case QWebChannelMessageTypes.response:
0090                 channel.handleResponse(data);
0091                 break;
0092             case QWebChannelMessageTypes.propertyUpdate:
0093                 channel.handlePropertyUpdate(data);
0094                 break;
0095             default:
0096                 console.error("invalid message received:", message.data);
0097                 break;
0098         }
0099     }
0100 
0101     this.execCallbacks = {};
0102     this.execId = 0;
0103     this.exec = function(data, callback)
0104     {
0105         if (!callback) {
0106             // if no callback is given, send directly
0107             channel.send(data);
0108             return;
0109         }
0110         if (channel.execId === Number.MAX_VALUE) {
0111             // wrap
0112             channel.execId = Number.MIN_VALUE;
0113         }
0114         if (data.hasOwnProperty("id")) {
0115             console.error("Cannot exec message with property id: " + JSON.stringify(data));
0116             return;
0117         }
0118         data.id = channel.execId++;
0119         channel.execCallbacks[data.id] = callback;
0120         channel.send(data);
0121     };
0122 
0123     this.objects = {};
0124 
0125     this.handleSignal = function(message)
0126     {
0127         var object = channel.objects[message.object];
0128         if (object) {
0129             object.signalEmitted(message.signal, message.args);
0130         } else {
0131             console.warn("Unhandled signal: " + message.object + "::" + message.signal);
0132         }
0133     }
0134 
0135     this.handleResponse = function(message)
0136     {
0137         if (!message.hasOwnProperty("id")) {
0138             console.error("Invalid response message received: ", JSON.stringify(message));
0139             return;
0140         }
0141         channel.execCallbacks[message.id](message.data);
0142         delete channel.execCallbacks[message.id];
0143     }
0144 
0145     this.handlePropertyUpdate = function(message)
0146     {
0147         for (var i in message.data) {
0148             var data = message.data[i];
0149             var object = channel.objects[data.object];
0150             if (object) {
0151                 object.propertyUpdate(data.signals, data.properties);
0152             } else {
0153                 console.warn("Unhandled property update: " + data.object + "::" + data.signal);
0154             }
0155         }
0156         channel.exec({type: QWebChannelMessageTypes.idle});
0157     }
0158 
0159     this.debug = function(message)
0160     {
0161         channel.send({type: QWebChannelMessageTypes.debug, data: message});
0162     };
0163 
0164     channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
0165         for (var objectName in data) {
0166             var object = new QObject(objectName, data[objectName], channel);
0167         }
0168         // now unwrap properties, which might reference other registered objects
0169         for (var objectName in channel.objects) {
0170             channel.objects[objectName].unwrapProperties();
0171         }
0172         if (initCallback) {
0173             initCallback(channel);
0174         }
0175         channel.exec({type: QWebChannelMessageTypes.idle});
0176     });
0177 };
0178 
0179 function QObject(name, data, webChannel)
0180 {
0181     this.__id__ = name;
0182     webChannel.objects[name] = this;
0183 
0184     // List of callbacks that get invoked upon signal emission
0185     this.__objectSignals__ = {};
0186 
0187     // Cache of all properties, updated when a notify signal is emitted
0188     this.__propertyCache__ = {};
0189 
0190     var object = this;
0191 
0192     // ----------------------------------------------------------------------
0193 
0194     this.unwrapQObject = function(response)
0195     {
0196         if (response instanceof Array) {
0197             // support list of objects
0198             var ret = new Array(response.length);
0199             for (var i = 0; i < response.length; ++i) {
0200                 ret[i] = object.unwrapQObject(response[i]);
0201             }
0202             return ret;
0203         }
0204         if (!response
0205             || !response["__QObject*__"]
0206             || response.id === undefined) {
0207             return response;
0208         }
0209 
0210         var objectId = response.id;
0211         if (webChannel.objects[objectId])
0212             return webChannel.objects[objectId];
0213 
0214         if (!response.data) {
0215             console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
0216             return;
0217         }
0218 
0219         var qObject = new QObject( objectId, response.data, webChannel );
0220         qObject.destroyed.connect(function() {
0221             if (webChannel.objects[objectId] === qObject) {
0222                 delete webChannel.objects[objectId];
0223                 // reset the now deleted QObject to an empty {} object
0224                 // just assigning {} though would not have the desired effect, but the
0225                 // below also ensures all external references will see the empty map
0226                 // NOTE: this detour is necessary to workaround QTBUG-40021
0227                 var propertyNames = [];
0228                 for (var propertyName in qObject) {
0229                     propertyNames.push(propertyName);
0230                 }
0231                 for (var idx in propertyNames) {
0232                     delete qObject[propertyNames[idx]];
0233                 }
0234             }
0235         });
0236         // here we are already initialized, and thus must directly unwrap the properties
0237         qObject.unwrapProperties();
0238         return qObject;
0239     }
0240 
0241     this.unwrapProperties = function()
0242     {
0243         for (var propertyIdx in object.__propertyCache__) {
0244             object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
0245         }
0246     }
0247 
0248     function addSignal(signalData, isPropertyNotifySignal)
0249     {
0250         var signalName = signalData[0];
0251         var signalIndex = signalData[1];
0252         object[signalName] = {
0253             connect: function(callback) {
0254                 if (typeof(callback) !== "function") {
0255                     console.error("Bad callback given to connect to signal " + signalName);
0256                     return;
0257                 }
0258 
0259                 object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
0260                 object.__objectSignals__[signalIndex].push(callback);
0261 
0262                 if (!isPropertyNotifySignal && signalName !== "destroyed") {
0263                     // only required for "pure" signals, handled separately for properties in propertyUpdate
0264                     // also note that we always get notified about the destroyed signal
0265                     webChannel.exec({
0266                         type: QWebChannelMessageTypes.connectToSignal,
0267                         object: object.__id__,
0268                         signal: signalIndex
0269                     });
0270                 }
0271             },
0272             disconnect: function(callback) {
0273                 if (typeof(callback) !== "function") {
0274                     console.error("Bad callback given to disconnect from signal " + signalName);
0275                     return;
0276                 }
0277                 object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
0278                 var idx = object.__objectSignals__[signalIndex].indexOf(callback);
0279                 if (idx === -1) {
0280                     console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
0281                     return;
0282                 }
0283                 object.__objectSignals__[signalIndex].splice(idx, 1);
0284                 if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
0285                     // only required for "pure" signals, handled separately for properties in propertyUpdate
0286                     webChannel.exec({
0287                         type: QWebChannelMessageTypes.disconnectFromSignal,
0288                         object: object.__id__,
0289                         signal: signalIndex
0290                     });
0291                 }
0292             }
0293         };
0294     }
0295 
0296     /**
0297      * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
0298      */
0299     function invokeSignalCallbacks(signalName, signalArgs)
0300     {
0301         var connections = object.__objectSignals__[signalName];
0302         if (connections) {
0303             connections.forEach(function(callback) {
0304                 callback.apply(callback, signalArgs);
0305             });
0306         }
0307     }
0308 
0309     this.propertyUpdate = function(signals, propertyMap)
0310     {
0311         // update property cache
0312         for (var propertyIndex in propertyMap) {
0313             var propertyValue = propertyMap[propertyIndex];
0314             object.__propertyCache__[propertyIndex] = propertyValue;
0315         }
0316 
0317         for (var signalName in signals) {
0318             // Invoke all callbacks, as signalEmitted() does not. This ensures the
0319             // property cache is updated before the callbacks are invoked.
0320             invokeSignalCallbacks(signalName, signals[signalName]);
0321         }
0322     }
0323 
0324     this.signalEmitted = function(signalName, signalArgs)
0325     {
0326         invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
0327     }
0328 
0329     function addMethod(methodData)
0330     {
0331         var methodName = methodData[0];
0332         var methodIdx = methodData[1];
0333         object[methodName] = function() {
0334             var args = [];
0335             var callback;
0336             for (var i = 0; i < arguments.length; ++i) {
0337                 var argument = arguments[i];
0338                 if (typeof argument === "function")
0339                     callback = argument;
0340                 else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
0341                     args.push({
0342                         "id": argument.__id__
0343                     });
0344                 else
0345                     args.push(argument);
0346             }
0347 
0348             webChannel.exec({
0349                 "type": QWebChannelMessageTypes.invokeMethod,
0350                 "object": object.__id__,
0351                 "method": methodIdx,
0352                 "args": args
0353             }, function(response) {
0354                 if (response !== undefined) {
0355                     var result = object.unwrapQObject(response);
0356                     if (callback) {
0357                         (callback)(result);
0358                     }
0359                 }
0360             });
0361         };
0362     }
0363 
0364     function bindGetterSetter(propertyInfo)
0365     {
0366         var propertyIndex = propertyInfo[0];
0367         var propertyName = propertyInfo[1];
0368         var notifySignalData = propertyInfo[2];
0369         // initialize property cache with current value
0370         // NOTE: if this is an object, it is not directly unwrapped as it might
0371         // reference other QObject that we do not know yet
0372         object.__propertyCache__[propertyIndex] = propertyInfo[3];
0373 
0374         if (notifySignalData) {
0375             if (notifySignalData[0] === 1) {
0376                 // signal name is optimized away, reconstruct the actual name
0377                 notifySignalData[0] = propertyName + "Changed";
0378             }
0379             addSignal(notifySignalData, true);
0380         }
0381 
0382         Object.defineProperty(object, propertyName, {
0383             configurable: true,
0384             get: function () {
0385                 var propertyValue = object.__propertyCache__[propertyIndex];
0386                 if (propertyValue === undefined) {
0387                     // This shouldn't happen
0388                     console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
0389                 }
0390 
0391                 return propertyValue;
0392             },
0393             set: function(value) {
0394                 if (value === undefined) {
0395                     console.warn("Property setter for " + propertyName + " called with undefined value!");
0396                     return;
0397                 }
0398                 object.__propertyCache__[propertyIndex] = value;
0399                 var valueToSend = value;
0400                 if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
0401                     valueToSend = { "id": valueToSend.__id__ };
0402                 webChannel.exec({
0403                     "type": QWebChannelMessageTypes.setProperty,
0404                     "object": object.__id__,
0405                     "property": propertyIndex,
0406                     "value": valueToSend
0407                 });
0408             }
0409         });
0410 
0411     }
0412 
0413     // ----------------------------------------------------------------------
0414 
0415     data.methods.forEach(addMethod);
0416 
0417     data.properties.forEach(bindGetterSetter);
0418 
0419     data.signals.forEach(function(signal) { addSignal(signal, false); });
0420 
0421     for (var name in data.enums) {
0422         object[name] = data.enums[name];
0423     }
0424 }
0425 
0426 //required for use with nodejs
0427 if (typeof module === 'object') {
0428     module.exports = {
0429         QWebChannel: QWebChannel
0430     };
0431 }