1/*
2 This file is part of the kblog library.
3
4 Copyright (c) 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
5 Copyright (c) 2006-2007 Christian Weilbach <christian_weilbach@web.de>
6 Copyright (c) 2007-2008 Mike McQuaid <mike@mikemcquaid.com>
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Library General Public
10 License as published by the Free Software Foundation; either
11 version 2 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Library General Public License for more details.
17
18 You should have received a copy of the GNU Library General Public License
19 along with this library; see the file COPYING.LIB. If not, write to
20 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 Boston, MA 02110-1301, USA.
22*/
23
24#include "blogger1.h"
25#include "blogger1_p.h"
26#include "blogpost.h"
27
28#include <kxmlrpcclient/client.h>
29
30#include <KDebug>
31#include <KDateTime>
32#include <KLocalizedString>
33
34#include <QList>
35
36#include <QStringList>
37
38using namespace KBlog;
39
40Blogger1::Blogger1( const KUrl &server, QObject *parent )
41 : Blog( server, *new Blogger1Private, parent )
42{
43 kDebug();
44 setUrl( server );
45}
46
47Blogger1::Blogger1( const KUrl &server, Blogger1Private &dd, QObject *parent )
48 : Blog( server, dd, parent )
49{
50 kDebug();
51 setUrl( server );
52}
53
54Blogger1::~Blogger1()
55{
56 kDebug();
57}
58
59QString Blogger1::interfaceName() const
60{
61 return QLatin1String( "Blogger 1.0" );
62}
63
64void Blogger1::setUrl( const KUrl &server )
65{
66 Q_D( Blogger1 );
67 Blog::setUrl( server );
68 delete d->mXmlRpcClient;
69 d->mXmlRpcClient = new KXmlRpc::Client( server );
70 d->mXmlRpcClient->setUserAgent( userAgent() );
71}
72
73void Blogger1::fetchUserInfo()
74{
75 Q_D( Blogger1 );
76 kDebug() << "Fetch user's info...";
77 QList<QVariant> args( d->blogger1Args() );
78 d->mXmlRpcClient->call(
79 QLatin1String("blogger.getUserInfo"), args,
80 this, SLOT(slotFetchUserInfo(QList<QVariant>,QVariant)),
81 this, SLOT(slotError(int,QString,QVariant)) );
82}
83
84void Blogger1::listBlogs()
85{
86 Q_D( Blogger1 );
87 kDebug() << "Fetch List of Blogs...";
88 QList<QVariant> args( d->blogger1Args() );
89 d->mXmlRpcClient->call(
90 QLatin1String("blogger.getUsersBlogs"), args,
91 this, SLOT(slotListBlogs(QList<QVariant>,QVariant)),
92 this, SLOT(slotError(int,QString,QVariant)) );
93}
94
95void Blogger1::listRecentPosts( int number )
96{
97 Q_D( Blogger1 );
98 kDebug() << "Fetching List of Posts...";
99 QList<QVariant> args( d->defaultArgs( blogId() ) );
100 args << QVariant( number );
101 d->mXmlRpcClient->call(
102 d->getCallFromFunction( Blogger1Private::GetRecentPosts ), args,
103 this, SLOT(slotListRecentPosts(QList<QVariant>,QVariant)),
104 this, SLOT(slotError(int,QString,QVariant)),
105 QVariant( number ) );
106}
107
108void Blogger1::fetchPost( KBlog::BlogPost *post )
109{
110 if ( !post ) {
111 kError() << "Blogger1::modifyPost: post is null pointer";
112 return;
113 }
114
115 Q_D( Blogger1 );
116 kDebug() << "Fetching Post with url" << post->postId();
117 QList<QVariant> args( d->defaultArgs( post->postId() ) );
118 unsigned int i= d->mCallCounter++;
119 d->mCallMap[ i ] = post;
120 d->mXmlRpcClient->call(
121 d->getCallFromFunction( Blogger1Private::FetchPost ), args,
122 this, SLOT(slotFetchPost(QList<QVariant>,QVariant)),
123 this, SLOT(slotError(int,QString,QVariant)),
124 QVariant( i ) );
125}
126
127void Blogger1::modifyPost( KBlog::BlogPost *post )
128{
129 Q_D( Blogger1 );
130
131 if ( !post ) {
132 kError() << "Blogger1::modifyPost: post is null pointer";
133 return;
134 }
135
136 kDebug() << "Uploading Post with postId" << post->postId();
137 unsigned int i= d->mCallCounter++;
138 d->mCallMap[ i ] = post;
139 QList<QVariant> args( d->defaultArgs( post->postId() ) );
140 d->readArgsFromPost( &args, *post );
141 d->mXmlRpcClient->call(
142 d->getCallFromFunction( Blogger1Private::ModifyPost ), args,
143 this, SLOT(slotModifyPost(QList<QVariant>,QVariant)),
144 this, SLOT(slotError(int,QString,QVariant)),
145 QVariant( i ) );
146}
147
148void Blogger1::createPost( KBlog::BlogPost *post )
149{
150 Q_D( Blogger1 );
151
152 if ( !post ) {
153 kError() << "Blogger1::createPost: post is null pointer";
154 return;
155 }
156
157 unsigned int i= d->mCallCounter++;
158 d->mCallMap[ i ] = post;
159 kDebug() << "Creating new Post with blogid" << blogId();
160 QList<QVariant> args( d->defaultArgs( blogId() ) );
161 d->readArgsFromPost( &args, *post );
162 d->mXmlRpcClient->call(
163 d->getCallFromFunction( Blogger1Private::CreatePost ), args,
164 this, SLOT(slotCreatePost(QList<QVariant>,QVariant)),
165 this, SLOT(slotError(int,QString,QVariant)),
166 QVariant( i ) );
167}
168
169void Blogger1::removePost( KBlog::BlogPost *post )
170{
171 Q_D( Blogger1 );
172
173 if ( !post ) {
174 kError() << "Blogger1::removePost: post is null pointer";
175 return;
176 }
177
178 unsigned int i = d->mCallCounter++;
179 d->mCallMap[ i ] = post;
180 kDebug() << "Blogger1::removePost: postId=" << post->postId();
181 QList<QVariant> args( d->blogger1Args( post->postId() ) );
182 args << QVariant( true ); // Publish must be set to remove post.
183 d->mXmlRpcClient->call(
184 QLatin1String("blogger.deletePost"), args,
185 this, SLOT(slotRemovePost(QList<QVariant>,QVariant)),
186 this, SLOT(slotError(int,QString,QVariant)),
187 QVariant( i ) );
188}
189
190Blogger1Private::Blogger1Private() :
191mXmlRpcClient(0)
192{
193 kDebug();
194 mCallCounter = 1;
195}
196
197Blogger1Private::~Blogger1Private()
198{
199 kDebug();
200 delete mXmlRpcClient;
201}
202
203QList<QVariant> Blogger1Private::defaultArgs( const QString &id )
204{
205 kDebug();
206 Q_Q ( Blogger1 );
207 QList<QVariant> args;
208 args << QVariant( QLatin1String( "0123456789ABCDEF" ) );
209 if ( !id.isEmpty() ) {
210 args << QVariant( id );
211 }
212 args << QVariant( q->username() )
213 << QVariant( q->password() );
214 return args;
215}
216
217// reimplemenet defaultArgs, since we may not use it virtually everywhere
218QList<QVariant> Blogger1Private::blogger1Args( const QString &id )
219{
220 kDebug();
221 Q_Q ( Blogger1 );
222 QList<QVariant> args;
223 args << QVariant( QLatin1String( "0123456789ABCDEF" ) );
224 if ( !id.isEmpty() ) {
225 args << QVariant( id );
226 }
227 args << QVariant( q->username() )
228 << QVariant( q->password() );
229 return args;
230}
231
232void Blogger1Private::slotFetchUserInfo( const QList<QVariant> &result, const QVariant &id )
233{
234 Q_Q( Blogger1 );
235 Q_UNUSED( id );
236
237 kDebug();
238 kDebug() << "TOP:" << result[0].typeName();
239 QMap<QString,QString> userInfo;
240 if ( result[0].type() != QVariant::Map ) {
241 kError() << "Could not fetch user's info out of the result from the server,"
242 << "not a map.";
243 emit q->error( Blogger1::ParsingError,
244 i18n( "Could not fetch user's info out of the result "
245 "from the server, not a map." ) );
246 return;
247 }
248 const QMap<QString,QVariant> resultMap = result[0].toMap();
249 userInfo[QLatin1String("nickname")]=resultMap[QLatin1String("nickname")].toString();
250 userInfo[QLatin1String("userid")]=resultMap[QLatin1String("userid")].toString();
251 userInfo[QLatin1String("url")]=resultMap[QLatin1String("url")].toString();
252 userInfo[QLatin1String("email")]=resultMap[QLatin1String("email")].toString();
253 userInfo[QLatin1String("lastname")]=resultMap[QLatin1String("lastname")].toString();
254 userInfo[QLatin1String("firstname")]=resultMap[QLatin1String("firstname")].toString();
255
256 emit q->fetchedUserInfo( userInfo );
257}
258
259void Blogger1Private::slotListBlogs( const QList<QVariant> &result, const QVariant &id )
260{
261 Q_Q( Blogger1 );
262 Q_UNUSED( id );
263
264 kDebug();
265 kDebug() << "TOP:" << result[0].typeName();
266 QList<QMap<QString,QString> > blogsList;
267 if ( result[0].type() != QVariant::List ) {
268 kError() << "Could not fetch blogs out of the result from the server,"
269 << "not a list.";
270 emit q->error( Blogger1::ParsingError,
271 i18n( "Could not fetch blogs out of the result "
272 "from the server, not a list." ) );
273 return;
274 }
275 const QList<QVariant> posts = result[0].toList();
276 QList<QVariant>::ConstIterator it = posts.begin();
277 QList<QVariant>::ConstIterator end = posts.end();
278 for ( ; it != end; ++it ) {
279 kDebug() << "MIDDLE:" << ( *it ).typeName();
280 const QMap<QString, QVariant> postInfo = ( *it ).toMap();
281 QMap<QString,QString> blogInfo;
282 blogInfo[ QLatin1String("id") ] = postInfo[QLatin1String("blogid")].toString();
283 blogInfo[ QLatin1String("url") ] = postInfo[QLatin1String("url")].toString();
284 blogInfo[ QLatin1String("apiUrl") ] = postInfo[QLatin1String("xmlrpc")].toString();
285 blogInfo[ QLatin1String("title") ] = postInfo[QLatin1String("blogName")].toString();
286 kDebug() << "Blog information retrieved: ID =" << blogInfo[QLatin1String("id")]
287 << ", Name =" << blogInfo[QLatin1String("title")];
288 blogsList << blogInfo;
289 }
290 emit q->listedBlogs( blogsList );
291}
292
293void Blogger1Private::slotListRecentPosts( const QList<QVariant> &result, const QVariant &id )
294{
295 Q_Q( Blogger1 );
296 int count = id.toInt(); // not sure if needed, actually the API should
297// not give more posts
298
299 kDebug();
300 kDebug() << "TOP:" << result[0].typeName();
301
302 QList <BlogPost> fetchedPostList;
303
304 if ( result[0].type() != QVariant::List ) {
305 kError() << "Could not fetch list of posts out of the"
306 << "result from the server, not a list.";
307 emit q->error( Blogger1::ParsingError,
308 i18n( "Could not fetch list of posts out of the result "
309 "from the server, not a list." ) );
310 return;
311 }
312 const QList<QVariant> postReceived = result[0].toList();
313 QList<QVariant>::ConstIterator it = postReceived.begin();
314 QList<QVariant>::ConstIterator end = postReceived.end();
315 for ( ; it != end; ++it ) {
316 BlogPost post;
317 kDebug() << "MIDDLE:" << ( *it ).typeName();
318 const QMap<QString, QVariant> postInfo = ( *it ).toMap();
319 if ( readPostFromMap( &post, postInfo ) ) {
320 kDebug() << "Post with ID:"
321 << post.postId()
322 << "appended in fetchedPostList";
323 post.setStatus( BlogPost::Fetched );
324 fetchedPostList.append( post );
325 } else {
326 kError() << "readPostFromMap failed!";
327 emit q->error( Blogger1::ParsingError, i18n( "Could not read post." ) );
328 }
329 if ( --count == 0 ) {
330 break;
331 }
332 }
333 kDebug() << "Emitting listRecentPostsFinished()";
334 emit q->listedRecentPosts( fetchedPostList );
335}
336
337void Blogger1Private::slotFetchPost( const QList<QVariant> &result, const QVariant &id )
338{
339 Q_Q( Blogger1 );
340 kDebug();
341
342 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
343 mCallMap.remove( id.toInt() );
344
345 //array of structs containing ISO.8601
346 // dateCreated, String userid, String postid, String content;
347 // TODO: Time zone for the dateCreated!
348 kDebug () << "TOP:" << result[0].typeName();
349 if ( result[0].type() == QVariant::Map &&
350 readPostFromMap( post, result[0].toMap() ) ) {
351 kDebug() << "Emitting fetchedPost()";
352 post->setStatus( KBlog::BlogPost::Fetched );
353 emit q->fetchedPost( post );
354 } else {
355 kError() << "Could not fetch post out of the result from the server.";
356 post->setError( i18n( "Could not fetch post out of the result from the server." ) );
357 post->setStatus( BlogPost::Error );
358 emit q->errorPost( Blogger1::ParsingError,
359 i18n( "Could not fetch post out of the result from the server." ), post );
360 }
361}
362
363void Blogger1Private::slotCreatePost( const QList<QVariant> &result, const QVariant &id )
364{
365 Q_Q( Blogger1 );
366 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
367 mCallMap.remove( id.toInt() );
368
369 kDebug();
370 //array of structs containing ISO.8601
371 // dateCreated, String userid, String postid, String content;
372 // TODO: Time zone for the dateCreated!
373 kDebug () << "TOP:" << result[0].typeName();
374 if ( result[0].type() != QVariant::String &&
375 result[0].type() != QVariant::Int ) {
376 kError() << "Could not read the postId, not a string or an integer.";
377 emit q->errorPost( Blogger1::ParsingError,
378 i18n( "Could not read the postId, not a string or an integer." ),
379 post );
380 return;
381 }
382 QString serverID;
383 if ( result[0].type() == QVariant::String ) {
384 serverID = result[0].toString();
385 } else if ( result[0].type() == QVariant::Int ) {
386 serverID = QString::fromLatin1( "%1" ).arg( result[0].toInt() );
387 }
388 post->setPostId( serverID );
389 post->setStatus( KBlog::BlogPost::Created );
390 kDebug() << "emitting createdPost()"
391 << "for title: \"" << post->title()
392 << "\" server id: " << serverID;
393 emit q->createdPost( post );
394}
395
396void Blogger1Private::slotModifyPost( const QList<QVariant> &result, const QVariant &id )
397{
398 Q_Q( Blogger1 );
399 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
400 mCallMap.remove( id.toInt() );
401
402 kDebug();
403 //array of structs containing ISO.8601
404 // dateCreated, String userid, String postid, String content;
405 // TODO: Time zone for the dateCreated!
406 kDebug() << "TOP:" << result[0].typeName();
407 if ( result[0].type() != QVariant::Bool &&
408 result[0].type() != QVariant::Int ) {
409 kError() << "Could not read the result, not a boolean.";
410 emit q->errorPost( Blogger1::ParsingError,
411 i18n( "Could not read the result, not a boolean." ),
412 post );
413 return;
414 }
415 post->setStatus( KBlog::BlogPost::Modified );
416 kDebug() << "emitting modifiedPost() for title: \""
417 << post->title() << "\"";
418 emit q->modifiedPost( post );
419}
420
421void Blogger1Private::slotRemovePost( const QList<QVariant> &result, const QVariant &id )
422{
423 Q_Q( Blogger1 );
424 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
425 mCallMap.remove( id.toInt() );
426
427 kDebug() << "slotRemovePost";
428 //array of structs containing ISO.8601
429 // dateCreated, String userid, String postid, String content;
430 // TODO: Time zone for the dateCreated!
431 kDebug() << "TOP:" << result[0].typeName();
432 if ( result[0].type() != QVariant::Bool &&
433 result[0].type() != QVariant::Int ) {
434 kError() << "Could not read the result, not a boolean.";
435 emit q->errorPost( Blogger1::ParsingError,
436 i18n( "Could not read the result, not a boolean." ),
437 post );
438 return;
439 }
440 post->setStatus( KBlog::BlogPost::Removed );
441 kDebug() << "emitting removedPost()";
442 emit q->removedPost( post );
443}
444
445void Blogger1Private::slotError( int number,
446 const QString &errorString,
447 const QVariant &id )
448{
449 Q_Q( Blogger1 );
450 Q_UNUSED( number );
451 kDebug() << "An error occurred: " << errorString;
452 BlogPost *post = mCallMap[ id.toInt() ];
453
454 if ( post )
455 emit q->errorPost( Blogger1::XmlRpc, errorString, post );
456 else
457 emit q->error( Blogger1::XmlRpc, errorString );
458}
459
460bool Blogger1Private::readPostFromMap(
461 BlogPost *post, const QMap<QString, QVariant> &postInfo )
462{
463 // FIXME: integrate error handling
464 if ( !post ) {
465 return false;
466 }
467 QStringList mapkeys = postInfo.keys();
468 kDebug() << endl << "Keys:" << mapkeys.join( QLatin1String(", ") );
469 kDebug() << endl;
470
471 KDateTime dt( postInfo[QLatin1String("dateCreated")].toDateTime(), KDateTime::UTC );
472 if ( dt.isValid() && !dt.isNull() ) {
473 post->setCreationDateTime( dt.toLocalZone() );
474 }
475 dt = KDateTime ( postInfo[QLatin1String("lastModified")].toDateTime(), KDateTime::UTC );
476 if ( dt.isValid() && !dt.isNull() ) {
477 post->setModificationDateTime( dt.toLocalZone() );
478 }
479 post->setPostId( postInfo[QLatin1String("postid")].toString().isEmpty() ? postInfo[QLatin1String("postId")].toString() :
480 postInfo[QLatin1String("postid")].toString() );
481
482 QString title( postInfo[QLatin1String("title")].toString() );
483 //QString description( postInfo["description"].toString() );
484 QString contents;
485 if ( postInfo[QLatin1String("content")].type() == QVariant::ByteArray ) {
486 QByteArray tmpContent = postInfo[QLatin1String("content")].toByteArray();
487 contents = QString::fromUtf8( tmpContent.data(), tmpContent.size() );
488 } else {
489 contents = postInfo[QLatin1String("content")].toString();
490 }
491 QStringList category;
492
493 // Check for hacked title/category support (e.g. in Wordpress)
494 QRegExp titleMatch = QRegExp( QLatin1String("<title>([^<]*)</title>") );
495 QRegExp categoryMatch = QRegExp( QLatin1String("<category>([^<]*)</category>") );
496 if ( contents.indexOf( titleMatch ) != -1 ) {
497 // Get the title value from the regular expression match
498 title = titleMatch.cap( 1 );
499 }
500 if ( contents.indexOf( categoryMatch ) != -1 ) {
501 // Get the category value from the regular expression match
502 category = categoryMatch.capturedTexts();
503 }
504 contents.remove( titleMatch );
505 contents.remove( categoryMatch );
506
507 post->setTitle( title );
508 post->setContent( contents );
509 post->setCategories( category );
510 return true;
511}
512
513bool Blogger1Private::readArgsFromPost( QList<QVariant> *args, const BlogPost &post )
514{
515 if ( !args ) {
516 return false;
517 }
518 const QStringList categories = post.categories();
519 QString content = QLatin1String("<title>") + post.title() + QLatin1String("</title>");
520 QStringList::const_iterator it;
521 QStringList::const_iterator end(categories.constEnd());
522 for ( it = categories.constBegin(); it != end; ++it ) {
523 content += QLatin1String("<category>") + *it + QLatin1String("</category>");
524 }
525 content += post.content();
526 *args << QVariant( content );
527 *args << QVariant( !post.isPrivate() );
528 return true;
529}
530
531QString Blogger1Private::getCallFromFunction( FunctionToCall type )
532{
533 switch ( type ) {
534 case GetRecentPosts: return QLatin1String("blogger.getRecentPosts");
535 case CreatePost: return QLatin1String("blogger.newPost");
536 case ModifyPost: return QLatin1String("blogger.editPost");
537 case FetchPost: return QLatin1String("blogger.getPost");
538 default: return QString();
539 }
540}
541
542#include "moc_blogger1.cpp"
543