Attachment 'GlobalSearchAndReplace.py'

Download

   1 # -*- coding: utf-8 -*-
   2 """
   3 MoinMoin - GlobalSearchAndReplace action
   4 
   5 This action allows you to select a set of pages by a search pattern
   6 and do a search and replace on each page from the set in one go.
   7 It offers a preview feature that lets you review what would edited
   8 before doing the actual replace.
   9 
  10 To hide misleading links in preview add the following to common.css of your
  11 theme:
  12 
  13 .global-replace-diff .diff-title {
  14     visibility: collapse;
  15 }
  16 
  17 Adapted to moin-1.9.8 and extended by David Linke.
  18 Based on version 1.0 of "SearchAndReplaceMultiplePages.py" downloaded
  19 from http://www.egenix.com/library/moinmoin/
  20 
  21 Written by Marc-Andre Lemburg <mal@egenix.com>,
  22 Based on the RenamePage action by Jürgen Hermann <jh@web.de>
  23 
  24 @copyright: 2015-2018, David Linke, MoinMoin:DavidLinke, https://github.com/dalito
  25 @copyright: 2008, eGenix.com Software, Skills and Services GmbH <info@egenix.com>
  26 @license: GNU GPL 2.0, see COPYING for details.
  27 """
  28 
  29 import re
  30 
  31 from MoinMoin import wikiutil
  32 from MoinMoin.PageEditor import PageEditor
  33 from MoinMoin.action import ActionBase
  34 from MoinMoin import log
  35 from MoinMoin.util import diff_html
  36 
  37 __version__ = '2.0'
  38 
  39 # Should only the superuser be allowed to use this action ?
  40 SUPERUSER_ONLY = False
  41 
  42 logging = log.getLogger(__name__)
  43 
  44 
  45 class GlobalSearchAndReplace(ActionBase):
  46     """Search and replace on multiple pages action
  47 
  48     Note: the action name is the class name
  49     """
  50 
  51     def __init__(self, pagename, request):
  52         ActionBase.__init__(self, pagename, request)
  53         self.page = PageEditor(self.request, pagename)
  54         self.orig_page = pagename
  55         self.feedback = u''
  56         self.no_refused_access = 0
  57 
  58     def is_allowed(self, pagename=None):
  59         """Check if user is allowed to access page"""
  60         if pagename is None:
  61             pagename = self.pagename
  62         may = self.request.user.may
  63         return may.write(pagename)
  64 
  65     def render(self):
  66         """
  67         Render action
  68 
  69         This action returns a wiki page with optional message, or
  70         redirect to original page.
  71         """
  72         _ = self.request.getText
  73         form = self.request.form
  74 
  75         if 'cancel' in form:
  76             # User cancelled
  77             return self.page.send_page()
  78 
  79         # Validate user rights and page state. If we get error here, we
  80         # return an error message, without the rename form.
  81         error = None
  82         if self.is_excluded():
  83             error = _('Action %(actionname)s is excluded in this wiki!') % {
  84                 'actionname': self.actionname}
  85         elif SUPERUSER_ONLY and not self.request.user.isSuperUser():
  86             error = _(u'Only superusers are allowed to use this action.')
  87         elif not self.page.exists():
  88             error = _(u'This page is already deleted or was never created!')
  89         if error:
  90             # Send page with an error message
  91             self.request.theme.add_msg(msg=error)
  92             return self.page.send_page()
  93 
  94         if not form.get('searchtext'):
  95             self.request.theme.add_msg(_(u'Please fill in a search text and '
  96                                          '(optional) a replacement text!'),
  97                                        'info')
  98         # Run search & replace on the pages
  99         elif ('replace' in form or 'preview' in form) and 'ticket' in form:
 100             self.replace()
 101 
 102 #        logging.info('form in search and replace: %s' % repr(form))
 103 
 104         # Show the form (with error or feedback information)
 105         self.request.theme.add_msg(self.makeform(), 'info')
 106         return self.page.send_page()
 107 
 108     def replace(self):
 109         """Replace text on pages matching a regexp"""
 110         _ = self.request.getText
 111         form = self.request.form
 112         formatter = self.request.formatter
 113         self.feedback = u''
 114         self.no_refused_access = 0
 115 
 116         # Require a valid ticket. Make outside attacks harder by
 117         # requiring two full HTTP transactions
 118         if not wikiutil.checkTicket(self.request, form['ticket']):
 119             self.error = _(u'Please use the interactive user interface to '
 120                            'do search & replace on multiple pages!')
 121             return
 122 
 123         # Get new name from form and normalize.
 124         comment = form.get('comment', [u''])
 125         comment = wikiutil.clean_input(comment)
 126 
 127         pagename = form.get('pagename')
 128         pagename = wikiutil.normalize_pagename(pagename, self.cfg)
 129 
 130         searchtext = form.get('searchtext')
 131         replacetext = form.get('replacetext', '')
 132 
 133         # test if regular expressions are OK
 134         try:
 135             re.compile(pagename)
 136         except Exception as err:
 137             self.error = _(u'Error in page name pattern: %s' % err)
 138             return
 139         try:
 140             re.compile(searchtext)
 141         except Exception as err:
 142             self.error = _(u'Error in search text pattern: %s' % err)
 143             return
 144 
 145         # Get list of all pages
 146         pages = self.request.rootpage.getPageList(user='', exists='')
 147         # Check which page names match
 148         pages_to_update = [p for p in pages if re.search(pagename, p)]
 149 
 150         self.feedback += formatter.rule()
 151         self.feedback += formatter.paragraph(1)
 152         self.feedback += _('Found %i matching pagename(s).' %
 153                            len(pages_to_update))
 154         self.feedback += formatter.paragraph(0)
 155 
 156         if not pages_to_update:
 157             return
 158 
 159         self.feedback += formatter.rule()
 160         if 'preview' not in self.request.form:
 161             self.feedback += formatter.paragraph(1)
 162             self.feedback += _('Updating pages...')
 163             self.feedback += formatter.paragraph(0)
 164             self.feedback += formatter.bullet_list(1)
 165 
 166         # replace on all matching pages one by one
 167         for page in pages_to_update:
 168             self.replace_on_page(page, searchtext, replacetext, comment)
 169 
 170         if 'preview' not in self.request.form:
 171             self.feedback += formatter.bullet_list(0)
 172 
 173         if self.no_refused_access:
 174             self.feedback += formatter.paragraph(1)
 175             self.feedback += formatter.smiley('<!>')
 176             self.feedback += _(u' %i pages skipped because of access '
 177                                'restrictions.' % self.no_refused_access)
 178             self.feedback += formatter.paragraph(0)
 179 
 180     def replace_on_page(self, pagename, searchtext, replacetext, comment):
 181         """Open page for editing and do search and replace in raw code
 182         """
 183         _ = self.request.getText
 184         formatter = self.request.formatter
 185 
 186         # Check permissions. Don't reveal the page name, just count.
 187         if not self.is_allowed(pagename):
 188             self.no_refused_access += 1
 189             return
 190 
 191         # Open page
 192         page = PageEditor(self.request, pagename)
 193 
 194         # Get page text
 195         oldtext = page.get_raw_body()
 196 
 197         if (self.request.form.get('ignore_pi', False)
 198                 and oldtext.startswith('#')):
 199             # do not replace in processing instruction (pi) / header
 200             header, content = [], []
 201             in_header = True
 202             for line in oldtext.split('\n'):
 203                 if line.startswith('#') and in_header:
 204                     header.append(line)
 205                 else:
 206                     content.append(line)
 207                     in_header = False
 208             # Apply replacements
 209             try:
 210                 newcontent = re.sub(searchtext, replacetext, u'\n'.join(content))
 211             except Exception as err:
 212                 self.error = _(u'Error in replace text pattern: %s' % err)
 213                 return
 214             newtext = u'%s\n%s' % (u'\n'.join(header), newcontent)
 215         else:
 216             # Apply replacements
 217             try:
 218                 newtext = re.sub(searchtext, replacetext, oldtext)
 219             except Exception as err:
 220                 self.error = _(u'Error in replace text pattern: %s' % err)
 221                 return
 222 
 223         # Either save page text or show preview of edit, if the text changed.
 224         if newtext.strip() != oldtext.strip():  # avoid hit on changed tail
 225             if 'preview' in self.request.form:
 226                 self.feedback += formatter.paragraph(1)
 227                 self.feedback += _('Would edit page <b>%s</b> as follows:' %
 228                                    wikiutil.escape(pagename))
 229                 self.feedback += formatter.paragraph(0)
 230                 self.feedback += u'<div class="global-replace-diff">'
 231                 self.feedback += formatter.rawHTML(
 232                     diff_html.diff(self.request, oldtext, newtext)
 233                 )
 234                 self.feedback += u'</div>\n'
 235             else:
 236                 try:
 237                     page.saveText(newtext, 0, comment=comment)
 238                     self.feedback += formatter.listitem(1)
 239                     self.feedback += formatter.smiley('(./)')
 240                     self.feedback += _('%s' % wikiutil.escape(pagename))
 241                     self.feedback += formatter.listitem(0)
 242                 except PageEditor.SaveError, reason:
 243                     self.error += _(u'Cannot save page "%s": %s ! <br>' %
 244                                     (wikiutil.escape(pagename), reason))
 245                     return
 246 
 247     def makeform(self):
 248         """Display a search&replace page form
 249 
 250         The form also provides error feedback in case there was an
 251         error during the replace.
 252         """
 253         from MoinMoin.widget.dialog import Dialog
 254         _ = self.request.getText
 255 
 256         error = ''
 257         if self.error:
 258             error = u'<p class="error">%s</p>\n' % self.error
 259 
 260         form = self.request.form
 261         namespace = {
 262             'error': error,
 263             'feedback': self.feedback,
 264             'action': self.__class__.__name__,
 265             'ticket': wikiutil.createTicket(self.request),
 266             'pagename': form.get('pagename', self.orig_page),
 267             'ignore_pi': 'checked' if form.get('ignore_pi', False) else '',
 268             'searchtext': form.get('searchtext', ''),
 269             'replacetext': form.get('replacetext', ''),
 270             'comment': form.get('comment', ''),
 271             'replace': _(u'Search & Replace'),
 272             'preview': _(u'Preview'),
 273             'cancel': _(u'Cancel'),
 274             'pagename_label': _(u"Page name pattern"),
 275             'ignore_pi_label': _(u"Skip processing instructions in header"),
 276             'searchtext_label': _(u"Search text pattern"),
 277             'replacetext_label': _(u"Replace text pattern"),
 278             'comment_label': _(u"Optional reason for the replacement"),
 279             'note': _(u"Note: Page name and search pattern may use regular "
 280                       "expression syntax (Python re-module syntax). "
 281                       "Groups can be referenced in the replace pattern "
 282                       "using \\1, \\2, etc."),
 283         }
 284         form = """
 285 %(error)s
 286 <form method="post" action="">
 287 <input type="hidden" name="action" value="%(action)s">
 288 <input type="hidden" name="ticket" value="%(ticket)s">
 289 <table>
 290     <tr>
 291         <td class="label"><label>%(pagename_label)s</label></td>
 292         <td class="content">
 293             <input type="text" name="pagename" size="60" value="%(pagename)s">
 294         </td>
 295     </tr>
 296     <tr>
 297         <td class="label"><label>%(ignore_pi_label)s</label></td>
 298         <td class="content">
 299             <input type="checkbox" name="ignore_pi" value="1" %(ignore_pi)s>
 300         </td>
 301     </tr>
 302     <tr>
 303         <td class="label"><label>%(searchtext_label)s</label></td>
 304         <td class="content">
 305             <input type="text" name="searchtext" size="60" value="%(searchtext)s">
 306         </td>
 307     </tr>
 308     <tr>
 309         <td class="label"><label>%(replacetext_label)s</label></td>
 310         <td class="content">
 311             <input type="text" name="replacetext" size="60" value="%(replacetext)s">
 312         </td>
 313     </tr>
 314     <tr>
 315         <td class="label"><label>%(comment_label)s</label></td>
 316         <td class="content">
 317             <input type="text" name="comment" size="60" maxlength="80" value="%(comment)s">
 318         </td>
 319     </tr>
 320     <tr>
 321         <td></td>
 322         <td class="buttons">
 323             <input type="submit" name="preview" value="%(preview)s">
 324             <input type="submit" name="replace" value="%(replace)s">
 325             <input type="submit" name="cancel" value="%(cancel)s">
 326         </td>
 327     </tr>
 328 </table>
 329 <p style="font-weight: normal">%(note)s</p>
 330 <p>%(feedback)s</p>
 331 </form>
 332 """ % namespace
 333 
 334         return Dialog(self.request, content=form)
 335 
 336 
 337 def execute(pagename, request):
 338     """Glue code for actions"""
 339     GlobalSearchAndReplace(pagename, request).render()

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2018-03-15 00:01:55, 23.4 KB) [[attachment:GlobalSearchAndReplace.png]]
  • [get | view] (2018-03-14 23:56:01, 12.7 KB) [[attachment:GlobalSearchAndReplace.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.