MediaWiki:Gadget-GalleryDetails.js

Revision as of 17:14, 16 August 2020 by PeaceDeadC (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <source lang="javascript">

/*
   Gallery details - reformat galleries to have one image below the next, and display extended image
   information next to them. Works also on categories and Special:NewFiles. On Special:NewFiles, it
   runs automatically unless you define

   var gallery_details_newfiles_run = false;

   in your monobook.js or other skin-specific JS file.

   Original version written by Magnus Manske, with some partial fixes by others.
   License of original: unknown.

   Complete rewrite January 2009 by User:Lupo. 
   Author: [[User:Lupo]], January 2009
   License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
*/
if (typeof (GalleryDetails) == 'undefined') { // Guard against double imports

importScript ('MediaWiki:Utilities.js');  // Generally useful operations
importScript ('MediaWiki:ImageLinks.js'); // Useful img links (Tineye search, del links for admins)

if (typeof (gallery_details_newfiles_run) == 'undefined')
  var gallery_details_newfiles_run = true;

if (typeof (gallery_details_close_windows) == 'undefined')
  var gallery_details_close_windows = false;

var gallery_details_good_tags = [
 "GFDL",
 "CC-",
 "Cc-",
 "PD",
 "Pd",
 "Public domain",
 "Copyrighted free use",
 "Attribution",
 "Images requiring attribution",
 "FAL",
 "UK Government images",
 "User-created GFDL images",
 "Items with OTRS permission confirmed",
 "GPL",
 "LGPL"
];
 
var gallery_details_action_tags = [
 "Flickr review needed",
 "Flickr images needing",
 "Recent unfree Flickr images"
];
 
var gallery_details_bad_tags = [
 "Copyright violations",
 "Images without ",
 "Media without ",
 "Media missing ",
 "Unknown as of ",
 "Deletion requests",
 "Other speedy deletions",
 "Possibly unfree"
];

var gallery_details_sidebar =
{ text : 'Gallery details'
 ,tip  : 'Display info on images'
}; 
var gallery_details_nom_buttons = [{
            label:'Deletion\xa0request...',
            tip: 'Nominate the file for deletion and notify uploader',
            del:true
   
         }, {
            label: 'Copyvio...',
            tip: 'Mark as copyvio (prompting for a reason) and notify uploader',
            tag: '{{copyvio|1=%PARAMETER%}}',
            talk_tag: '{{subst:copyvionote|1=%FILE%}}',
            img_summary: 'Marking as possible copyvio because %PARAMETER%',
            talk_summary: 'Notification of possible copyright violation',
            prompt_text: "Why is this file a copyright violation?"
 
         }, {
            label: 'No\xa0source',
            tip  : 'Mark as missing source info and notify uploader',
            tag: '{{subst:nsd}}',
            talk_tag: '{{subst:image source|1=%FILE%}}',
            img_summary: 'File has no source',
            talk_summary: '%FILE% does not have a source'
 
         }, {
            label: 'No\xa0permission',
            tip  : 'Mark as missing permission and notify uploader',
            tag: '{{subst:npd}}',
            talk_tag: '{{subst:image permission|1=%FILE%}}',
            img_summary: 'Missing permission',
            talk_summary: 'Please send permission for %FILE% to [[COM:OTRS|OTRS]]'
 
         }, {
            label: 'No\xa0license',
            tip  : 'Mark as missing license info and notify uploader',
            tag: '{{subst:nld}}',
            talk_tag: '{{subst:image license|1=%FILE%}}',
            img_summary: 'Missing license',
            talk_summary: '%FILE% does not have a license'
 
         }];

// titleFromLink
//   Extract a page title from a href of a link node. Works for both types of hrefs:
//   https://foo.wikipedia.org/w/index.php?title=PageName and also /wiki/PageName
//
// Parameters
//   node    DOM node   The link node from which the page name is to be extracted
 
function titleFromLink (node)
{
  if (!node || typeof (node.getAttribute) != 'function') return "";
  return titleFromHref (node.getAttribute ('href', 2));
  // The ", 2" is for IE. IE sometimes has problems with encoded UTF-8 characters: using any other
  // way to get the href, it insists to decode the value, but does do so wrongly. For more info,
  // see https://commons.wikimedia.org/wiki/MediaWiki_talk:Gadget-HotCat.js/Archive01#Questions
}

// Localization hook. Check [[MediaWiki:Gadget-GalleryDetails.js/de]] for an example.
if (mw.config.get('wgUserLanguage') != 'en') importScript ('MediaWiki:Gadget-GalleryDetails.js/' + mw.config.get('wgUserLanguage'));

var GalleryDetailsViewer = function () {};
GalleryDetailsViewer.prototype =
{
  display : function (model) // Redefine this in children
  {
  },

  toggle : function (model) // Redefine this in children
  {
  },
  
  // Default implementations
  
  check_info : function (info)
  {
    if (!info.done)
      return document.createTextNode ('Request failed.');
    else if (!info.page || typeof (info.page.missing) != 'undefined')
      return document.createTextNode ('File not found?!');
    
    return null;
  },
  
  get_exif_data : function (info, keys)
  {
    if (!info || !info.page || !info.page.imageinfo) return null;
    if (!keys) keys = ['Model', 'Make', 'DateTime', 'DateTimeOriginal'];

    function find_metadata (list, keys)
    {
      if (!list || list.length == 0) return null;
      var found = 0;
      var result = {};
      var all = !keys || keys.length == 0;
      for (var i = 0; i < list.length && (all || found < keys.length); i++) {
        if (all) {
          result[list[i].name] = list[i].value;
          found++;
        } else {
          for (var j = 0; j < keys.length; j++) {
            if (list[i].name == keys[j]) {
              result[list[i].name] = list[i].value;
              found++;
              break;
            }
          }
        }
      }
      if (found == 0) return null; else return result;
    }

    var imagedata = info.page.imageinfo[0];
    var result = [];
    var exif = find_metadata (imagedata.metadata, keys);
    var span = null;
    if (exif) {
      if (exif['Model']) {
        if (exif['Make'] && exif['Model'].indexOf (exif['Make']) >= 0) exif['Make'] = null;
        span = document.createElement ('span');
        span.appendChild (document.createTextNode ('Camera: '));
        if (exif['Make']) span.appendChild (document.createTextNode (exif['Make'] + ' '));
        var a = makeRawLink (
                    exif['Model']
                  , 'https://en.wikipedia.org/wiki/' + encodeURI (exif['Model'])
                );
        a.className = 'external';
        a.style.background = 'none';
        a.style.padding = '0';
        span.appendChild (a);
        result[result.length] = span;
      }
      var time = exif['DateTime'] || exif['DateTimeOriginal'] || null;
      if (time) {
        span = document.createElement ('span');
        span.appendChild (document.createTextNode ('EXIF time: ' + time));
        result[result.length] = span;
      }
    }
    return result;
  },

  create_details_box : function (info)
  {
    var page  = info.page;
    var title = info.name;
    var imagedata = page.imageinfo[0];
    var users = info.users;
    var size = imagedata.size;
    var width = imagedata.width;
    var height = imagedata.height;
    var url = imagedata.url;
    var comment = imagedata.comment;
    var timestamp = imagedata.timestamp;

    // Output generation: image info
    var t = document.createElement ('span');
    t.appendChild (document.createTextNode (width + "×" + height + "px"));
    if (width < 600 && height < 600 && width > 0 && height > 0) t.style.color = 'red';
    var ndiv = document.createElement ('div');
    ndiv.className = 'gallery_details_box';
    ndiv.style.border = '3px solid '
      + (info.status == GalleryDetails.BAD_FILE
         ? 'red'
         : (info.status == GalleryDetails.GOOD_FILE ? 'green' : 'yellow')
        );
    ndiv.style.margin = "5px";
    ndiv.style.padding = "5px";
    ndiv.appendChild (this.make_wiki_link (page.title));
    ndiv.appendChild (document.createTextNode (' ('));
    ndiv.appendChild (
      makeRawLink (
          'edit'
        , mw.config.get('wgArticlePath').replace ('$1', encodeURI (page.title) + '?action=edit')
      )
    );
    ndiv.appendChild (document.createTextNode (', '));
    ndiv.appendChild (
      makeRawLink (
          'hist'
        , mw.config.get('wgArticlePath').replace ('$1', encodeURI (page.title) + '?action=history')
      )
    );
    ndiv.appendChild (document.createTextNode (')'));
    ndiv.appendChild (document.createElement ('br'));
    if (timestamp) {
      timestamp = timestamp.replace ('T', '\xa0').replace ('Z', '\xa0(UTC)');
      ndiv.appendChild (document.createTextNode (timestamp));
      ndiv.appendChild (document.createElement ('br'));      
    }
    ndiv.appendChild (t);
    if (page.imageinfo.length > 1) {
      t = document.createElement ('b');
      t.appendChild (document.createTextNode (' (Re-upload)'));
      ndiv.appendChild (t);
    }
    ndiv.appendChild (document.createElement ('br'));
    if (imagedata.metadata) {
      var exif = this.get_exif_data (info);
      if (exif && exif.length > 0) {
        for (var i = 0; i < exif.length; i++) {
          ndiv.appendChild (exif[i]);
          ndiv.appendChild (document.createElement ('br'));
        }
      }
    }
    ndiv.appendChild (this.make_wiki_link ('File_talk:' + title));
    ndiv.appendChild (document.createElement ('br'));
    var user = this.create_user_link (ndiv, users[0]);
    if ((user == 'Rotatebot' || user == 'FlickreviewR' || user == 'Cropbot' || user == 'Picasa Review Bot') && users.length > 1) {
      ndiv.appendChild (document.createElement ('br'));
      t = document.createElement ('b');
      t.appendChild (document.createTextNode ('Real: '));
      ndiv.appendChild (t);
      user = this.create_user_link (ndiv, users[1]);
    }
    if (info.categories && info.categories.length > 0) {
      ndiv.appendChild (document.createElement ('hr'));            
      for (var i = 0; i < info.categories.length; i++) {
        ndiv.appendChild (this.make_wiki_link (info.categories[i]));
        if (i+1 < info.categories.length) ndiv.appendChild (document.createElement ('br'));
      }
    }
    var text = page.revisions;
    if (text) text = text[0];
    this.create_action_links (ndiv, page.title, user, text ? text.revid : null, url);
    return ndiv;
  },
  
  create_user_link : function (box, usernames)
  {
    var user = usernames.user;
    var proxy = null;

    if (usernames.real) {
      proxy = user; user = usernames.real;
    }
    box.appendChild (this.make_wiki_link ('User:' + user));
    box.appendChild (document.createTextNode (' ('));
    box.appendChild (this.make_wiki_link ('User_talk:' + user, 'talk'));
    box.appendChild (document.createTextNode (', '));
    box.appendChild (
      makeRawLink (
          'logs'
        , mw.config.get('wgArticlePath').replace ('$1', 'Special:Log') + '?user=' + encodeURIComponent (user)
      )
    );
    box.appendChild (document.createTextNode (')'));
    if (proxy) {
      box.appendChild (document.createTextNode (' via\xa0'));
      box.appendChild (this.make_wiki_link ('User:' + proxy, proxy));
      box.appendChild (document.createTextNode (' ('));
      box.appendChild (
        makeRawLink (
            'logs'
          , mw.config.get('wgArticlePath').replace ('$1', 'Special:Log') + '?user='
            + encodeURIComponent (proxy)
        )
      );
      box.appendChild (document.createTextNode (')'));
    }
    return user;
  },

  create_page_box : function (info)
  {
    var text = info.page.revisions[0];
    if (!text) return null;
    // Output generation: image page text 
    text = text["*"];
    if (!text) return document.createTextNode ('\xa0');
    var ndiv = document.createElement ('div');
    ndiv.className = 'gallery_details_page_box';
    ndiv.style.margin = "5px";
    ndiv.style.padding = "5px";
    ndiv.style.fontSize = "8pt";
    ndiv.style.lineHeight = "9pt";
    text = text.split('\n');
    for (var i = 0; i < text.length; i++) {
      var s = text[i];
      var url_re = /^(.*?)(https?:\/\/[^ \]\}"\|<]*)(.*)$/; // " Fix syntax coloring
      while (s.length > 0) {
        var matches = url_re.exec (s);
        if (matches && matches.length > 0) {
          if (matches[1].length > 0)
            ndiv.appendChild (document.createTextNode (matches[1]));
          var a = makeRawLink (matches[2], matches[2]);
          a.className = 'external';
          a.style.background = 'none';
          a.style.padding = '0';
          ndiv.appendChild (a);
          s = matches[3];
        } else {
          ndiv.appendChild (document.createTextNode (s));
          break;
        }
      }
      if (i + 1 < text.length) ndiv.appendChild (document.createElement ('br'));
    }
    return ndiv;
  },
  
  create_action_links :  function (ndiv, title, user, rev_id, img_url)
  {
    var tools = $('<div style="font-size: small; background: #DDD;"></div>');

    $.each(gallery_details_nom_buttons, function (k, v) {
       var link = $('<a href="#" title="' + v.tip + '">'+v.label+'</a>');
       if (v.del) {
         link.click(function(event) {
           event.preventDefault();
           AjaxQuickDelete.nominateForDeletion(title);
         });
       } else {
         link.click(function(event) {
           event.preventDefault();
           AjaxQuickDelete.insertTagOnPage(v.tag, v.img_summary, v.talk_tag, v.talk_summary, v.prompt_text, title);
         });
       }
       if (k>0) tools.append('\xa0| ');
       tools.append(link);
    });
    tools = tools.get(0);
    
    var additional_links = ImageLinks.get_links (title, user, rev_id, img_url);
    // If we have additional links, those from the first group may be added for anyone.
    if (additional_links && additional_links.length > 0) {
      if (additional_links[0] && additional_links[0].length > 0) {
        for (var i = 0; i < additional_links[0].length; i++) {
          tools.appendChild (document.createTextNode ('\xa0| '));
          tools.appendChild (additional_links[0][i]);
        }
      }
      if (mw.config.get('wgUserGroups').join (' ').indexOf ('sysop') >= 0 && additional_links.length > 1) {
        tools.appendChild (document.createElement ('hr'));
        tools.appendChild (document.createTextNode ('Quick deletions (without user notification)'));
        tools.appendChild (document.createElement ('br'));
        for (var i = 1; i < additional_links.length; i++) {
          if (additional_links[i]) {
            for (var j = 0; j < additional_links[i].length; j++) {
              if (j > 0) tools.appendChild (document.createTextNode ('\xa0| '));
              tools.appendChild (additional_links[i][j]);
            }
            if (i+1 < additional_links.length) tools.appendChild (document.createElement ('br'));
          }
        }
      }
    }
    ndiv.appendChild (tools);
  },

  make_wiki_link : function (title, text)
  {
    if (!text) text = title.replace (/_/g, ' ');
    var result =
      makeRawLink (
          text
        , mw.config.get('wgArticlePath').replace ('$1', encodeURI (title.replace (/ /g, '_')))
        , null
        , text);
    return result;
  }
};

var GalleryDetailsTableViewer = function () {};

GalleryDetailsTableViewer.prototype = new GalleryDetailsViewer;
GalleryDetailsTableViewer.prototype.display =
  function (model)
  {
    // Display the images in the model in a new table, one image per row.
    this.view = document.createElement ('table');
    this.view.className = 'gallery_details';
    this.view.style.display = 'none';
    model.root.parentNode.insertBefore (this.view, model.root);
    for (var i = 0; i < model.nof_images; i++) {
      var existing = model.image_list[i];
      if (!existing.box.parentNode) continue; // This image is no longer in the table?!
      var row = this.view.insertRow (-1);
      var cell = null;
      if (existing.box.nodeName.toLowerCase () == 'table') {
        cell = document.createElement ('td');
        cell.appendChild (existing.box.cloneNode (true));
        cell.colSpan = "2";
        row.appendChild (cell);
        row = this.view.insertRow (-1);
      } else if (existing.box.nodeName.toLowerCase () == 'li') { // MW1.17
        cell = document.createElement('td');
        var toCopy = existing.box.firstChild;
        while (toCopy && toCopy.nodeType != 1) toCopy = toCopy.nextSibling;
        if (toCopy) {
          cell.appendChild (toCopy.cloneNode (true));
          row.appendChild (cell);
        }
      } else { // pre MW1.17 gallery; existing.box.parentNode is a table cell
        cell = existing.box.parentNode.cloneNode (true);
        row.appendChild (cell);
      }
      cell = document.createElement ('td');
      var details      = this.check_info (existing);
      var page_content = null;
      if (!details) {
        details = this.create_details_box (existing);
        page_content = this.create_page_box (existing) || document.createTextNode ('No info found.');
      }
      if (!page_content) cell.colSpan = "2";
      cell.appendChild (details);
      row.appendChild (cell);
      if (page_content) {
        cell = document.createElement ('td');
        cell.appendChild (page_content);
        row.appendChild (cell);
      }
    }
    this.toggle (model);
  };
  
GalleryDetailsTableViewer.prototype.toggle = 
  function (model)
  {
    x = model.root.style.display;
    model.root.style.display = this.view.style.display;
    this.view.style.display = x;
  };

var GalleryDetails = function () {this.initialize.apply (this, arguments);};

GalleryDetails.ACTION_NEEDED = 0;
GalleryDetails.GOOD_FILE     = 1;
GalleryDetails.BAD_FILE      = 2;

GalleryDetails.prototype =
{
  root           : null,
  image_list     : [],
  wrapper        : null,
  nof_images     : 0,
  handled        : -1,
  viewer         : null,
  
  injectSpinner: function (elementBefore, id) {
  	// Will be replaced once jQuery.spinner module is loaded
  },
  
  removeSpinner: function (id) {
  	// Will be replaced once jQuery.spinner module is loaded
  },
  
  initialize : function (root, id, is_single)
  {
    this.is_single  = is_single;
    this.root       = root;
    this.wrapper    = document.createElement ('div');
    this.wrapper.id = id;
    var spinner_anchor = document.createElement ('span');
    this.wrapper.appendChild (spinner_anchor);
    this.root.parentNode.insertBefore (this.wrapper, this.root);
    
    var existing = null;
    if (is_single)
      existing = window.jQuery(this.root).find('.gallerybox');
    else
      existing = window.jQuery(this.root).find('table.searchResultImage');
    if (!existing  || existing.length == 0) return;
    // Extract the names
    this.image_list = new Array ();
    for (var i = 0; i < existing.length; i++) {
      var this_title = this.title_from_box (existing[i]);
      if (this_title && this_title.length > 0) {
        this.image_list[this.image_list.length] =
         {  name       : this_title.replace (/ /g, '_')
          , box        : existing[i]
          , done       : false
          , page       : null
          , categories : null
          , status     : GalleryDetails.ACTION_NEEDED
          , table      : existing[i]
          , users      : null
         };
      } else {
        var errorbox = window.jQuery(existing[i]).find('table.MediaTransformError');
        if (errorbox && errorbox.length > 0) {
          errorbox[0].style.width = errorbox[0].parentNode.offsetWidth + 'px';
          errorbox[0].style.height = "";
        }
      }
    }
    this.nof_images = this.image_list.length;
    if (this.nof_images == 0) return;
    this.handled = 0;
    // Now start the query (or queries). We limit this to at most 50 filenames per request to avoid
    // hitting a server limit.
    var start = 0;
    var to_do = this.nof_images;
    this.injectSpinner (spinner_anchor, id);
    while (to_do > 0) {
      var chunk = 50;
      if (chunk > to_do) chunk = to_do;
      this.make_call (start, chunk);
      to_do = to_do - chunk;
      start = start + chunk;
    }      
  },
  
  make_call : function (from, length)
  {
    // Get the filenames of images #from to #(from + length - 1)
    var titles = "";
    for (var idx = from; idx < from + length; idx++) {
      if (this.image_list[idx].name && this.image_list[idx].name.length > 0) {
        titles += (titles.length > 0 ? '|' : "")
                  + 'File:' + encodeURIComponent (this.image_list[idx].name);
      }
    }
    
    var this_obj = this;
    $.ajax({
      url: mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php'
     ,type: 'POST' // Avoid Get-request URL length limits
     ,data: 'format=json&action=query&prop=imageinfo|categories|revisions&iiprop=timestamp|user|comment|url|size|metadata'
      + '&rvdir=older&rvprop=ids|content&iilimit=2&cllimit=' + length * 10 + '&titles=' + titles
     ,dataType: 'json'
     ,success: function (json) { this_obj.add_info (json, from, length); }
     ,error: function () { this_obj.done (length); }
    });
  },
  
  add_info : function (info, from, length)
  {
    if (info) {
      for (var i = from; i < from + length; i++) {
        this.image_list[i].done = true;
        var title = this.image_list[i].name; // No namespace prefix!
        if (!title || title.length == 0) continue;

        // Search through the results to find this entry
        var page = null;
        for (var p in info.query.pages) {
          // Rm namespace for comparison
          info.query.pages[p].title = info.query.pages[p].title.replace (/ /g, '_');
          var colon = info.query.pages[p].title.indexOf (':');
          if (title == info.query.pages[p].title.substr (colon + 1)) {
            page = info.query.pages[p]; break;
          }
        }
        if (page) {
          this.image_list[i].page = page;
          // Category check
          if (typeof (page.missing) == 'undefined') {
            var catdata = page.categories;
            var categories = new Array();
            var goodcat = false;
            var badcat = false;
            var actioncat = false;
        
            if (catdata) {
              for (var cidx = 0 ; cidx < catdata.length ; cidx++) { 
                var cat = catdata[cidx].title;
                goodcat = goodcat || this.cat_match (cat, gallery_details_good_tags);
                badcat = badcat || this.cat_match (cat, gallery_details_bad_tags);
                actioncat = actioncat || this.cat_match (cat, gallery_details_action_tags);
                categories[categories.length] = cat;
              }
            }
            if (actioncat) goodcat = false;
            this.image_list[i].categories = categories;
            this.image_list[i].status     =
              (badcat ? GalleryDetails.BAD_FILE
                      : (goodcat ? GalleryDetails.GOOD_FILE : GalleryDetails.ACTION_NEEDED)
              );
            // Try to handle bot uploads
            if (page.imageinfo && page.imageinfo.length > 0) {
              this.image_list[i].users = new Array ();
              this.image_list[i].users[0] = {user : page.imageinfo[0].user, real : null};
              if (page.imageinfo.length > 1)
                this.image_list[i].users[1] = {user : page.imageinfo[1].user, real : null};
            } else
              this.image_list[i].page = null;
            if (   page.imageinfo && page.imageinfo.length > 0
                && page.revisions && page.revisions.length > 0) {
              var text = page.revisions[0];
              if (text) text = text["*"];
              if (text) {
                for (var u = 0; u < this.image_list[i].users.length; u++) {
                  var match = null; 
                  var user  = this.image_list[i].users[u].user;
                  if (user.replace (/ /g, '_') == 'Flickr_upload_bot') {
                    // Check for the bot's upload template
                    match =
                      /\{\{User:Flickr upload bot\/upload(\|[^\|\}]*)?\|reviewer=([^\}]*)\}\}/.exec (text);
                    if (match) match = match[2];
                  } else if (user == 'File Upload Bot (Magnus Manske)') {
                    // CommonsHelper
                    match =
                      /transferred to Commons by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\] using/.exec (text);
                    if (!match)
                      // geograph_org2commons, regex accounts for typo ("transferd") and its
                      // possible future correction
                      match =
                        /geograph.org.uk\]; transferr?e?d by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\] using/.exec (text);
                    if (!match && /(www\.)?flickr\.com\/photos\//.test (text))
                      // flickr2commons
                      match = /\* Uploaded by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\]/.exec (text);
                    if (match) match = match[1];
                    
                    function fix_double_encoding (match) {
                      if (!match) return match;
                      var utf8 = /[\u00C2-\u00F4][\u0080-\u00BF][\u0080-\u00BF]?[\u0080-\u00BF]?/g;
                      if (!utf8.test (match)) return match;
                      // Looks like we have a double encoding. At least it contains character
                      // sequences that might be legal UTF-8 encodings. Translate them into %-
                      // syntax and try to decode again.
                      var temp = "", curr = 0, m, hex_digit = "0123456789ABCDEF";
                      var str = match.replace (/%/g, '%25');
                      utf8.lastIndex = 0; // Reset regexp to beginning of string
                      try {
                        while ((m = utf8.exec (str)) != null) {
                          temp += str.substring (curr, m.index);
                          m = m[0];
                          for (var i = 0; i < m.length; i++) {
                            temp += '%'
                                    + hex_digit.charAt (m.charCodeAt (i) / 16)
                                    + hex_digit.charAt (m.charCodeAt (i) % 16);
                          }
                          curr = utf8.lastIndex;
                        }
                        if (curr < str.length) temp += str.substring (curr);
                        temp = decodeURIComponent (temp);
                        return temp;
                      } catch (e) {
                      }
                      return match;
                    }

                    match = fix_double_encoding (match);
                  } else if (user == 'FlickrLickr') {
                    match = /\n\|reviewer=\s*(.*)\n/.exec (text);
                    if (match) match = match[1];
                  }
                  if (match)
                    this.image_list[i].users[u].real =
                      match.replace (/^\s\s*/, "").replace (/\s\s*$/, "");
                }
              }
            }
          }
        }
      } // end loop
    }
    this.done (length);
  },
    
  done : function (n)
  {
    this.handled += n;
    if (this.handled >= this.nof_images) {
      // Done; display it
      this.removeSpinner (this.wrapper.id);
      if (this.is_single) {
        this.viewer = GalleryDetailsLoader.getViewer ();
        this.viewer.display (this);
      } else {
        // Create viewers for each entry, and pass them a faked model
        this.viewer = new Array ();
        for (var i = 0; i < this.nof_images; i++) {
          this.viewer[this.viewer.length] =
            { view       : GalleryDetailsLoader.getViewer ()
             ,root       : this.image_list[i].table
             ,image_list : [this.image_list[i]]
             ,nof_images : 1
             ,wrapper    : this.wrapper
            };
        }
        for (var i = 0; i < this.viewer.length; i++)
          this.viewer[i].view.display (this.viewer[i]);
      }          
    }
  },
  
  toggle : function ()
  {
    if (this.handled >= this.nof_images) {
      if (this.is_single) {
        this.viewer.toggle (this);
      } else {
        for (var i = 0; i < this.viewer.length; i++)
          this.viewer[i].view.toggle (this.viewer[i]);
      }
    }
  },
  
  title_from_box : function (box)
  {
    var thumblks = window.jQuery(box).find('a.image');
    if (thumblks && thumblks.length > 0 && thumblks[0]) {
      // Extract the file name (without namespace) from the link
      var name = titleFromLink (thumblks[0]) || "";
      return name.substring (name.indexOf (':') + 1); // Strip namespace
    }
    return "";
  },
  
  cat_match : function (cat, test)
  {
    if (!cat) return false;
    var c = cat.substr (cat.indexOf (':') + 1); // Strip namespace
    for (var i = 0; i < test.length; i++ ) {
     if (c.substr (0, test[i].length) == test[i]) return true;
    }
    return false;
  }
  
}; // end GalleryDetails

