~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/search/builtin.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
"""
 
3
    MoinMoin - search engine internals
 
4
 
 
5
    @copyright: 2005 MoinMoin:FlorianFesti,
 
6
                2005 MoinMoin:NirSoffer,
 
7
                2005 MoinMoin:AlexanderSchremmer,
 
8
                2006-2008 MoinMoin:ThomasWaldmann,
 
9
                2006 MoinMoin:FranzPletz
 
10
    @license: GNU GPL, see COPYING for details
 
11
"""
 
12
 
 
13
import sys, os, time, errno, codecs
 
14
 
 
15
from MoinMoin import log
 
16
logging = log.getLogger(__name__)
 
17
 
 
18
from MoinMoin import wikiutil, config
 
19
from MoinMoin.Page import Page
 
20
from MoinMoin.util import lock, filesys
 
21
from MoinMoin.search.results import getSearchResults
 
22
from MoinMoin.search.queryparser import Match, TextMatch, TitleMatch
 
23
 
 
24
##############################################################################
 
25
# Search Engine Abstraction
 
26
##############################################################################
 
27
 
 
28
class UpdateQueue:
 
29
    """ Represents a locked page queue on the disk
 
30
 
 
31
        XXX: check whether we just can use the caching module
 
32
    """
 
33
 
 
34
    def __init__(self, f, lock_dir):
 
35
        """
 
36
        @param f: file to write to
 
37
        @param lock_dir: directory to save the lock files
 
38
        """
 
39
        self.file = f
 
40
        self.writeLock = lock.WriteLock(lock_dir, timeout=10.0)
 
41
        self.readLock = lock.ReadLock(lock_dir, timeout=10.0)
 
42
 
 
43
    def exists(self):
 
44
        """ Checks if the queue exists on the filesystem """
 
45
        return os.path.exists(self.file)
 
46
 
 
47
    def append(self, pagename):
 
48
        """ Append a page to queue
 
49
 
 
50
        @param pagename: string to save
 
51
        """
 
52
        if not self.writeLock.acquire(60.0):
 
53
            logging.warning("can't add %r to xapian update queue: can't lock queue" % pagename)
 
54
            return
 
55
        try:
 
56
            f = codecs.open(self.file, 'a', config.charset)
 
57
            try:
 
58
                f.write(pagename + "\n")
 
59
            finally:
 
60
                f.close()
 
61
        finally:
 
62
            self.writeLock.release()
 
63
 
 
64
    def pages(self):
 
65
        """ Return list of pages in the queue """
 
66
        if self.readLock.acquire(1.0):
 
67
            try:
 
68
                return self._decode(self._read())
 
69
            finally:
 
70
                self.readLock.release()
 
71
        return []
 
72
 
 
73
    def remove(self, pages):
 
74
        """ Remove pages from the queue
 
75
 
 
76
        When the queue is empty, the queue file is removed, so exists()
 
77
        can tell if there is something waiting in the queue.
 
78
 
 
79
        @param pages: list of pagenames to remove
 
80
        """
 
81
        if self.writeLock.acquire(30.0):
 
82
            try:
 
83
                queue = self._decode(self._read())
 
84
                for page in pages:
 
85
                    try:
 
86
                        queue.remove(page)
 
87
                    except ValueError:
 
88
                        pass
 
89
                if queue:
 
90
                    self._write(queue)
 
91
                else:
 
92
                    self._removeFile()
 
93
                return True
 
94
            finally:
 
95
                self.writeLock.release()
 
96
        return False
 
97
 
 
98
    # Private -------------------------------------------------------
 
99
 
 
100
    def _decode(self, data):
 
101
        """ Decode queue data
 
102
 
 
103
        @param data: the data to decode
 
104
        """
 
105
        pages = data.splitlines()
 
106
        return self._filterDuplicates(pages)
 
107
 
 
108
    def _filterDuplicates(self, pages):
 
109
        """ Filter duplicates in page list, keeping the order
 
110
 
 
111
        @param pages: list of pages to filter
 
112
        """
 
113
        unique = []
 
