1/*
2 Copyright (C) 2005 Ivor Hewitt <ivor@ivor.org>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17*/
18
19// Own
20#include "filteropts.h"
21
22// Qt
23#include <QtCore/QRegExp>
24#include <QtCore/QTextStream>
25#include <QtDBus/QtDBus>
26#include <QCheckBox>
27#include <QLabel>
28#include <QLayout>
29#include <QTreeView>
30#include <QWhatsThis>
31
32// KDE
33#include <kaboutdata.h>
34#include <kconfig.h>
35#include <kfiledialog.h>
36#include <kglobal.h>
37#include <khbox.h>
38#include <klocale.h>
39#include <KPluginFactory>
40#include <KPluginLoader>
41#include <KListWidget>
42#include <klistwidgetsearchline.h>
43#include <klineedit.h>
44#include <kpushbutton.h>
45#include <knuminput.h>
46#include <KTabWidget>
47
48K_PLUGIN_FACTORY_DECLARATION(KcmKonqHtmlFactory)
49
50KCMFilter::KCMFilter( QWidget *parent, const QVariantList& )
51 : KCModule( KcmKonqHtmlFactory::componentData(), parent ),
52 mGroupname( "Filter Settings" ),
53 mSelCount(0),
54 mOriginalString(QString())
55{
56 mConfig = KSharedConfig::openConfig("khtmlrc", KConfig::NoGlobals);
57 setButtons(Default|Apply|Help);
58
59 QVBoxLayout *topLayout = new QVBoxLayout(this);
60
61 mEnableCheck = new QCheckBox(i18n("Enable filters"), this);
62 topLayout->addWidget( mEnableCheck );
63
64 mKillCheck = new QCheckBox(i18n("Hide filtered images"), this);
65 topLayout->addWidget( mKillCheck );
66
67 mFilterWidget = new KTabWidget( this );
68 topLayout->addWidget( mFilterWidget );
69
70 QWidget *container = new QWidget( mFilterWidget );
71 mFilterWidget->addTab( container, i18n("Manual Filter") );
72
73 QVBoxLayout *vbox = new QVBoxLayout;
74
75 mListBox = new KListWidget;
76 mListBox->setSelectionMode(QListWidget::ExtendedSelection);
77
78 // If the filter list were sensitive to ordering, then we would need to
79 // preserve the order of items inserted or arranged by the user (and the
80 // GUI would need "Move Up" and "Move Down" buttons to reorder the filters).
81 // However, now the filters are applied in an unpredictable order because
82 // of the new hashed matching algorithm. So the list can stay sorted.
83 mListBox->setSortingEnabled( true );
84
85 KHBox *searchBox = new KHBox;
86 searchBox->setSpacing( -1 );
87 new QLabel( i18n("Search:"), searchBox ) ;
88
89 mSearchLine = new KListWidgetSearchLine( searchBox, mListBox );
90
91 vbox->addWidget( searchBox );
92
93 vbox->addWidget(mListBox);
94
95 QLabel *exprLabel = new QLabel( i18n("<qt>Filter expression (e.g. <tt>http://www.example.com/ad/*</tt>, <a href=\"filterhelp\">more information</a>):"), this );
96 connect( exprLabel, SIGNAL(linkActivated(QString)), SLOT(slotInfoLinkActivated(QString)) );
97 vbox->addWidget(exprLabel);
98
99 mString = new KLineEdit;
100 vbox->addWidget(mString);
101
102 KHBox *buttonBox = new KHBox;
103 vbox->addWidget(buttonBox);
104
105 container->setLayout(vbox);
106
107 /** tab for automatic filter lists */
108 container = new QWidget( mFilterWidget );
109 mFilterWidget->addTab( container, i18n("Automatic Filter") );
110 QGridLayout *grid = new QGridLayout;
111 grid->setColumnStretch( 2, 1 );
112 container->setLayout(grid);
113
114 mAutomaticFilterList = new QTreeView( container );
115 mAutomaticFilterList->setModel( &mAutomaticFilterModel );
116 grid->addWidget( mAutomaticFilterList, 0, 0, 1, 3 );
117
118 QLabel *label = new QLabel( i18n( "Automatic update interval:" ), container );
119 grid->addWidget( label, 1, 0 );
120 mRefreshFreqSpinBox = new KIntSpinBox( container );
121 grid->addWidget( mRefreshFreqSpinBox, 1, 1 );
122 mRefreshFreqSpinBox->setRange( 1, 365 );
123 mRefreshFreqSpinBox->setSuffix(ki18np(" day", " days"));
124
125 /** connect signals and slots */
126 connect( &mAutomaticFilterModel, SIGNAL(changed(bool)), this, SIGNAL(changed(bool)) );
127 connect( mRefreshFreqSpinBox, SIGNAL(valueChanged(int)), this, SLOT(spinBoxChanged(int)) );
128
129 mInsertButton = new KPushButton( KIcon("list-add"), i18n("Insert"), buttonBox );
130 connect( mInsertButton, SIGNAL(clicked()), SLOT(insertFilter()) );
131 mUpdateButton = new KPushButton( KIcon("document-edit"), i18n("Update"), buttonBox );
132 connect( mUpdateButton, SIGNAL(clicked()), SLOT(updateFilter()) );
133 mRemoveButton = new KPushButton( KIcon("list-remove"), i18n("Remove"), buttonBox );
134 connect( mRemoveButton, SIGNAL(clicked()), SLOT(removeFilter()) );
135
136 mImportButton = new KPushButton( KIcon("document-import"), i18n("Import..."),buttonBox);
137 connect( mImportButton, SIGNAL(clicked()), SLOT(importFilters()) );
138 mExportButton = new KPushButton( KIcon("document-export"), i18n("Export..."),buttonBox);
139 connect( mExportButton, SIGNAL(clicked()), SLOT(exportFilters()) );
140
141 KHBox *impexpBox = new KHBox;
142 QLabel *impexpLabel = new QLabel( i18n("<qt>More information on "
143 "<a href=\"importhelp\">import format</a>, "
144 "<a href=\"exporthelp\">export format</a>"), impexpBox );
145 connect( impexpLabel, SIGNAL(linkActivated(QString)), SLOT(slotInfoLinkActivated(QString)) );
146
147 vbox->addWidget(impexpBox,0,Qt::AlignRight);
148
149 connect( mEnableCheck, SIGNAL(toggled(bool)), this, SLOT(slotEnableChecked()));
150 connect( mKillCheck, SIGNAL(clicked()), this, SLOT(slotKillChecked()));
151 connect( mListBox, SIGNAL(itemSelectionChanged()),this, SLOT(slotItemSelected()));
152 connect( mString, SIGNAL(textChanged(QString)), this, SLOT(updateButton()) );
153/*
154 * Whats this items
155 */
156 mEnableCheck->setWhatsThis( i18n("Enable or disable AdBlocK filters. When enabled, a set of URL expressions "
157 "should be defined in the filter list for blocking to take effect."));
158 mKillCheck->setWhatsThis( i18n("When enabled blocked images will be removed from the page completely, "
159 "otherwise a placeholder 'blocked' image will be used."));
160
161 // The list is no longer sensitive to order, because of the new hashed
162 // matching. So this tooltip doesn't imply that.
163 //
164 // FIXME: blocking of frames is not currently implemented by KHTML
165 mListBox->setWhatsThis( i18n("This is the list of URL filters that will be applied to all embedded "
166 "images and media objects.") );
167 // "images, objects and frames.") );
168
169 mString->setWhatsThis( i18n("<qt><p>Enter an expression to filter. Filters can be defined as either:"
170 "<ul><li>a shell-style wildcard, e.g. <tt>http://www.example.com/ads*</tt>, the wildcards <tt>*?[]</tt> may be used</li>"
171 "<li>a full regular expression by surrounding the string with '<tt>/</tt>', e.g. <tt>/\\/(ad|banner)\\./</tt></li></ul>"
172 "<p>Any filter string can be preceded by '<tt>@@</tt>' to whitelist (allow) any matching URL, "
173 "which takes priority over any blacklist (blocking) filter."));
174}
175
176KCMFilter::~KCMFilter()
177{
178}
179
180void KCMFilter::slotInfoLinkActivated(const QString &url)
181{
182 if ( url == "filterhelp" )
183 QWhatsThis::showText( QCursor::pos(), mString->whatsThis() );
184 else if ( url == "importhelp" )
185 QWhatsThis::showText( QCursor::pos(), i18n("<qt><p>The filter import format is a plain text file. "
186 "Blank lines, comment lines starting with '<tt>!</tt>' "
187 "and the header line <tt>[AdBlock]</tt> are ignored. "
188 "Any other line is added as a filter expression.") );
189 else if ( url == "exporthelp" )
190 QWhatsThis::showText( QCursor::pos(), i18n("<qt><p>The filter export format is a plain text file. "
191 "The file begins with a header line <tt>[AdBlock]</tt>, then all of "
192 "the filters follow each on a separate line.") );
193}
194
195void KCMFilter::slotKillChecked()
196{
197 emit changed( true );
198}
199
200void KCMFilter::slotEnableChecked()
201{
202 updateButton();
203 emit changed( true );
204}
205
206void KCMFilter::slotItemSelected()
207{
208 int currentId=-1;
209 int i;
210 for( i=0,mSelCount=0; i < mListBox->count() && mSelCount<2; ++i )
211 {
212 if (mListBox->item(i)->isSelected())
213 {
214 currentId=i;
215 mSelCount++;
216 }
217 }
218
219 if ( currentId >= 0 )
220 {
221 mOriginalString = mListBox->item(currentId)->text();
222 mString->setText(mOriginalString);
223 mString->setFocus(Qt::OtherFocusReason);
224 }
225 updateButton();
226}
227
228void KCMFilter::updateButton()
229{
230 bool state = mEnableCheck->isChecked();
231 bool expressionIsNotEmpty = !mString->text().isEmpty();
232 bool filterEdited = expressionIsNotEmpty && mOriginalString!=mString->text();
233
234 mInsertButton->setEnabled(state && expressionIsNotEmpty && filterEdited );
235 mUpdateButton->setEnabled(state && (mSelCount == 1) && expressionIsNotEmpty && filterEdited );
236 mRemoveButton->setEnabled(state && (mSelCount > 0));
237 mImportButton->setEnabled(state);
238 mExportButton->setEnabled(state && mListBox->count()>0);
239
240 mListBox->setEnabled(state);
241 mString->setEnabled(state);
242 mKillCheck->setEnabled(state);
243
244 if (filterEdited)
245 {
246 if (mSelCount==1 && mUpdateButton->isEnabled()) mUpdateButton->setDefault(true);
247 else if (mInsertButton->isEnabled()) mInsertButton->setDefault(true);
248 }
249 else
250 {
251 mInsertButton->setDefault(false);
252 mUpdateButton->setDefault(false);
253 }
254
255 mAutomaticFilterList->setEnabled(state);
256 mRefreshFreqSpinBox->setEnabled(state);
257}
258
259void KCMFilter::importFilters()
260{
261 QString inFile = KFileDialog::getOpenFileName(KUrl(), QString(), this);
262 if (!inFile.isEmpty())
263 {
264 QFile f(inFile);
265 if ( f.open( QIODevice::ReadOnly ) )
266 {
267 QTextStream ts( &f );
268 QStringList paths;
269 QString line;
270 while (!ts.atEnd())
271 {
272 line = ts.readLine();
273 if (line.isEmpty() || line.compare("[adblock]", Qt::CaseInsensitive) == 0)
274 continue;
275
276 // Treat leading ! as filter comment, otherwise check expressions
277 // are valid.
278 if (!line.startsWith("!")) //krazy:exclude=doublequote_chars
279 {
280 if (line.length()>2 && line[0]=='/' && line[line.length()-1] == '/')
281 {
282 QString inside = line.mid(1, line.length()-2);
283 QRegExp rx(inside);
284 if (!rx.isValid())
285 continue;
286 }
287 else
288 {
289 QRegExp rx(line);
290 rx.setPatternSyntax(QRegExp::Wildcard);
291 if (!rx.isValid())
292 continue;
293 }
294
295 if (mListBox->findItems(line, Qt::MatchCaseSensitive|Qt::MatchExactly).isEmpty())
296 {
297 paths.append(line);
298 }
299 }
300 }
301 f.close();
302
303 mListBox->addItems( paths );
304 emit changed(true);
305 }
306 }
307}
308
309void KCMFilter::exportFilters()
310{
311 QString outFile = KFileDialog::getSaveFileName(KUrl(), QString(), this);
312 if (!outFile.isEmpty())
313 {
314
315 QFile f(outFile);
316 if ( f.open( QIODevice::WriteOnly ) )
317 {
318 QTextStream ts( &f );
319 ts.setCodec( "UTF-8" );
320 ts << "[AdBlock]" << endl;
321
322 int nbLine = mListBox->count();
323 for( int i = 0; i < nbLine; ++i )
324 ts << mListBox->item(i)->text() << endl;
325
326 f.close();
327 }
328 }
329}
330
331void KCMFilter::defaults()
332{
333 mAutomaticFilterModel.defaults();
334
335 mListBox->clear();
336 mEnableCheck->setChecked(false);
337 mKillCheck->setChecked(false);
338 mString->clear();
339 updateButton();
340}
341
342void KCMFilter::save()
343{
344 KConfigGroup cg(mConfig, mGroupname);
345 cg.deleteGroup();
346 cg = KConfigGroup(mConfig, mGroupname);
347
348 cg.writeEntry("Enabled",mEnableCheck->isChecked());
349 cg.writeEntry("Shrink",mKillCheck->isChecked());
350
351 int i;
352 for( i = 0; i < mListBox->count(); ++i )
353 {
354 QString key = "Filter-" + QString::number(i);
355 cg.writeEntry(key, mListBox->item(i)->text());
356 }
357 cg.writeEntry("Count",mListBox->count());
358
359 mAutomaticFilterModel.save(cg);
360 cg.writeEntry( "HTMLFilterListMaxAgeDays", mRefreshFreqSpinBox->value() );
361
362 cg.sync();
363
364 QDBusMessage message =
365 QDBusMessage::createSignal("/KonqMain", "org.kde.Konqueror.Main", "reparseConfiguration");
366 QDBusConnection::sessionBus().send(message);
367}
368
369void KCMFilter::load()
370{
371 QStringList paths;
372
373 KConfigGroup cg(mConfig, mGroupname);
374 mAutomaticFilterModel.load(cg);
375 mAutomaticFilterList->resizeColumnToContents( 0 );
376 int refreshFreq = cg.readEntry( "HTMLFilterListMaxAgeDays", 7 );
377 mRefreshFreqSpinBox->setValue( refreshFreq < 1 ? 1 : refreshFreq );
378
379 mEnableCheck->setChecked( cg.readEntry("Enabled", false));
380 mKillCheck->setChecked( cg.readEntry("Shrink", false));
381
382 QMap<QString,QString> entryMap = cg.entryMap();
383 QMap<QString,QString>::ConstIterator it;
384 int num = cg.readEntry("Count",0);
385 for (int i=0; i<num; ++i)
386 {
387 QString key = "Filter-" + QString::number(i);
388 it = entryMap.constFind(key);
389 if (it != entryMap.constEnd())
390 paths.append(it.value());
391 }
392
393 mListBox->addItems( paths );
394 updateButton();
395}
396
397void KCMFilter::insertFilter()
398{
399 QString newFilter = mString->text();
400
401 if ( !newFilter.isEmpty() && mListBox->findItems( newFilter, Qt::MatchCaseSensitive|Qt::MatchExactly ).isEmpty() )
402 {
403 mListBox->clearSelection();
404 mListBox->addItem( newFilter );
405
406 // The next line assumed that the new item would be added at the end
407 // of the list, but that may not be the case if sorting is enabled.
408 // So we search again to locate the just-added item.
409 //int id = mListBox->count()-1;
410 QListWidgetItem *newItem = mListBox->findItems( newFilter, Qt::MatchCaseSensitive|Qt::MatchExactly ).first();
411 if (newItem != 0 )
412 {
413 int id = mListBox->row(newItem);
414
415 mListBox->item(id)->setSelected(true);
416 mListBox->setCurrentRow(id);
417 }
418
419 updateButton();
420 emit changed( true );
421 }
422}
423
424void KCMFilter::removeFilter()
425{
426 for( int i = mListBox->count(); i >= 0; --i )
427 {
428 if (mListBox->item(i) && mListBox->item(i)->isSelected())
429 delete mListBox->takeItem(i);
430 }
431 mString->clear();
432 emit changed( true );
433 updateButton();
434}
435
436void KCMFilter::updateFilter()
437{
438 if ( !mString->text().isEmpty() )
439 {
440 int index = mListBox->currentRow();
441 if ( index >= 0 )
442 {
443 mListBox->item(index)->setText(mString->text());
444 emit changed( true );
445 }
446 }
447 updateButton();
448}
449
450QString KCMFilter::quickHelp() const
451{
452 return i18n("<h1>Konqueror AdBlocK</h1> Konqueror AdBlocK allows you to create a list of filters"
453 " that are checked against linked images and frames. URL's that match are either discarded or"
454 " replaced with a placeholder image. ");
455}
456
457void KCMFilter::spinBoxChanged( int )
458{
459 emit changed( true );
460}
461
462AutomaticFilterModel::AutomaticFilterModel(QObject * parent)
463 : QAbstractItemModel(parent),
464 mGroupname( "Filter Settings" )
465{
466 //mConfig = KSharedConfig::openConfig("khtmlrc", KConfig::NoGlobals);
467 mConfig = KSharedConfig::openConfig("khtmlrc", KConfig::IncludeGlobals);
468}
469
470void AutomaticFilterModel::load(KConfigGroup &cg)
471{
472 beginResetModel();
473 mFilters.clear();
474 const int maxNumFilters = 1024;
475 const bool defaultHTMLFilterListEnabled = false;
476
477 for ( int numFilters = 1; numFilters < maxNumFilters; ++numFilters )
478 {
479 struct FilterConfig filterConfig;
480 filterConfig.filterName = cg.readEntry( QString( "HTMLFilterListName-" ) + QString::number( numFilters ), "" );
481 if ( filterConfig.filterName == "" )
482 break;
483
484 filterConfig.enableFilter = cg.readEntry( QString( "HTMLFilterListEnabled-" ) + QString::number( numFilters ), defaultHTMLFilterListEnabled );
485 filterConfig.filterURL = cg.readEntry( QString( "HTMLFilterListURL-" ) + QString::number( numFilters ), "" );
486 filterConfig.filterLocalFilename = cg.readEntry( QString( "HTMLFilterListLocalFilename-" ) + QString::number( numFilters ), "" );
487
488 mFilters << filterConfig;
489 }
490 endResetModel();
491}
492
493void AutomaticFilterModel::save(KConfigGroup &cg)
494{
495 for ( int i = mFilters.count() - 1; i >= 0; --i )
496 {
497 cg.writeEntry( QString( "HTMLFilterListLocalFilename-" ) + QString::number( i + 1 ), mFilters[i].filterLocalFilename );
498 cg.writeEntry( QString( "HTMLFilterListURL-" ) + QString::number( i + 1 ), mFilters[i].filterURL );
499 cg.writeEntry( QString( "HTMLFilterListName-" ) + QString::number( i + 1 ), mFilters[i].filterName );
500 cg.writeEntry( QString( "HTMLFilterListEnabled-" ) + QString::number( i + 1 ), mFilters[i].enableFilter );
501 }
502}
503
504void AutomaticFilterModel::defaults()
505{
506 mConfig = KSharedConfig::openConfig( "khtmlrc", KConfig::IncludeGlobals );
507 KConfigGroup cg( mConfig, mGroupname );
508 load( cg );
509}
510
511QModelIndex AutomaticFilterModel::index(int row, int column, const QModelIndex & /*parent*/) const
512{
513 return createIndex(row, column, (void*)NULL);
514}
515
516QModelIndex AutomaticFilterModel::parent(const QModelIndex & /*index*/) const
517{
518 return QModelIndex();
519}
520
521bool AutomaticFilterModel::hasChildren(const QModelIndex & parent) const
522{
523 return parent == QModelIndex();
524}
525
526int AutomaticFilterModel::rowCount(const QModelIndex & /*parent*/) const
527{
528 return mFilters.count();
529}
530
531int AutomaticFilterModel::columnCount(const QModelIndex & /*parent*/) const
532{
533 return 2;
534}
535
536QVariant AutomaticFilterModel::data(const QModelIndex &index, int role) const
537{
538 if (!index.isValid())
539 return QVariant();
540
541 if (role == Qt::DisplayRole && index.row()<mFilters.count())
542 switch ( index.column() ) {
543 case 0: return QVariant( mFilters[index.row()].filterName );
544 case 1: return QVariant( mFilters[index.row()].filterURL );
545 default: return QVariant( "?" );
546 }
547 else if ( role == Qt::CheckStateRole && index.column() == 0 && index.row()<mFilters.count() )
548 return mFilters[index.row()].enableFilter ? Qt::Checked : Qt::Unchecked;
549 else
550 return QVariant();
551
552}
553
554bool AutomaticFilterModel::setData( const QModelIndex & index, const QVariant & value, int role )
555{
556 if (role == Qt::CheckStateRole && index.column() == 0 && index.row() < mFilters.count())
557 {
558 mFilters[index.row()].enableFilter = static_cast<Qt::CheckState>(value.toInt()) == Qt::Checked;
559 emit dataChanged(index, index);
560 emit changed( true );
561 return true;
562 }
563
564 return false;
565}
566
567QVariant AutomaticFilterModel::headerData(int section, Qt::Orientation orientation, int role) const
568{
569 if (role != Qt::DisplayRole || orientation != Qt::Horizontal)
570 return QVariant();
571
572
573 switch ( section ) {
574 case 0: return QVariant( i18n( "Name" ) );
575 case 1: return QVariant( i18n( "URL" ) );
576 default: return QVariant( "?" );
577 }
578}
579
580Qt::ItemFlags AutomaticFilterModel::flags( const QModelIndex & index ) const
581{
582 Qt::ItemFlags rc = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
583 if (index.column() == 0)
584 rc |= Qt::ItemIsUserCheckable;
585 return rc;
586}
587
588#include "filteropts.moc"
589
590