//
// ClientKitApi.cpp - defines interface to JavaScript and includes the Perforce C++ API
//
#include "ClientKitApi.h"
#include "mainwindow.h"
#include "clientapi.h"
#include <enviro.h>
#include <QString>
#include <QStringList>
#include <QFile>
#include <QDir>
#include <QProcess>
#include <QDesktopServices>
#include <QUrl>
#include <QVariantMap>
#include <QVariantList>
#include <QCoreApplication>
#include <QLocale>
#include <QWebPage>
class HClientUser : public ClientUser
{
public:
HClientUser(ClientKitApi* ha) {mClientKitApi = ha;}
virtual void OutputInfo( char /*level*/, const char * data )
{
QVariantMap entry;
entry.insert(data, "");
mTaggedList.append(entry);
}
virtual void OutputStat(StrDict *varList)
{
StrRef var, val;
QVariantMap entry;
for( int i = 0; varList->GetVar( i, var, val ); i++ )
{
if( var == "func" || var == P4Tag::v_specFormatted )
continue;
entry.insert(var.Text(),val.Text());
}
mTaggedList.append(entry);
}
virtual void OutputError( const char * errBuf)
{
QString errmsg = QString("OutputError: " + QString(errBuf));
mClientKitApi->errorList()->append(errmsg);
}
virtual void InputData( StrBuf *strbuf, Error *e )
{
QString inputData = mClientKitApi->inputData();
if (inputData.isEmpty())
{
e->Set( E_FAILED, "No user-input supplied." );
mClientKitApi->errorList()->append("Error: No input data. Did you forget to call p4.setInput()?");
}
if (mClientKitApi)
strbuf->Set(inputData.toLocal8Bit()); // todo: refactor into constructor
}
virtual int Help( char *const *help )
{
QString errmsg = QString("Help: " + QString(*help));
mClientKitApi->errorList()->append(errmsg);
return 0;
}
void clearOutputMap() {mTaggedList.clear();}
QVariantList getOutputList() {return mTaggedList;}
private:
QVariantList mTaggedList;
ClientKitApi* mClientKitApi;
};
ClientKitApi::ClientKitApi(QObject* /*parent*/, MainWindow* mainWindow, QString pointerName)
{
mMainWindow = mainWindow;
mMyJSPointerName = pointerName;
}
QString ClientKitApi::inputData()
{
return mInputForm;
}
// todo: this code could be moved to class Connection
bool ClientKitApi::updateConnection(QString p, QString u, QString c)
{
bool changedSomething;
if (!p.isEmpty())
{
changedSomething = (mConnection.port == p);
mConnection.port = p;
}
if (!u.isEmpty())
{
changedSomething = (mConnection.user == u);
mConnection.user = u;
}
if (!c.isEmpty())
{
changedSomething = (mConnection.client == c);
mConnection.client = c;
}
return changedSomething;
}
// Create a new pointer that's exposed to the JS from P4ClientKit
//
// Allows JS to manage connections similar to the Perforce derived APIs
//
// todo: Perhaps return a object with error/success message?
//
// The reason that this call does not take arguments for the connection
// (which is safer) is because it's slightly less flexible to do it that way
//
QString ClientKitApi::createP4(const QString& newP4PointerName)
{
QString returnMessage;
// 1. Check to see if we already have a p4 pointer of this name
if (mMainWindow->p4PointersList().contains(newP4PointerName))
{
returnMessage="Error: p4 pointer name '"+newP4PointerName+"' already created";
return returnMessage;
}
if (mMainWindow->p4PointersList().count() > 1000)
{
returnMessage="Error: Exceeds allowable number of p4 pointers (1000)"; // 1000 might be too many
return returnMessage;
}
// create and expose the new pointer
mMainWindow->createClientKitApi(newP4PointerName);
returnMessage="Success: p4 pointer name '"+newP4PointerName+"' created";
return returnMessage;
}
//
// 1. A callback for AJAX style calling needs to be added to run()
// e.g. p4.run("info","",callbackFunction);
//
// 1a. Run the commands in a different thread so we can implement non-blocking
// callbacks (AJAX)
//
QVariant ClientKitApi::run( const QString& command)
{
QVariantList args;
return run(command, args);
}
// Convenience override
QVariant ClientKitApi::run(const QVariantList& cmdTokens)
{
if (cmdTokens.isEmpty())
return QVariant();
QVariantList tokens = cmdTokens;
QString cmd = tokens.takeFirst().toString();
return run(cmd, tokens);
}
// Main p4.run() function
QVariant ClientKitApi::run(const QString& command, const QVariantList& args)
{
ClientApi client;
Connection con = connection();
if (con.isEmpty())
{
QVariantMap errorMap;
errorMap.insert("Error", "No connection defined");
return errorMap;
}
client.SetPort( con.port.toLatin1() ); // todo: Don't limit to Latin1 charset
client.SetUser( con.user.toLatin1() );
client.SetClient( con.client.toLatin1() );
if (!con.charset.toLatin1().isEmpty())
client.SetCharset( con.charset.toLatin1() );
client.SetProtocol("tag", ""); // Pretty much always use tagged. There are a few exceptions like diff2
Error e;
client.Init( &e );
int numArgs = args.count();
HClientUser ui(this);
QByteArray ba0;
QByteArray ba1;
QByteArray ba2;
QByteArray ba3;
QByteArray ba4;
QByteArray ba5;
char* arg0;
char* arg1;
char* arg2;
char* arg3;
char* arg4;
char* arg5;
if (numArgs > 0) //todo: process more than 6 args
{
ba0 = args[0].toString().toLatin1();
arg0 = ba0.data();
if (numArgs > 1)
{
ba1 = args[1].toString().toLatin1();
arg1 = ba1.data();
if (numArgs > 2)
{
ba2 = args[2].toString().toLatin1();
arg2 = ba2.data();
if (numArgs > 3)
{
ba3 = args[3].toString().toLatin1();
arg3 = ba3.data();
if (numArgs > 4)
{
ba4 = args[4].toString().toLatin1();
arg4 = ba4.data();
if (numArgs > 5)
{
ba5 = args[5].toString().toLatin1();
arg5 = ba5.data();
if (numArgs > 6)
{
mErrorList += "Warning: Exceeded maximum number of processed arguments";
}
}
}
}
}
}
}
char *argv[] = { arg0, arg1, arg2, arg3, arg4, arg5 };
int argc = numArgs;
client.SetArgv( argc, argv );
ui.clearOutputMap();
mErrorList.clear();
client.Run( command.toLocal8Bit(), &ui );
client.Final( &e );
QVariantList infoList = ui.getOutputList();
QVariantMap returnMap;
returnMap.insert("size", infoList.count());
returnMap.insert("data", infoList);
if (mErrorList.count() > 0)
{
returnMap.insert("error", mErrorList);
}
return returnMap; // returning a QMap is the magic that causes a JavaScript object to appear in the JavaScript code
}
//
// 1. todo: Design a security feature to restrict where and what types of files can be written by the JavaScript
//
bool ClientKitApi::createFile(const QString& filename, const QString& fileContents)
{
// add security check
QString fName = filename;
int last = filename.lastIndexOf(QDir::separator());
//check for a directory pre-pended on the file, and create a directory if needed
if( last > -1 )
{
//separate the directory and create it
QString directory = filename.left(last);
QDir dir;
dir.mkdir(directory);
dir.setCurrent(directory);
//separate the file name
fName = filename.right(filename.length() - (last + 1));
}
QFile file(fName);
if (file.open(QFile::ReadWrite))
{
QByteArray fileArray = fileContents.toAscii();
file.write(fileArray);
file.close();
return true;
}
return false;
}
// todo: Change so this does not set the configuration on a "get" method
// Because we don't hang on the connection, we don't have a client api to
// read this from.
//
QString ClientKitApi::getConfig()
{
ClientApi client;
Connection con = connection();
if (con.isEmpty())
{
return "No connection defined";
}
client.SetPort( con.port.toLatin1() );
client.SetUser( con.user.toLatin1() );
client.SetClient( con.client.toLatin1() );
client.SetProtocol("tag", ""); // Pretty much always use tagged. There are a few exceptions like diff2
Error e;
client.Init( &e );
qDebug(client.GetConfig().Text());
return client.GetConfig().Text();
}
//
// 1. todo: Check to see if this is the desired way to read a file. If translation
// from 8 bit into 16 bit Unicode is desired, should use QTextStream instead
//
QVariant ClientKitApi::readFile(const QString& fname)
{
QVariantList retList;
QFile file(fname);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
retList.append("Could not read file '" + fname + "'");
return retList;
}
while (!file.atEnd())
{
QByteArray line = file.readLine();
retList.append(line);
}
QVariant retVar(retList);
return retVar;
}
// Returns list of all files and directories in ClientKitApi's current directory
//
// ls() of an empty string returns the drives
//
// Although it perhaps seems awkward to force the JS to include the dir name
// (rather than having a target dir) it does keep behavior out of the C++ container
//
// 1. todo: Could expose more powerful entryList methods (file filters, sorting) if needed
//
QVariant ClientKitApi::ls(const QString& dirname)
{
return ls(dirname, false);
}
QVariant ClientKitApi::ls(const QString& dirname, bool fileInfo)
{
if (fileInfo)
{
if (dirname.isEmpty())
{
QString returnMessage = "Error: Please include a local directory path.";
return returnMessage;
}
else
{
QDir dir(dirname);
QFileInfoList slist = dir.entryInfoList();
QVariantList outputList;
for (int i=2; i < slist.count(); i++)
{
QVariantMap outputMap;
if (slist[i].isDir())
outputMap.insert("type","dir");
else if (slist[i].isFile())
outputMap.insert("type","file");
outputMap.insert("path",slist[i].canonicalFilePath());
outputMap.insert("isHidden",slist[i].isHidden());
outputList.append(outputMap);
}
return outputList;
}
}
else
{
QStringList slist;
if (dirname.isEmpty())
{
QFileInfoList drives = QDir::drives();
for (int i = 0; i < drives.size(); ++i)
{
QString drive = drives.at(i).absoluteFilePath();
slist.append(drive);
}
}
else
{
QDir dir(dirname);
slist = dir.entryList();
}
QVariant retVar(slist);
return retVar;
}
}
QVariant ClientKitApi::currentDir()
{
QDir cdir = QDir::current();
return cdir.absolutePath();
}
QVariant ClientKitApi::applicationPath()
{
QString appDirPath = QCoreApplication::applicationDirPath();
return QVariant(appDirPath);
}
/////////////////////////////// Container API ///////////////////////////////////////////////
void ClientKitApi::setWindowSize(const int& x, const int& y)
{
mMainWindow->resize(x, y);
}
void ClientKitApi::setWindowStyle(const QString& styleName)
{
mMainWindow->setWindowStyle(styleName);
}
QVariant ClientKitApi::getWindowSize()
{
QVariantMap size;
size.insert("width", mMainWindow->size().width());
size.insert("height", mMainWindow->size().height());
return QVariant(size);
}
QString ClientKitApi::getWindowStyle()
{
return mMainWindow->getWindowStyle();
}
QStringList ClientKitApi::getWindowStyles()
{
return mMainWindow->getWindowStyles();
}
void ClientKitApi::setWindowTitle(const QString & windowTitle)
{
mMainWindow->setWindowTitle(windowTitle);
}
QVariant ClientKitApi::getSystemLocale()
{
QLocale sl = QLocale::system();
QLocale::Language lang = sl.language();
QString langStr = QLocale::languageToString(lang);
QString locStr = sl.name();
QVariantMap retMap;
retMap.insert("Locale Name", locStr);
retMap.insert("Language Name", langStr);
retMap.insert("Language id", lang);
return QVariant(retMap);
}
//
// This is even more dangerous than createFile!
//
// Definitely add security, either by defining a whitelist, or prompting user
//
// Most users should choose to comment out this function
//
QString ClientKitApi::startProcess(const QString& program, const QStringList& arguments)
{
QProcess *process = new QProcess();
process->setProcessChannelMode(QProcess::MergedChannels);
process->start(program, arguments);
if( !process->waitForStarted() )
return "process failed to start";
if(!process->waitForFinished())
return "process failed to finish";
return process->readAll();
}
// This is useful for opening, e.g., a help page (opens in system default browser)
bool ClientKitApi::openUrlInBrowser(QString filepath)
{
return QDesktopServices::openUrl(QUrl(filepath));
}