114
        seen = {}
 
115
        for name in pages:
 
116
            if not name in seen:
 
117
                unique.append(name)
 
118
                seen[name] = 1
 
119
        return unique
 
120
 
 
121
    def _read(self):
 
122
        """ Read and return queue data
 
123
 
 
124
        This does not do anything with the data so we can release the
 
125
        lock as soon as possible, enabling others to update the queue.
 
126
        """
 
127
        try:
 
128
            f = codecs.open(self.file, 'r', config.charset)
 
129
            try:
 
130
                return f.read()
 
131
            finally:
 
132
                f.close()
 
133
        except (OSError, IOError), err:
 
134
            if err.errno != errno.ENOENT:
 
135
                raise
 
136
            return ''
 
137
 
 
138
    def _write(self, pages):
 
139
        """ Write pages to queue file
 
140
 
 
141
        Requires queue write locking.
 
142
 
 
143
        @param pages: list of pages to write
 
144
        """
 
145
        # XXX use tmpfile/move for atomic replace on real operating systems
 
146
        data = '\n'.join(pages) + '\n'
 
147
        f = codecs.open(self.file, 'w', config.charset)
 
148
        try:
 
149
            f.write(data)
 
150
        finally:
 
151
            f.close()
 
152
 
 
153
    def _removeFile(self):
 
154
        """ Remove queue file
 
155
 
 
156
        Requires queue write locking.
 
157
        """
 
158
        try:
 
159
            os.remove(self.file)
 
160
        except OSError, err:
 
161
            if err.errno != errno.ENOENT:
 
162
                raise
 
163
 
 
164
 
 
165
class BaseIndex:
 
166
    """ Represents a search engine index """
 
167
 
 
168
    class LockedException(Exception):
 
169
        pass
 
170
 
 
171
    def __init__(self, request):
 
172
        """
 
173
        @param request: current request
 
174
        """
 
175
        self.request = request
 
176
        main_dir = self._main_dir()
 
177
        self.dir = os.path.join(main_dir, 'index')
 
178
        if not os.path.exists(self.dir):
 
179
            os.makedirs(self.dir)
 
180
        self.sig_file = os.path.join(main_dir, 'complete')
 
181
        lock_dir = os.path.join(main_dir, 'index-lock')
 
182
        self.lock = lock.WriteLock(lock_dir, timeout=3600.0, readlocktimeout=60.0)
 
183
        #self.read_lock = lock.ReadLock(lock_dir, timeout=3600.0)
 
184
        self.update_queue = UpdateQueue(os.path.join(main_dir, 'update-queue'),
 
185
                                os.path.join(main_dir, 'update-queue-lock'))
 
186
        self.remove_queue = UpdateQueue(os.path.join(main_dir, 'remove-queue'),
 
187
                                os.path.join(main_dir, 'remove-queue-lock'))
 
188
 
 
189
        # Disabled until we have a sane way to build the index with a
 
190
        # queue in small steps.
 
191
        ## if not self.exists():
 
192
        ##    self.indexPagesInNewThread(request)
 
193
 
 
194
    def _main_dir(self):
 
195
        raise NotImplemented('...')
 
196
 
 
197
    def exists(self):
 
198
        """ Check if index exists """
 
199
        return os.path.exists(self.sig_file)
 
200
 
 
201
    def mtime(self):
 
202
        """ Modification time of the index """
 
203
        return os.path.getmtime(self.dir)
 
204
 
 
205
    def touch(self):
 
206
        """ Touch the index """
 
207
        filesys.touch(self.dir)
 
208
 
 
209
    def _search(self, query):
 
210
        """ Actually perfom the search (read-lock acquired)
 
211
 
 
212
        @param query: the search query objects tree
 
213
        """
 
214
        raise NotImplemented('...')
 
215
 
 
216
    def search(self, query, **kw):
 
217
        """ Search for items in the index
 
218
 
 
219
        @param query: the search query objects to pass to the index
 
220
        """
 
221
        #if not self.read_lock.acquire(1.0):
 
