/* ============================================================ * * This file is a part of kipi-plugins project * http://www.kipi-plugins.org * * Date : 2007-16-07 * Description : a kipi plugin to export images to Picasa web service * * Copyright (C) 2007-2008 by Vardhman Jain * Copyright (C) 2008 by Gilles Caulier * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ // C++ includes. #include #include #include #include // TQt includes. #include #include #include #include #include #include #include #include #include #include // KDE includes. #include #include #include #include #include #include #include #include #include #include // LibKExiv2 includes. #include // LibKDcraw includes. #include #include #if KDCRAW_VERSION < 0x000106 #include #endif // Local includes. #include "pluginsversion.h" #include "mpform.h" #include "picasawebitem.h" #include "picasawebtalker.h" #include "picasawebwindow.h" #include "picasaweblogin.h" #include "picasawebtalker.moc" class PicasawebLogin; namespace KIPIPicasawebExportPlugin { PicasawebTalker::PicasawebTalker( TQWidget* parent ) : m_parent( parent ), m_job( 0 ) { m_apikey="49d585bafa0758cb5c58ab67198bf632"; m_secret="34b39925e6273ffd"; connect(this, TQT_SIGNAL(signalError(const TQString&)), this, TQT_SLOT(slotError(const TQString&))); authProgressDlg=new TQProgressDialog(); } PicasawebTalker::~PicasawebTalker() { if (m_job) m_job->kill(); } TQString PicasawebTalker::getApiSig(TQString secret, TQStringList headers) { TQStringList compressed ;//= new List(headers.Length); for ( TQStringList::Iterator it = headers.begin(); it != headers.end(); ++it ) { TQStringList str=TQStringList::split("=",(*it)); compressed.append(str[0].stripWhiteSpace()+str[1].stripWhiteSpace()); } compressed.sort(); TQString merged=compressed.join(""); TQString final = secret + merged; const char *test=final.ascii(); KMD5 context (test); //kdDebug()<< "Test Hex Digest output: " << context.hexDigest().data() << endl; return context.hexDigest().data(); } void PicasawebTalker::getToken(const TQString& username, const TQString& password ) { if (m_job) { m_job->kill(); m_job = 0; } TQString url = "https://www.google.com/accounts/ClientLogin"; PicasawebLogin *loginDialog = new PicasawebLogin(TQT_TQWIDGET(kapp->activeWindow()), TQString("LoginWindow"), username, password); /*if (username!=NULL && username.length() > 0){ // kdDebug()<<"Showing stored username"<< username << endl; loginDialog->setUsername(username); if (password != NULL && password.length() > 0){ // kdDebug()<<"Showing stored password"<< password << endl; loginDialog->setPassword(password); // kdDebug()<<"Showing stored password"<< password << endl; } } */ TQString username_edit, password_edit; if (!loginDialog) { kdDebug()<<" Out of memory error "<< endl; } if (loginDialog->exec() == TQDialog::Accepted) { username_edit = loginDialog->username(); password_edit = loginDialog->password(); } else { //Return something which say authentication needed. return ; } m_username = username_edit; username_edit = username; TQString accountType = "GOOGLE"; if (!(username_edit.endsWith("@gmail.com"))) username_edit += "@gmail.com"; TQByteArray buffer; TQStringList qsl; qsl.append("Email="+username_edit); qsl.append("Passwd="+password_edit); qsl.append("accountType="+accountType); qsl.append("service=lh2"); qsl.append("source=kipi-picasaweb-client"); TQString dataParameters = qsl.join("&"); TQTextStream ts(buffer, IO_Append|IO_WriteOnly); ts.setEncoding(TQTextStream::UnicodeUTF8); ts << dataParameters; TDEIO::TransferJob* job = TDEIO::http_post(url, buffer, false); job->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded" ); m_state = FE_GETTOKEN; authProgressDlg->setLabelText(i18n("Getting the token")); connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_job = job; m_buffer.resize(0); emit signalBusy( true ); } void PicasawebTalker::authenticate(const TQString& token, const TQString& username, const TQString& password) { if (!token || token.length() < 1) { checkToken(token); m_username = username; m_password = password; //this would be needed if the checktoken failed //we would need to reauthenticate using auth } else { getToken(username, password); } } void PicasawebTalker::checkToken(const TQString& /*token*/) { if (m_job) { m_job->kill(); m_job = 0; } TQString url = "https://www.google.com/accounts/ClientLogin"; TQString auth_string = "GoogleLogin auth=" + m_token; TQByteArray tmp; TDEIO::TransferJob* job = TDEIO::http_post(url, tmp, false); job->addMetaData("customHTTPHeader", "Authorization: " + auth_string); job->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded"); connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_state = FE_CHECKTOKEN; authProgressDlg->setLabelText(i18n("Checking if previous token is still valid")); authProgressDlg->setProgress(1,4); m_job = job; m_buffer.resize(0); emit signalBusy( true ); } /** PicasaWeb's Album listing request/response * First a request is sent to the url below and then we might(?) get a redirect URL * WE then need to send the GET request to the Redirect url (this however gets taken care off by the * TDEIO libraries. * This uses the authenticated album list fetching to get all the albums included the unlisted-albums * which is not returned for an unauthorised request as done without the Authorization header. */ void PicasawebTalker::listAllAlbums() { if (m_job) { m_job->kill(); m_job = 0; } TQString url = "http://picasaweb.google.com/data/feed/api/user/" + m_username + "?kind=album"; TQByteArray tmp; TQString auth_string = "GoogleLogin auth=" + m_token; TDEIO::TransferJob* job = TDEIO::get(url, !tmp.isNull(), false); job->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded" ); job->addMetaData("customHTTPHeader", "Authorization: " + auth_string ); connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_state = FE_LISTALBUMS; m_job = job; m_buffer.resize(0); emit signalBusy( true ); } void PicasawebTalker::getPhotoProperty(const TQString& method,const TQString& argList) { if (m_job) { m_job->kill(); m_job = 0; } TQString url="http://www.picasaweb.com/services/rest/?"; TQStringList headers; headers.append("api_key="+ m_apikey); headers.append("method="+method); headers.append("frob="+ m_frob); headers.append(argList); TQString md5=getApiSig(m_secret,headers); headers.append("api_sig="+ md5); TQString queryStr=headers.join("&"); TQString postUrl=url+queryStr; TQByteArray tmp; TDEIO::TransferJob* job = TDEIO::http_post(postUrl, tmp, false); job->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded" ); connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_state = FE_GETPHOTOPROPERTY; m_job = job; m_buffer.resize(0); emit signalBusy( true ); //authProgressDlg->setLabelText("Getting the Token from the server"); //authProgressDlg->setProgress(3,4); } void PicasawebTalker::addPhotoTag(const TQString& photoURI, const TQString& tag) { //if (m_job && m_state != FE_ADDTAG){ //we shouldn't kill the old tag request // m_job->kill(); // m_job = 0; //} TQString addTagXML = TQString(" " "%1 " " " "").arg(tag); TQString postUrl = TQString("%1").arg(photoURI); TQByteArray buffer; TQTextStream ts(buffer, IO_Append|IO_WriteOnly); ts.setEncoding(TQTextStream::UnicodeUTF8); ts << addTagXML; TQString auth_string = "GoogleLogin auth=" + m_token; TDEIO::TransferJob* job = TDEIO::http_post(postUrl, buffer, false); job->addMetaData("content-type", "Content-Type: application/atom+xml"); job->addMetaData("content-length", TQString("Content-Length: %1").arg(addTagXML.length())); job->addMetaData("customHTTPHeader", "Authorization: " + auth_string ); //connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), // this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_state = FE_ADDTAG; m_job = job; m_buffer.resize(0); emit signalBusy(true); } void PicasawebTalker::listPhotos(const TQString& /*albumName*/) { // TODO } void PicasawebTalker::createAlbum(const TQString& albumTitle, const TQString& albumDesc, const TQString& location, uint timestamp, const TQString& access, const TQString& media_keywords, bool isCommentsEnabled) { if (m_job) { m_job->kill(); m_job = 0; } TQString newAlbumXML = TQString(" " "%1 " "%2 " "%3 " "%4 " "%5 " "%6 " " " "%7 " " " " " " ").arg(albumTitle) .arg(albumDesc) .arg(location) .arg(access) .arg(isCommentsEnabled==true?"true":"false") .arg(timestamp) .arg(media_keywords); TQByteArray buffer; TQTextStream ts(buffer, IO_Append|IO_WriteOnly); ts.setEncoding(TQTextStream::UnicodeUTF8); ts << newAlbumXML; MPForm form; TQString postUrl = "http://www.picasaweb.google.com/data/feed/api/user/" + m_username ; TQString auth_string = "GoogleLogin auth=" + m_token; TDEIO::TransferJob* job = TDEIO::http_post(postUrl, buffer, false); job->addMetaData("content-type", "Content-Type: application/atom+xml"); job->addMetaData("content-length", TQString("Content-Length: %1").arg(newAlbumXML.length())); job->addMetaData("customHTTPHeader", "Authorization: " + auth_string ); connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_state = FE_CREATEALBUM; m_job = job; m_buffer.resize(0); emit signalBusy(true); } bool PicasawebTalker::addPhoto(const TQString& photoPath, FPhotoInfo& info, const TQString& albumId, bool rescale, int maxDim, int imageQuality) { // Disabling this totally may be checking the m_state and doing selecting // disabling is a better idea /*if (m_job) { m_job->kill(); m_job = 0; }*/ TQString album_id = albumId; if (album_id.length() == 0) album_id = "test"; TQString postUrl = "http://www.picasaweb.google.com/data/feed/api/user/" + KURL::encode_string(m_username) + "/albumid/" + album_id; TQString path = postUrl; TQStringList headers; MPForm form; TQString auth_string = "GoogleLogin auth=" + m_token; //form.addPair("Authorization", auth_string); //Create the Body in atom-xml TQStringList body_xml; body_xml.append(""); body_xml.append(""+ info.title +""); body_xml.append(""+ info.description +""); body_xml.append(""); body_xml.append(""); TQString body = body_xml.join(""); form.addPair("test", body, "application/atom+xml"); // save the tags for this photo in to the tags hashmap tags_map.insert(info.title, info.tags); TQImage image; // Check if RAW file. #if KDCRAW_VERSION < 0x000106 TQString rawFilesExt(KDcrawIface::DcrawBinary::instance()->rawFiles()); #else TQString rawFilesExt(KDcrawIface::KDcraw::rawFiles()); #endif TQFileInfo fileInfo(photoPath); if (rawFilesExt.upper().contains(fileInfo.extension(false).upper())) KDcrawIface::KDcraw::loadDcrawPreview(image, photoPath); else image.load(photoPath); if (!image.isNull()) { path = locateLocal("tmp", TQFileInfo(photoPath).baseName().stripWhiteSpace() + ".jpg"); if (rescale && (image.width() > maxDim || image.height() > maxDim)) image = image.smoothScale(maxDim, maxDim, TQ_ScaleMin); image.save(path, "JPEG", imageQuality); // Restore all metadata. KExiv2Iface::KExiv2 exiv2Iface; if (exiv2Iface.load(photoPath)) { exiv2Iface.setImageProgramId(TQString("Kipi-plugins"), TQString(kipiplugins_version)); exiv2Iface.setImageDimensions(image.size()); exiv2Iface.save(path); } else { kdWarning(51000) << "(picasawebExport::Image doesn't have exif data)" << endl; } kdDebug() << "Resizing and saving to temp file: " << path << endl; } if (!form.addFile("photo", path)) return false; form.finish(); TDEIO::TransferJob* job = TDEIO::http_post(postUrl, form.formData(), false); job->addMetaData("content-type", form.contentType()); job->addMetaData("customHTTPHeader", "Authorization: " + auth_string ); connect(job, TQT_SIGNAL(data(TDEIO::Job*, const TQByteArray&)), this, TQT_SLOT(data(TDEIO::Job*, const TQByteArray&))); connect(job, TQT_SIGNAL(result(TDEIO::Job *)), this, TQT_SLOT(slotResult(TDEIO::Job *))); m_state = FE_ADDPHOTO; m_job = job; m_buffer.resize(0); emit signalBusy(true); return true; } TQString PicasawebTalker::getUserName() { return m_username; } TQString PicasawebTalker::getUserId() { return m_userId; } void PicasawebTalker::cancel() { if (m_job) { m_job->kill(); m_job = 0; } if (authProgressDlg && !authProgressDlg->isHidden()) authProgressDlg->hide(); } void PicasawebTalker::info(TDEIO::Job* /*job*/, const TQString& /*str*/) { } void PicasawebTalker::data(TDEIO::Job*, const TQByteArray& data) { if (data.isEmpty()) return; int oldSize = m_buffer.size(); m_buffer.resize(m_buffer.size() + data.size()); TQString output_data = TQString(data); memcpy(m_buffer.data()+oldSize, data.data(), data.size()); } void PicasawebTalker::slotError(const TQString & error) { TQString transError; int errorNo = 0; if (!error.isEmpty()) errorNo = atoi(error.latin1()); switch (errorNo) { case 2: transError=i18n("No photo specified");break; case 3: transError=i18n("General upload failure");break; case 4: transError=i18n("Filesize was zero");break; case 5: transError=i18n("Filetype was not recognised");break; case 6: transError=i18n("User exceeded upload limit");break; case 96: transError=i18n("Invalid signature"); break; case 97: transError=i18n("Missing signature"); break; case 98: transError=i18n("Login Failed / Invalid auth token"); break; case 100: transError=i18n("Invalid API Key"); break; case 105: transError=i18n("Service currently unavailable");break; case 108: transError=i18n("Invalid Frob");break; case 111: transError=i18n("Format \"xxx\" not found"); break; case 112: transError=i18n("Method \"xxx\" not found"); break; case 114: transError=i18n("Invalid SOAP envelope");break; case 115: transError=i18n("Invalid XML-RPC Method Call");break; case 116: transError=i18n("The POST method is now required for all setters"); break; default: transError=i18n("Unknown error"); }; KMessageBox::error(TQT_TQWIDGET(kapp->activeWindow()), i18n("Error Occured: %1\n We can not proceed further").arg(transError)); //kdDebug()<<"Not handling the error now will see it later"<error()) { if (m_state == FE_ADDPHOTO) { emit signalAddPhotoFailed(job->errorString()); } else { job->showErrorDialog(m_parent); } return; } switch(m_state) { case(FE_LOGIN): //parseResponseLogin(m_buffer); break; case(FE_CREATEALBUM): parseResponseCreateAlbum(m_buffer); break; case(FE_LISTALBUMS): parseResponseListAlbums(m_buffer); break; case(FE_GETFROB): break; case(FE_GETTOKEN): parseResponseGetToken(m_buffer); break; case(FE_CHECKTOKEN): parseResponseCheckToken(m_buffer); break; case(FE_GETAUTHORIZED): break; case(FE_LISTPHOTOS): parseResponseListPhotos(m_buffer); break; case(FE_GETPHOTOPROPERTY): parseResponsePhotoProperty(m_buffer); break; case(FE_ADDPHOTO): parseResponseAddPhoto(m_buffer); break; case(FE_ADDTAG): parseResponseAddTag(m_buffer); break; } } void PicasawebTalker::parseResponseCheckToken(const TQByteArray &data) { bool success = false; TQString errorString; TQString username; TQString transReturn(data); // If checktoken failed. // getToken ... if(transReturn.startsWith("Error=")) success = false; else success = true; if(!success) getToken(m_username, m_password); //emit signalError(errorString); } void PicasawebTalker::parseResponseGetToken(const TQByteArray &data) { bool success = false; TQString errorString; TQString str(data); //Check the response code should it be 200, proceed //if it is 403 handle the error mesg //figure out the auth string from this response if (str.find("Auth=")) { TQStringList strList = TQStringList::split("Auth=", str); m_token = strList[1]; success = 1; } if(success) { authProgressDlg->hide(); emit signalTokenObtained(m_token); } else { emit signalError(errorString); } emit signalBusy(false); } void PicasawebTalker::getHTMLResponseCode(const TQString& /*str*/) { } void PicasawebTalker::parseResponseListAlbums(const TQByteArray &data) { bool success = false; TQString str(data); TQDomDocument doc( "feed" ); if ( !doc.setContent( data ) ) { return; } TQDomElement docElem = doc.documentElement(); TQDomNode node = docElem.firstChild(); TQDomElement e; TQString feed_id, feed_updated, feed_title, feed_subtitle; TQString feed_icon_url, feed_link_url; TQString feed_username, feed_user_uri; TQString album_id, album_title, album_description; m_albumsList = new TQValueList (); while(!node.isNull()) { if (node.isElement() && node.nodeName() == "entry") { success = true; e = node.toElement(); TQDomNode details=e.firstChild(); PicasaWebAlbum fps; TQDomNode detailsNode = details; while(!detailsNode.isNull()) { if(detailsNode.isElement()) { if(detailsNode.nodeName() == "id") { // The node data is a URL of which album id is the string following the last / // like http://www.picasaweb.google.com/.../AlbumID TQString albumIdUrl = detailsNode.toElement().text(); int index = albumIdUrl.findRev("/"); int length = albumIdUrl.length(); TQString album_id = albumIdUrl.right(length - index - 1); fps.id = album_id; } if(detailsNode.nodeName() == "title") { album_title = "Not fetched"; if(detailsNode.toElement().attribute("type")=="text") album_title = detailsNode.toElement().text(); //this is what is obtained from data. fps.title = album_title; } if(detailsNode.nodeName()=="gphoto:name") { TQString name = detailsNode.toElement().text(); } } detailsNode = detailsNode.nextSibling(); } m_albumsList->append(fps); } node = node.nextSibling(); } if (!success) { emit signalGetAlbumsListFailed(i18n("Failed to fetch photoSets List")); m_albumsList = NULL; } else { emit signalGetAlbumsListSucceeded(); } } void PicasawebTalker::parseResponseListPhotos(const TQByteArray &data) { TQDomDocument doc( "getPhotosList" ); if ( !doc.setContent( data ) ) { return; } TQDomElement docElem = doc.documentElement(); TQDomNode node = docElem.firstChild(); //TQDomElement e; // TODO } void PicasawebTalker::parseResponseCreateAlbum(const TQByteArray &data) { bool success = false; TQString errorString; TQString response(data); TQDomDocument doc( "AddPhoto Response" ); // parse the new album name TQDomElement docElem = doc.documentElement(); TQString title, photo_id, album_id, photoURI; TQDomNode node = docElem.firstChild(); //this should mean TQDomElement e; while( !node.isNull() ) { if ( node.isElement()) { TQString node_name = node.nodeName(); TQString node_value = node.toElement().text(); if(node_name == "title") { success = true; title = node_value; } else if (node_name == "id") photoURI = node_value; else if(node_name == "gphoto:id") photo_id = node_value; else if(node_name == "gphoto:albumid") album_id = node_value; } node = node.nextSibling(); } // Raise a popup informing success } void PicasawebTalker::parseResponseAddTag(const TQByteArray &data) { TQString str(data); remaining_tags_count -= 1; emit signalBusy( false ); m_buffer.resize(0); if (remaining_tags_count == 0) emit signalAddPhotoSucceeded(); } void PicasawebTalker::parseResponseAddPhoto(const TQByteArray &data) { bool success = false; TQString line; TQString str(data); success = 1; TQDomDocument doc( "AddPhoto Response" ); if ( !doc.setContent( data ) ) { return; } TQDomElement docElem = doc.documentElement(); TQString title, photo_id, album_id, photoURI; TQDomNode node = docElem.firstChild(); //this should mean TQDomElement e; while( !node.isNull() ) { if ( node.isElement()) { TQString node_name = node.nodeName(); TQString node_value = node.toElement().text(); if(node_name == "title") { success = true; title = node_value; } else if (node_name == "id") photoURI = node_value; else if(node_name == "gphoto:id") photo_id = node_value; else if(node_name == "gphoto:albumid") album_id = node_value; } node = node.nextSibling(); } if (!success) { emit signalAddPhotoFailed(i18n("Failed to upload photo")); } else { // Update the tags information from the tags_map TQStringList tags = tags_map[title]; remaining_tags_count = tags.count(); if (tags.count() == 0) emit signalAddPhotoSucceeded(); for ( TQStringList::Iterator it = tags.begin(); it != tags.end(); ++it ) { TQString photoURI= TQString("http://picasaweb.google.com/data/feed/api/user/" "%1/albumid/%2/photoid/%3").arg(m_username).arg(album_id).arg(photo_id); addPhotoTag( photoURI, *it); } } } void PicasawebTalker::parseResponsePhotoProperty(const TQByteArray &data) { bool success = false; TQString line; TQDomDocument doc( "Photos Properties" ); if ( !doc.setContent( data ) ) { return; } TQDomElement docElem = doc.documentElement(); TQDomNode node = docElem.firstChild(); TQDomElement e; while( !node.isNull() ) { if ( node.isElement() && node.nodeName() == "photoid" ) { e = node.toElement(); // try to convert the node to an element. TQDomNode details=e.firstChild(); success=true; } if ( node.isElement() && node.nodeName() == "err" ) { kdDebug()<<"Checking Error in response"<