mw.loader.using('jquery.spinner', function () {
	GalleryDetails.prototype.injectSpinner = function (elementBefore, id) {
		window.jQuery(elementBefore).injectSpinner(id);
	};
	GalleryDetails.prototype.removeSpinner = function (id) {
		window.jQuery.removeSpinner(id);
	};
});

window.GalleryDetailsLoader =
{
  initialized : false,

  initialize : function ()
  {
    GalleryDetailsLoader.initialized = true;
    var tables = window.jQuery('.gallery');
    if (tables.length == 0) {
      // Check whether we're on Special:Search (or a modified Special:Log) and have image results
      if (mw.config.get('wgNamespaceNumber') == -1)
        tables = window.jQuery('table.searchResultImage');
    }
    if (tables.length == 0) return; // Nothing here: nothing to do.
    if (gallery_details_newfiles_run
        && mw.config.get('wgNamespaceNumber') == -1 // Special
        && mw.config.get('wgCanonicalSpecialPageName') == "Newimages") {
      GalleryDetailsLoader.autoStart ();
    } else {
      mw.util.addPortletLink(
         'p-tb'
        ,'javascript:GalleryDetailsLoader.load ();'
        ,gallery_details_sidebar.text
        ,'t-gallerydetails'
        ,gallery_details_sidebar.tip
      );
    }
  },
  
  autoStart : function () {
  	if (typeof window.titleFromHref == 'undefined' || typeof window.ImageLinks == 'undefined') {
  		// Wait until we have our asynchronously loaded dependencies (importScript above)
  		window.setTimeout (GalleryDetailsLoader.autoStart, 500); // Half a second
  		return;
  	}
  	GalleryDetailsLoader.load ();
  },
  
  loaders : [],
  
  load : function ()
  {
    if (GalleryDetailsLoader.loaders.length > 0) {
      for (var i = 0; i < GalleryDetailsLoader.loaders.length; i++) {
        GalleryDetailsLoader.loaders[i].toggle ();
      }
    } else {
      var tables = window.jQuery('.gallery');
      if (tables.length > 0) {    
        for (var i = 0; i < tables.length; i++) {
          GalleryDetailsLoader.loaders[GalleryDetailsLoader.loaders.length] =
            new GalleryDetails (
                    tables[i]
                  , 'gallery_details_wrapper_' + i
                  , true
                )
          ;
        }
      } else if (mw.config.get('wgNamespaceNumber') == -1) {
        tables = window.jQuery('table.searchResultImage');
        if (tables.length > 0) {
          var list = tables[0].parentNode;
          if (list.nodeName.toLowerCase () == 'li')
            list = list.parentNode;
          else
            return;
          GalleryDetailsLoader.loaders[GalleryDetailsLoader.loaders.length] =
              new GalleryDetails (
                      list
                    , 'gallery_details_wrapper_' + i
                    , false
                  )
          ;
        }
      }  
    }
  },
  
  getViewer : function ()
  {
    return new GalleryDetailsTableViewer (); 
  }
  
};

$(document).ready(GalleryDetailsLoader.initialize);

} // end if (guard against double imports)

// </source>