222
        #    raise self.LockedException
 
223
        #try:
 
224
        hits = self._search(query, **kw)
 
225
        #finally:
 
226
        #    self.read_lock.release()
 
227
        return hits
 
228
 
 
229
    def update_page(self, pagename, now=1):
 
230
        """ Update a single page in the index
 
231
 
 
232
        @param pagename: the name of the page to update
 
233
        @keyword now: do all updates now (default: 1)
 
234
        """
 
235
        self.update_queue.append(pagename)
 
236
        if now:
 
237
            self._do_queued_updates_InNewThread()
 
238
 
 
239
    def remove_item(self, pagename, attachment=None, now=1):
 
240
        """ Removes a page and all its revisions or a single attachment
 
241
 
 
242
        @param pagename: name of the page to be removed
 
243
        @keyword attachment: optional, only remove this attachment of the page
 
244
        @keyword now: do all updates now (default: 1)
 
245
        """
 
246
        self.remove_queue.append('%s//%s' % (pagename, attachment or ''))
 
247
        if now:
 
248
            self._do_queued_updates_InNewThread()
 
249
 
 
250
    def indexPages(self, files=None, mode='update'):
 
251
        """ Index all pages (and files, if given)
 
252
 
 
253
        Can be called only from a script. To index pages during a user
 
254
        request, use indexPagesInNewThread.
 
255
        @keyword files: iterator or list of files to index additionally
 
256
        @keyword mode: set the mode of indexing the pages, either 'update', 'add' or 'rebuild'
 
257
        """
 
258
        if not self.lock.acquire(1.0):
 
259
            logging.warning("can't index: can't acquire lock")
 
260
            return
 
261
        try:
 
262
            self._unsign()
 
263
            start = time.time()
 
264
            request = self._indexingRequest(self.request)
 
265
            self._index_pages(request, files, mode)
 
266
            logging.info("indexing completed successfully in %0.2f seconds." %
 
267
                        (time.time() - start))
 
268
            self._sign()
 
269
        finally:
 
270
            self.lock.release()
 
271
 
 
272
    def indexPagesInNewThread(self, files=None, mode='update'):
 
273
        """ Index all pages in a new thread
 
274
 
 
275
        Should be called from a user request. From a script, use indexPages.
 
276
        """
 
277
        # Prevent rebuilding the index just after it was finished
 
278
        if self.exists():
 
279
            return
 
280
 
 
281
        from threading import Thread
 
282
        indexThread = Thread(target=self._index_pages, args=(files, mode))
 
283
        indexThread.setDaemon(True)
 
284
 
 
285
        # Join the index thread after current request finish, prevent
 
286
        # Apache CGI from killing the process.
 
287
        def joinDecorator(finish):
 
288
            def func():
 
289
                finish()
 
290
                indexThread.join()
 
291
            return func
 
292
 
 
293
        self.request.finish = joinDecorator(self.request.finish)
 
294
        indexThread.start()
 
295
 
 
296
    def _index_pages(self, request, files=None, mode='update'):
 
297
        """ Index all pages (and all given files)
 
298
 
 
299
        This should be called from indexPages or indexPagesInNewThread only!
 
300
 
 
301
        This may take some time, depending on the size of the wiki and speed
 
302
        of the machine.
 
303
 
 
304
        When called in a new thread, lock is acquired before the call,
 
305
        and this method must release it when it finishes or fails.
 
306
 
 
307
        @param request: current request
 
308
        @keyword files: iterator or list of files to index additionally
 
309
        @keyword mode: set the mode of indexing the pages, either 'update',
 
310
        'add' or 'rebuild'
 
311
        """
 
312
        raise NotImplemented('...')
 
313
 
 
314
    def _remove_item(self, writer, page, attachment=None):
 
315
        """ Remove a page and all its revisions from the index or just
 
316
            an attachment of that page
 
317
 
 
318
        @param pagename: name of the page to remove
 
319
        @keyword attachment: optionally, just remove this attachment
 
320
        """
 
321
        raise NotImplemented('...')
 
