1/*
2 This file is part of the kblog library.
3
4 Copyright (c) 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
5 Copyright (c) 2006-2009 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 "movabletype.h"
25#include "movabletype_p.h"
26#include "blogpost.h"
27
28#include <kxmlrpcclient/client.h>
29#include <kio/job.h>
30
31#include <KDebug>
32#include <KLocalizedString>
33#include <KDateTime>
34
35#include <QtCore/QStringList>
36
37using namespace KBlog;
38
39MovableType::MovableType( const KUrl &server, QObject *parent )
40 : MetaWeblog( server, *new MovableTypePrivate, parent )
41{
42 kDebug();
43}
44
45MovableType::MovableType( const KUrl &server, MovableTypePrivate &dd,
46 QObject *parent )
47 : MetaWeblog( server, dd, parent )
48{
49 kDebug();
50}
51
52MovableType::~MovableType()
53{
54 kDebug();
55}
56
57QString MovableType::interfaceName() const
58{
59 return QLatin1String( "Movable Type" );
60}
61
62void MovableType::listRecentPosts( int number )
63{
64 Q_D( MovableType );
65 kDebug();
66 QList<QVariant> args( d->defaultArgs( blogId() ) );
67 args << QVariant( number );
68 d->mXmlRpcClient->call(
69 QLatin1String("metaWeblog.getRecentPosts"), args,
70 this, SLOT(slotListRecentPosts(QList<QVariant>,QVariant)),
71 this, SLOT(slotError(int,QString,QVariant)),
72 QVariant( number ) );
73}
74
75void MovableType::listTrackBackPings( KBlog::BlogPost *post )
76{
77 Q_D( MovableType );
78 kDebug();
79 QList<QVariant> args;
80 args << QVariant( post->postId() );
81 unsigned int i = d->mCallCounter++;
82 d->mCallMap[ i ] = post;
83 d->mXmlRpcClient->call(
84 QLatin1String("mt.getTrackbackPings"), args,
85 this, SLOT(slotListTrackbackPings(QList<QVariant>,QVariant)),
86 this, SLOT(slotError(int,QString,QVariant)),
87 QVariant( i ) );
88}
89
90void MovableType::fetchPost( BlogPost *post )
91{
92 Q_D( MovableType );
93 kDebug();
94 d->loadCategories();
95 if ( d->mCategoriesList.isEmpty() &&
96 post->categories( ).count() ) {
97 d->mFetchPostCache << post;
98 if ( d->mFetchPostCache.count() ) {
99 // we are already trying to fetch another post, so we don't need to start
100 // another listCategories() job
101 return;
102 }
103
104 connect( this, SIGNAL(listedCategories(QList<QMap<QString,QString> >)),
105 this, SLOT(slotTriggerFetchPost()) );
106 listCategories();
107 } else {
108 MetaWeblog::fetchPost( post );
109 }
110}
111
112void MovableType::createPost( BlogPost *post )
113{
114 // reimplemented because we do this:
115 // http://comox.textdrive.com/pipermail/wp-testers/2005-July/000284.html
116 kDebug();
117 Q_D( MovableType );
118
119 // we need mCategoriesList to be loaded first, since we cannot use the post->categories()
120 // names later, but we need to map them to categoryId of the blog
121 d->loadCategories();
122 if ( d->mCategoriesList.isEmpty() &&
123 !post->categories().isEmpty() ) {
124 kDebug() << "No categories in the cache yet. Have to fetch them first.";
125 d->mCreatePostCache << post;
126 connect( this, SIGNAL(listedCategories(QList<QMap<QString,QString> >)),
127 this, SLOT(slotTriggerCreatePost()) );
128 listCategories();
129 }
130 else {
131 bool publish = post->isPrivate();
132 // If we do setPostCategories() later than we disable publishing first.
133 if ( !post->categories().isEmpty() ) {
134 post->setPrivate( true );
135 if ( d->mSilentCreationList.contains( post ) ) {
136 kDebug() << "Post already in mSilentCreationList, this *should* never happen!";
137 } else {
138 d->mSilentCreationList << post;
139 }
140 }
141 MetaWeblog::createPost( post );
142 // HACK: uuh this a bit ugly now... reenable the original publish argument,
143 // since createPost should have parsed now
144 post->setPrivate( publish );
145 }
146}
147
148void MovableType::modifyPost( BlogPost *post )
149{
150 // reimplemented because we do this:
151 // http://comox.textdrive.com/pipermail/wp-testers/2005-July/000284.html
152 kDebug();
153 Q_D( MovableType );
154
155 // we need mCategoriesList to be loaded first, since we cannot use the post->categories()
156 // names later, but we need to map them to categoryId of the blog
157 d->loadCategories();
158 if ( d->mCategoriesList.isEmpty() &&
159 !post->categories().isEmpty() ) {
160 kDebug() << "No categories in the cache yet. Have to fetch them first.";
161 d->mModifyPostCache << post;
162 connect( this, SIGNAL(listedCategories(QList<QMap<QString,QString> >)),
163 this, SLOT(slotTriggerModifyPost()) );
164 listCategories();
165 }
166 else {
167 MetaWeblog::modifyPost( post );
168 }
169}
170
171void MovableTypePrivate::slotTriggerCreatePost()
172{
173 kDebug();
174 Q_Q( MovableType );
175
176 q->disconnect( q, SIGNAL(listedCategories(QList<QMap<QString,QString> >)),
177 q, SLOT(slotTriggerCreatePost()) );
178 // now we can recall createPost with the posts from the cache
179 QList<BlogPost*>::Iterator it = mCreatePostCache.begin();
180 QList<BlogPost*>::Iterator end = mCreatePostCache.end();
181 for ( ; it != end; it++ ) {
182 q->createPost( *it );
183 }
184 mCreatePostCache.clear();
185}
186
187void MovableTypePrivate::slotTriggerModifyPost()
188{
189 kDebug();
190 Q_Q( MovableType );
191
192 q->disconnect( q, SIGNAL(listedCategories(QList<QMap<QString,QString> >)),
193 q, SLOT(slotTriggerModifyPost()) );
194 // now we can recall createPost with the posts from the cache
195 QList<BlogPost*>::Iterator it = mModifyPostCache.begin();
196 QList<BlogPost*>::Iterator end = mModifyPostCache.end();
197 for ( ; it != end; it++ ) {
198 q->modifyPost( *it );
199 }
200 mModifyPostCache.clear();
201}
202
203void MovableTypePrivate::slotTriggerFetchPost()
204{
205 kDebug();
206 Q_Q( MovableType );
207
208 q->disconnect( q, SIGNAL(listedCategories(QList<QMap<QString,QString> >)),
209 q, SLOT(slotTriggerFetchPost()) );
210 QList<BlogPost*>::Iterator it = mFetchPostCache.begin();
211 QList<BlogPost*>::Iterator end = mFetchPostCache.end();
212 for ( ; it != end; it++ ) {
213 q->fetchPost( *it );
214 }
215 mFetchPostCache.clear();
216}
217
218MovableTypePrivate::MovableTypePrivate()
219{
220 kDebug();
221}
222
223MovableTypePrivate::~MovableTypePrivate()
224{
225 kDebug();
226}
227
228void MovableTypePrivate::slotCreatePost( const QList<QVariant> &result, const QVariant &id )
229{
230 Q_Q( MovableType );
231 // reimplement from Blogger1 to chainload the categories stuff before emit()
232 kDebug();
233 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
234 mCallMap.remove( id.toInt() );
235
236 kDebug();
237 //array of structs containing ISO.8601
238 // dateCreated, String userid, String postid, String content;
239 kDebug () << "TOP:" << result[0].typeName();
240 if ( result[0].type() != QVariant::String &&
241 result[0].type() != QVariant::Int ) {
242 kError() << "Could not read the postId, not a string or an integer.";
243 emit q->errorPost( Blogger1::ParsingError,
244 i18n( "Could not read the postId, not a string or an integer." ),
245 post );
246 return;
247 }
248 QString serverID;
249 if ( result[0].type() == QVariant::String ) {
250 serverID = result[0].toString();
251 }
252 if ( result[0].type() == QVariant::Int ) {
253 serverID = QString::fromLatin1( "%1" ).arg( result[0].toInt() );
254 }
255 post->setPostId( serverID );
256 if ( mSilentCreationList.contains( post ) )
257 {
258 // set the categories and publish afterwards
259 setPostCategories( post, !post->isPrivate() );
260 } else {
261 kDebug() << "emitting createdPost()"
262 << "for title: \"" << post->title()
263 << "\" server id: " << serverID;
264 post->setStatus( KBlog::BlogPost::Created );
265 emit q->createdPost( post );
266 }
267}
268
269void MovableTypePrivate::slotFetchPost( const QList<QVariant> &result, const QVariant &id )
270{
271 Q_Q( MovableType );
272 kDebug();
273
274 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
275 mCallMap.remove( id.toInt() );
276
277 //array of structs containing ISO.8601
278 // dateCreated, String userid, String postid, String content;
279 kDebug () << "TOP:" << result[0].typeName();
280 if ( result[0].type() == QVariant::Map &&
281 readPostFromMap( post, result[0].toMap() ) ) {
282 } else {
283 kError() << "Could not fetch post out of the result from the server.";
284 post->setError( i18n( "Could not fetch post out of the result from the server." ) );
285 post->setStatus( BlogPost::Error );
286 emit q->errorPost( Blogger1::ParsingError,
287 i18n( "Could not fetch post out of the result from the server." ), post );
288 }
289 if ( post->categories().isEmpty() ) {
290 QList<QVariant> args( defaultArgs( post->postId() ) );
291 unsigned int i= mCallCounter++;
292 mCallMap[ i ] = post;
293 mXmlRpcClient->call(
294 QLatin1String("mt.getPostCategories"), args,
295 q, SLOT(slotGetPostCategories(QList<QVariant>,QVariant)),
296 q, SLOT(slotError(int,QString,QVariant)),
297 QVariant( i ) );
298 } else {
299 kDebug() << "Emitting fetchedPost()";
300 post->setStatus( KBlog::BlogPost::Fetched );
301 emit q->fetchedPost( post );
302 }
303}
304
305void MovableTypePrivate::slotModifyPost( const QList<QVariant> &result, const QVariant &id )
306{
307 Q_Q( MovableType );
308 // reimplement from Blogger1
309 kDebug();
310 KBlog::BlogPost *post = mCallMap[ id.toInt() ];
311 mCallMap.remove( id.toInt() );
312
313 //array of structs containing ISO.8601
314 // dateCreated, String userid, String postid, String content;
315 kDebug() << "TOP:" << result[0].typeName();
316 if ( result[0].type() != QVariant::Bool &&
317 result[0].type() != QVariant::Int ) {
318 kError() << "Could not read the result, not a boolean.";
319 emit q->errorPost( Blogger1::ParsingError,
320 i18n( "Could not read the result, not a boolean." ),
321 post );
322 return;
323 }
324 if ( mSilentCreationList.contains( post ) ) {
325 post->setStatus( KBlog::BlogPost::Created );
326 mSilentCreationList.removeOne( post );
327 emit q->createdPost( post );
328 } else {
329 if ( !post->categories().isEmpty() ) {
330 setPostCategories( post, false );
331 }
332 }
333}
334
335void MovableTypePrivate::setPostCategories( BlogPost *post, bool publishAfterCategories )
336{
337 kDebug();
338 Q_Q( MovableType );
339
340 unsigned int i = mCallCounter++;
341 mCallMap[ i ] = post;
342 mPublishAfterCategories[ i ] = publishAfterCategories;
343 QList<QVariant> catList;
344 QList<QVariant> args( defaultArgs( post->postId() ) );
345
346 // map the categoryId of the server to the name
347 QStringList categories = post->categories();
348 for ( int j = 0; j < categories.count(); j++ ) {
349 for ( int k = 0; k < mCategoriesList.count(); k++ ) {
350 if ( mCategoriesList[k][QLatin1String("name")] == categories[j] ) {
351 kDebug() << "Matched category with name: " << categories[ j ] << " and id: " << mCategoriesList[ k ][ QLatin1String("categoryId") ];
352 QMap<QString,QVariant> category;
353 //the first in the QStringList of post->categories()
354 // is the primary category
355 category[QLatin1String("categoryId")] = mCategoriesList[k][QLatin1String("categoryId")].toInt();
356 catList << QVariant( category );
357 break;
358 }
359 if ( k == mCategoriesList.count() ) {
360 kDebug() << "Couldn't find categoryId for: " << categories[j];
361 }
362 }
363 }
364 args << QVariant( catList );
365
366 mXmlRpcClient->call(
367 QLatin1String("mt.setPostCategories"), args,
368 q, SLOT(slotSetPostCategories(QList<QVariant>,QVariant)),
369 q, SLOT(slotError(int,QString,QVariant)),
370 QVariant( i ) );
371}
372
373void MovableTypePrivate::slotGetPostCategories(const QList<QVariant>& result,const QVariant& id)
374{
375 kDebug();
376 Q_Q( MovableType );
377
378 int i = id.toInt();
379 BlogPost* post = mCallMap[ i ];
380 mCallMap.remove( i );
381
382 if ( result[ 0 ].type() != QVariant::List ) {
383 kError() << "Could not read the result, not a list. Category fetching failed! We will still emit fetched post now.";
384 emit q->errorPost( Blogger1::ParsingError,
385 i18n( "Could not read the result - is not a list. Category fetching failed." ), post );
386
387 post->setStatus( KBlog::BlogPost::Fetched );
388 emit q->fetchedPost( post );
389 } else {
390 QList<QVariant> categoryList = result[ 0 ].toList();
391 QList<QString> newCatList;
392 QList<QVariant>::ConstIterator it = categoryList.constBegin();
393 QList<QVariant>::ConstIterator end = categoryList.constEnd();
394 for ( ; it != end; it++ ) {
395 newCatList << ( *it ).toMap()[ QLatin1String("categoryName") ].toString();
396 }
397 kDebug() << "categories list: " << newCatList;
398 post->setCategories( newCatList );
399 post->setStatus( KBlog::BlogPost::Fetched );
400 emit q->fetchedPost( post );
401 }
402}
403
404void MovableTypePrivate::slotSetPostCategories(const QList<QVariant>& result,const QVariant& id)
405{
406 kDebug();
407 Q_Q( MovableType );
408
409 int i = id.toInt();
410 BlogPost* post = mCallMap[ i ];
411 bool publish = mPublishAfterCategories[ i ];
412 mCallMap.remove( i );
413 mPublishAfterCategories.remove( i );
414
415 if ( result[0].type() != QVariant::Bool ) {
416 kError() << "Could not read the result, not a boolean. Category setting failed! We will still publish if now if necessary. ";
417 emit q->errorPost( Blogger1::ParsingError,
418 i18n( "Could not read the result - is not a boolean value. Category setting failed. Will still publish now if necessary." ),
419 post );
420 }
421 // Finally publish now, if the post was meant to be published in the beginning.
422 // The first boolean is necessary to only publish if the post is created, not
423 // modified.
424 if ( publish && !post->isPrivate() ) {
425 q->modifyPost( post );
426 }
427
428 // this is the end of the chain then
429 if ( !publish ) {
430 if ( mSilentCreationList.contains( post ) ) {
431 kDebug() << "emitting createdPost() for title: \""
432 << post->title() << "\"";
433 post->setStatus( KBlog::BlogPost::Created );
434 mSilentCreationList.removeOne( post );
435 emit q->createdPost( post );
436 } else {
437 kDebug() << "emitting modifiedPost() for title: \""
438 << post->title() << "\"";
439 post->setStatus( KBlog::BlogPost::Modified );
440 emit q->modifiedPost( post );
441 }
442 }
443}
444
445QList<QVariant> MovableTypePrivate::defaultArgs( const QString &id )
446{
447 Q_Q( MovableType );
448 QList<QVariant> args;
449 if ( !id.isEmpty() ) {
450 args << QVariant( id );
451 }
452 args << QVariant( q->username() )
453 << QVariant( q->password() );
454 return args;
455}
456
457bool MovableTypePrivate::readPostFromMap( BlogPost *post, const QMap<QString, QVariant> &postInfo )
458{
459
460 // FIXME: integrate error handling
461 kDebug() << "readPostFromMap()";
462 if ( !post ) {
463 return false;
464 }
465 QStringList mapkeys = postInfo.keys();
466 kDebug() << endl << "Keys:" << mapkeys.join( QLatin1String(", ") );
467 kDebug() << endl;
468
469 KDateTime dt =
470 KDateTime( postInfo[QLatin1String("dateCreated")].toDateTime(), KDateTime::UTC );
471 if ( dt.isValid() && !dt.isNull() ) {
472 post->setCreationDateTime( dt.toLocalZone() );
473 }
474
475 dt =
476 KDateTime( postInfo[QLatin1String("lastModified")].toDateTime(), KDateTime::UTC );
477 if ( dt.isValid() && !dt.isNull() ) {
478 post->setModificationDateTime( dt.toLocalZone() );
479 }
480
481 post->setPostId( postInfo[QLatin1String("postid")].toString().isEmpty() ? postInfo[QLatin1String("postId")].toString() :
482 postInfo[QLatin1String("postid")].toString() );
483
484 QString title( postInfo[QLatin1String("title")].toString() );
485 QString description( postInfo[QLatin1String("description")].toString() );
486 QStringList categoryIdList = postInfo[QLatin1String("categories")].toStringList();
487 QStringList categories;
488 // since the metaweblog definition is ambigious, we try different
489 // category mappings
490 for ( int i = 0; i < categoryIdList.count(); i++ ) {
491 for ( int k = 0; k < mCategoriesList.count(); k++ ) {
492 if ( mCategoriesList[ k ][ QLatin1String("name") ] == categoryIdList[ i ] ) {
493 categories << mCategoriesList[ k ][ QLatin1String("name") ];
494 } else if ( mCategoriesList[ k ][ QLatin1String("categoryId") ] == categoryIdList[ i ]) {
495 categories << mCategoriesList[ k ][ QLatin1String("name") ];
496 }
497 }
498 }
499
500 //TODO 2 new keys are:
501 // String mt_convert_breaks, the value for the convert_breaks field
502 post->setSlug( postInfo[QLatin1String("wp_slug")].toString() );
503 post->setAdditionalContent( postInfo[QLatin1String("mt_text_more")].toString() );
504 post->setTitle( title );
505 post->setContent( description );
506 post->setCommentAllowed( (bool)postInfo[QLatin1String("mt_allow_comments")].toInt() );
507 post->setTrackBackAllowed( (bool)postInfo[QLatin1String("mt_allow_pings")].toInt() );
508 post->setSummary( postInfo[QLatin1String("mt_excerpt")].toString() );
509 post->setTags( postInfo[QLatin1String("mt_keywords")].toStringList() );
510 post->setLink( postInfo[QLatin1String("link")].toString() );
511 post->setPermaLink( postInfo[QLatin1String("permaLink")].toString() );
512 QString postStatus = postInfo[QLatin1String("post_status")].toString();
513 if ( postStatus != QLatin1String("publish") &&
514 !postStatus.isEmpty() ) {
515 /**
516 * Maybe this field wasn't set by server! so, on that situation, we will assume it as non-Private,
517 * The postStatus.isEmpty() check is for that!
518 * I found this field on Wordpress output! it's value can be: publish, private, draft (as i see)
519 */
520 post->setPrivate( true );
521 }
522 if ( !categories.isEmpty() ) {
523 kDebug() << "Categories:" << categories;
524 post->setCategories( categories );
525 }
526 return true;
527}
528
529void MovableTypePrivate::slotListTrackBackPings(
530 const QList<QVariant> &result, const QVariant &id )
531{
532 Q_Q( MovableType );
533 kDebug() << "slotTrackbackPings()";
534 BlogPost *post = mCallMap[ id.toInt() ];
535 mCallMap.remove( id.toInt() );
536 QList<QMap<QString,QString> > trackBackList;
537 if ( result[0].type() != QVariant::List ) {
538 kError() << "Could not fetch list of trackback pings out of the"
539 << "result from the server.";
540 emit q->error( MovableType::ParsingError,
541 i18n( "Could not fetch list of trackback pings out of the "
542 "result from the server." ) );
543 return;
544 }
545 const QList<QVariant> trackBackReceived = result[0].toList();
546 QList<QVariant>::ConstIterator it = trackBackReceived.begin();
547 QList<QVariant>::ConstIterator end = trackBackReceived.end();
548 for ( ; it != end; ++it ) {
549 QMap<QString,QString> tping;
550 kDebug() << "MIDDLE:" << ( *it ).typeName();
551 const QMap<QString, QVariant> trackBackInfo = ( *it ).toMap();
552 tping[ QLatin1String("title") ] = trackBackInfo[ QLatin1String("pingTitle")].toString();
553 tping[ QLatin1String("url") ] = trackBackInfo[ QLatin1String("pingURL")].toString();
554 tping[ QLatin1String("ip") ] = trackBackInfo[ QLatin1String("pingIP")].toString();
555 trackBackList << tping;
556 }
557 kDebug() << "Emitting listedTrackBackPings()";
558 emit q->listedTrackBackPings( post, trackBackList );
559}
560
561bool MovableTypePrivate::readArgsFromPost( QList<QVariant> *args, const BlogPost &post )
562{
563 //TODO 2 new keys are:
564 // String mt_convert_breaks, the value for the convert_breaks field
565 // array mt_tb_ping_urls, the list of TrackBack ping URLs for this entry
566 if ( !args ) {
567 return false;
568 }
569 QMap<QString, QVariant> map;
570 map[QLatin1String("categories")] = post.categories();
571 map[QLatin1String("description")] = post.content();
572 if ( !post.additionalContent().isEmpty() ) {
573 map[QLatin1String("mt_text_more")] = post.additionalContent();
574 }
575 map[QLatin1String("title")] = post.title();
576 map[QLatin1String("dateCreated")] = post.creationDateTime().dateTime().toUTC();
577 map[QLatin1String("mt_allow_comments")] = (int)post.isCommentAllowed();
578 map[QLatin1String("mt_allow_pings")] = (int)post.isTrackBackAllowed();
579 map[QLatin1String("mt_excerpt")] = post.summary();
580 map[QLatin1String("mt_keywords")] = post.tags().join( QLatin1String(",") );
581 //map["mt_tb_ping_urls"] check for that, i think this should only be done on the server.
582 *args << map;
583 *args << QVariant( !post.isPrivate() );
584 return true;
585}
586
587#include "moc_movabletype.cpp"
588