322
 
 
323
    def _do_queued_updates_InNewThread(self):
 
324
        """ do queued index updates in a new thread
 
325
 
 
326
        Should be called from a user request. From a script, use indexPages.
 
327
        """
 
328
        if not self.lock.acquire(1.0):
 
329
            logging.warning("can't index: can't acquire lock")
 
330
            return
 
331
        try:
 
332
            def lockedDecorator(f):
 
333
                def func(*args, **kwargs):
 
334
                    try:
 
335
                        return f(*args, **kwargs)
 
336
                    finally:
 
337
                        self.lock.release()
 
338
                return func
 
339
 
 
340
            from threading import Thread
 
341
            indexThread = Thread(
 
342
                    target=lockedDecorator(self._do_queued_updates),
 
343
                    args=(self._indexingRequest(self.request), ))
 
344
            indexThread.setDaemon(True)
 
345
 
 
346
            # Join the index thread after current request finish, prevent
 
347
            # Apache CGI from killing the process.
 
348
            def joinDecorator(finish):
 
349
                def func():
 
350
                    finish()
 
351
                    indexThread.join()
 
352
                return func
 
353
 
 
354
            self.request.finish = joinDecorator(self.request.finish)
 
355
            indexThread.start()
 
356
        except:
 
357
            self.lock.release()
 
358
            raise
 
359
 
 
360
    def _do_queued_updates(self, request, amount=5):
 
361
        """ Perform updates in the queues (read-lock acquired)
 
362
 
 
363
        @param request: the current request
 
364
        @keyword amount: how many updates to perform at once (default: 5)
 
365
        """
 
366
        raise NotImplemented('...')
 
367
 
 
368
    def optimize(self):
 
369
        """ Optimize the index if possible """
 
370
        raise NotImplemented('...')
 
371
 
 
372
    def contentfilter(self, filename):
 
373
        """ Get a filter for content of filename and return unicode content.
 
374
 
 
375
        @param filename: name of the file
 
376
        """
 
377
        request = self.request
 
378
        mt = wikiutil.MimeType(filename=filename)
 
379
        for modulename in mt.module_name():
 
380
            try:
 
381
                execute = wikiutil.importPlugin(request.cfg, 'filter', modulename)
 
382
                break
 
383
            except wikiutil.PluginMissingError:
 
384
                pass
 
385
            else:
 
386
                logging.info("Cannot load filter for mimetype %s" % modulename)
 
387
        try:
 
388
            data = execute(self, filename)
 
389
            logging.debug("Filter %s returned %d characters for file %s" % (modulename, len(data), filename))
 
390
        except (OSError, IOError), err:
 
391
            data = ''
 
392
            logging.warning("Filter %s threw error '%s' for file %s" % (modulename, str(err), filename))
 
393
        return mt.mime_type(), data
 
394
 
 
395
    def _indexingRequest(self, request):
 
396
        """ Return a new request that can be used for index building.
 
397
 
 
398
        This request uses a security policy that lets the current user
 
399
        read any page. Without this policy some pages will not render,
 
400
        which will create broken pagelinks index.
 
401
 
 
402
        @param request: current request
 
403
        """
 
404
        from MoinMoin.request.request_cli import Request
 
405
        from MoinMoin.security import Permissions
 
406
        request = Request(request.url)
 
407
        class SecurityPolicy(Permissions):
 
408
            def read(self, *args, **kw):
 
409
                return True
 
410
        request.user.may = SecurityPolicy(request.user)
 
411
        return request
 
412
 
 
413
    def _unsign(self):
 
414
        """ Remove sig file - assume write lock acquired """
 
415
        try:
 
416
            os.remove(self.sig_file)
 
417
        except OSError, err:
 
418
            if err.errno != errno.ENOENT:
 
419
                raise
 
420
 
 
421
    def _sign(self):
 
422
        """ Add sig file - assume write lock acquired """
 
423
        f = file(self.sig_file, 'w')
 
424
        try:
 
425
            f.write('')
 
426
        finally:
 
427
            f.close()
 
428
 
 
429
 
 
430
##############################################################################
 
431
### Searching
 
432
##############################################################################
 
433
 
 
434
class Search:
 
435
    """ A search run """
 
436
 
 
437
    def __init__(self, request, query, sort='weight', mtime=None,
 
438
            historysearch=0):
 
439
        """
 
440
        @param request: current request
 
441
        @param query: search query objects tree
 
442
        @keyword sort: the sorting of the results (default: 'weight')
 
443
        @keyword mtime: only show items newer than this timestamp (default: None)
 
444
        @keyword historysearch: whether to show old revisions of a page (default: 0)
 
445
        """
 
446
        self.request = request
 
447
        self.query = query
 
448
        self.sort = sort
 
449
        self.mtime = mtime
 
450
        self.historysearch = historysearch
 
451
        self.filtered = False
 
452
        self.fs_rootpage = "FS" # XXX FS hardcoded
 
453
 
 
454
    def run(self):
 
455
        """ Perform search and return results object """
 
456
        start = time.time()
 
457
        if self.request.cfg.xapian_search:
 
458
            hits = self._xapianSearch()
 
459
            logging.debug("_xapianSearch found %d hits" % len(hits))
 
460
        else:
 
461
            hits = self._moinSearch()
 
462
            logging.debug("_moinSearch found %d hits" % len(hits))
 
463
 
 
464
        # important - filter deleted pages or pages the user may not read!
 
465
        if not self.filtered:
 
466
            hits = self._filter(hits)
 
467
            logging.debug("after filtering: %d hits" % len(hits))
 
468
 
 
469
        # when xapian was used, we can estimate the numer of matches
 
470
        # Note: hits can't be estimated by xapian with historysearch enabled
 
471
        if not self.request.cfg.xapian_index_history and hasattr(self, '_xapianMset'):
 
472
            _ = self.request.getText
 
473
            mset = self._xapianMset
 
474
            m_lower = mset.get_matches_lower_bound()
 
475
            m_estimated = mset.get_matches_estimated()
 
476
            m_upper = mset.get_matches_upper_bound()
 
477
            estimated_hits = (m_estimated == m_upper and m_estimated == m_lower
 
478
                              and '' or _('about'), m_estimated)
 
479
        else:
 
480
            estimated_hits = None
 
481
 
 
482
        return getSearchResults(self.request, self.query, hits, start,
 
483
                self.sort, estimated_hits)
 
484
 
 
485
    # ----------------------------------------------------------------
 
486
    # Private!
 
487
 
 
488
    def _xapianIndex(request):
 
489
        """ Get the xapian index if possible
 
490
 
 
491
        @param request: current request
 
492
        """
 
493
        try:
 
494
            from MoinMoin.search.Xapian import Index
 
495
            index = Index(request)
 
496
        except ImportError:
 
497
            return None
 
498
 
 
499
        if index.exists():
 
500
            return index
 
501
 
 
502
    _xapianIndex = staticmethod(_xapianIndex)
 
503
 
 
504
    def _xapianSearch(self):
 
505
        """ Search using Xapian
 
506
 
 
507
        Get a list of pages using fast xapian search and
 
508
        return moin search in those pages if needed.
 
509
        """
 
510
        clock = self.request.clock
 
511
        pages = None
 
512
        index = self._xapianIndex(self.request)
 
513
 
 
514
        if index and self.query.xapian_wanted():
 
515
            clock.start('_xapianSearch')
 
516
            try:
 
517
                from MoinMoin.support import xapwrap
 
518
 
 
519
                clock.start('_xapianQuery')
 
520
                query = self.query.xapian_term(self.request, index.allterms)
 
521
                description = str(query)
 
522
                logging.debug("_xapianSearch: query = %r" % description)
 
523
                query = xapwrap.index.QObjQuery(query)
 
524
                enq, mset, hits = index.search(query, sort=self.sort,
 
525
                        historysearch=self.historysearch)
 
526
                clock.stop('_xapianQuery')
 
527
 
 
528
                logging.debug("_xapianSearch: finds: %r" % hits)
 
529
                def dict_decode(d):
 
530
                    """ decode dict values to unicode """
 
531
                    for key in d:
 
532
                        d[key] = d[key].decode(config.charset)
 
533
                    return d
 
534
                pages = [dict_decode(hit['values']) for hit in hits]
 
535
                logging.debug("_xapianSearch: finds pages: %r" % pages)
 
536
 
 
537
                self._xapianEnquire = enq
 
538
                self._xapianMset = mset
 
539
                self._xapianIndex = index
 
540
            except BaseIndex.LockedException:
 
541
                pass
 
542
            #except AttributeError:
 
543
            #    pages = []
 
544
 
 
545
            try:
 
546
                # xapian handled the full query
 
547
                if not self.query.xapian_need_postproc():
 
548
                    clock.start('_xapianProcess')
 
549
                    try:
 
550
                        return self._getHits(hits, self._xapianMatch)
 
551
                    finally:
 
552
                        clock.stop('_xapianProcess')
 
553
            finally:
 
554
                clock.stop('_xapianSearch')
 
555
        elif not index:
 
556
            # we didn't use xapian in this request because we have no index,
 
557
            # so we can just disable it until admin builds an index and
 
558
            # restarts moin processes
 
559
            self.request.cfg.xapian_search = 0
 
560
 
 
561
        # some postprocessing by _moinSearch is required
 
562
        return self._moinSearch(pages)
 
563
 
 
564
    def _xapianMatchDecider(self, term, pos):
 
565
        """ Returns correct Match object for a Xapian match
 
566
 
 
567
        @param term: the term as string
 
568
        @param pos: starting position of the match
 
569
        """
 
570
        if term[0] == 'S': # TitleMatch
 
571
            return TitleMatch(start=pos, end=pos+len(term)-1)
 
572
        else: # TextMatch (incl. headers)
 
573
            return TextMatch(start=pos, end=pos+len(term))
 
574
 
 
575
    def _xapianMatch(self, uid, page=None):
 
576
        """ Get all relevant Xapian matches per document id
 
577
 
 
578
        @param uid: the id of the document in the xapian index
 
579
        """
 
580
        positions = {}
 
581
        term = self._xapianEnquire.get_matching_terms_begin(uid)
 
582
        while term != self._xapianEnquire.get_matching_terms_end(uid):
 
583
            term_name = term.get_term()
 
584
            for pos in self._xapianIndex.termpositions(uid, term.get_term()):
 
585
                if pos not in positions or \
 
586
                        len(positions[pos]) < len(term_name):
 
587
                    positions[pos] = term_name
 
588
            term.next()
 
589
        matches = [self._xapianMatchDecider(term, pos) for pos, term
 
590
            in positions.iteritems()]
 
591
 
 
592
        if not matches:
 
593
            return [Match()] # dummy for metadata, we got a match!
 
594
 
 
595
        return matches
 
596
 
 
597
    def _moinSearch(self, pages=None):
 
598
        """ Search pages using moin's built-in full text search
 
599
 
 
600
        Return list of tuples (page, match). The list may contain
 
601
        deleted pages or pages the user may not read.
 
602
 
 
603
        @keyword pages: optional list of pages to search in
 
604
        """
 
605
        self.request.clock.start('_moinSearch')
 
606
        if pages is None:
 
607
            # if we are not called from _xapianSearch, we make a full pagelist,
 
608
            # but don't search attachments (thus attachment name = '')
 
609
            pages = [{'pagename': p, 'attachment': '', 'wikiname': 'Self', } for p in self._getPageList()]
 
610
        hits = self._getHits(pages, self._moinMatch)
 
611
        self.request.clock.stop('_moinSearch')
 
612
        return hits
 
613
 
 
614
    def _moinMatch(self, page, uid=None):
 
615
        """ Get all matches from regular moinSearch
 
616
 
 
617
        @param page: the current page instance
 
618
        """
 
619
        if page:
 
620
            return self.query.search(page)
 
621
 
 
622
    def _getHits(self, pages, matchSearchFunction):
 
623
        """ Get the hit tuples in pages through matchSearchFunction """
 
624
        logging.debug("_getHits searching in %d pages ..." % len(pages))
 
625
        hits = []
 
626
        revisionCache = {}
 
627
        fs_rootpage = self.fs_rootpage
 
628
        for hit in pages:
 
629
            if 'values' in hit:
 
630
                valuedict = hit['values']
 
631
                uid = hit['uid']
 
632
            else:
 
633
                valuedict = hit
 
634
                uid = None
 
635
 
 
636
            wikiname = valuedict['wikiname']
 
637
            pagename = valuedict['pagename']
 
638
            attachment = valuedict['attachment']
 
639
            logging.debug("_getHits processing %r %r %r" % (wikiname, pagename, attachment))
 
640
 
 
641
            if 'revision' in valuedict and valuedict['revision']:
 
642
                revision = int(valuedict['revision'])
 
643
            else:
 
644
                revision = 0
 
645
 
 
646
            if wikiname in (self.request.cfg.interwikiname, 'Self'): # THIS wiki
 
647
                page = Page(self.request, pagename, rev=revision)
 
648
                if not self.historysearch and revision:
 
649
                    revlist = page.getRevList()
 
650
                    # revlist can be empty if page was nuked/renamed since it was included in xapian index
 
651
                    if not revlist or revlist[0] != revision:
 
652
                        # nothing there at all or not the current revision
 
653
                        continue
 
654
                if attachment:
 
655
                    if pagename == fs_rootpage: # not really an attachment
 
656
                        page = Page(self.request, "%s/%s" % (fs_rootpage, attachment))
 
657
                        hits.append((wikiname, page, None, None))
 
658
                    else:
 
659
                        matches = matchSearchFunction(page=None, uid=uid)
 
660
                        hits.append((wikiname, page, attachment, matches))
 
661
                else:
 
662
                    matches = matchSearchFunction(page=page, uid=uid)
 
663
                    logging.debug("matchSearchFunction %r returned %r" % (matchSearchFunction, matches))
 
664
                    if matches:
 
665
                        if not self.historysearch and \
 
666
                                pagename in revisionCache and \
 
667
                                revisionCache[pagename][0] < revision:
 
668
                            hits.remove(revisionCache[pagename][1])
 
669
                            del revisionCache[pagename]
 
670
                        hits.append((wikiname, page, attachment, matches))
 
671
                        revisionCache[pagename] = (revision, hits[-1])
 
672
            else: # other wiki
 
673
                hits.append((wikiname, pagename, attachment, None, revision))
 
674
        return hits
 
675
 
 
676
    def _getPageList(self):
 
677
        """ Get list of pages to search in
 
678
 
 
679
        If the query has a page filter, use it to filter pages before
 
680
        searching. If not, get a unfiltered page list. The filtering
 
681
        will happen later on the hits, which is faster with current
 
682
        slow storage.
 
683
        """
 
684
        filter_ = self.query.pageFilter()
 
685
        if filter_:
 
686
            # There is no need to filter the results again.
 
687
            self.filtered = True
 
688
            return self.request.rootpage.getPageList(filter=filter_)
 
689
        else:
 
690
            return self.request.rootpage.getPageList(user='', exists=0)
 
691
 
 
692
    def _filter(self, hits):
 
693
        """ Filter out deleted or acl protected pages
 
694
 
 
695
        @param hits: list of hits
 
696
        """
 
697
        userMayRead = self.request.user.may.read
 
698
        fs_rootpage = self.fs_rootpage + "/"
 
699
        thiswiki = (self.request.cfg.interwikiname, 'Self')
 
700
        filtered = [(wikiname, page, attachment, match)
 
701
                for wikiname, page, attachment, match in hits
 
702
                    if (not wikiname in thiswiki or
 
703
                       page.exists() and userMayRead(page.page_name) or
 
704
                       page.page_name.startswith(fs_rootpage)) and
 
705
                       (not self.mtime or self.mtime <= page.mtime_usecs()/1000000)]
 
706
        return filtered
 
707