diff --git a/swh/web/ui/static/js/filedrop.js b/swh/web/ui/static/js/filedrop.js
deleted file mode 100644
--- a/swh/web/ui/static/js/filedrop.js
+++ /dev/null
@@ -1,194 +0,0 @@
-/**
- * Search page management
- */
-
-
-$.fn.extend({
- /**
- * Call on any HTMLElement to make that element the recipient of files
- * drag & dropped into it.
- * Files then have their sha1 checksum calculated
- * and searched in SWH.
- * Args:
- * resultDiv: the table where the result should be displayed
- * errorDiv: the element where the error message should be displayed
- */
- filedrop: function(fileLister, searchForm) {
-
- return this.each(function() {
-
- var dragwin = $(this);
- var fileshovering = false;
-
- dragwin.on('dragover', function(event) {
- event.stopPropagation();
- event.preventDefault();
- });
-
- dragwin.on('dragenter', function(event) {
- event.stopPropagation();
- event.preventDefault();
- if (!fileshovering) {
- dragwin.css("border-style", "solid");
- dragwin.css("box-shadow", "inset 0 3px 4px");
- fileshovering = true;
- }
- });
-
- dragwin.on('dragover', function(event) {
- event.stopPropagation();
- event.preventDefault();
- if (!fileshovering) {
- dragwin.css("border-style", "solid");
- dragwin.css("box-shadow", "inset 0 3px 4px");
- fileshovering = true;
- }
- });
-
- dragwin.on('dragleave', function(event) {
- event.stopPropagation();
- event.preventDefault();
- if (fileshovering) {
- dragwin.css("border-style", "dashed");
- dragwin.css("box-shadow", "none");
- fileshovering = false;
- }
- });
-
- dragwin.on('drop', function(event) {
- event.stopPropagation();
- event.preventDefault();
- if (fileshovering) {
- dragwin.css("border-style", "dashed");
- dragwin.css("box-shadow", "none");
- fileshovering = false;
- }
- var myfiles = event.originalEvent.dataTransfer.files;
- if (myfiles.length >= 1) {
- handleFiles(myfiles, fileLister, searchForm);
- }
- });
- });
- },
- /**
- * Call on a jQuery-selected input to make it sensitive to
- * the reception of new files, and have it process received
- * files.
- * Args:
- * fileLister: the element keeping track of the files
- * searchForm: the form whose submission will POST the file
- * information
- */
- filedialog: function(fileLister, searchForm) {
- return this.each(function() {
- var elem = $(this);
- elem.on('change', function(){
- handleFiles(this.files, fileLister, searchForm);
- });
- });
- },
- /**
- * Call on a jQuery-selected element to delegate its click
- * event to the given input instead.
- * Args:
- * input: the element to be clicked when the caller is clicked.
- */
- inputclick: function(input) {
- return this.each(function() {
- $(this).click(function(event) {
- event.preventDefault();
- input.click();
- });
- });
- },
- /**
- * Call on a form to intercept its submission event and
- * check the validity of the text input if present before submitting
- * the form.
- * Args:
- * textInput: the input to validate
- * messageElement: the element where the warning will be written
- */
- checkSubmission: function(textInput, messageElement) {
- var CHECKSUM_RE = /^([0-9a-f]{40}|[0-9a-f]{64})$/i;
- $(this).submit(function(event) {
- event.preventDefault();
- var q = textInput.val();
- if (q && !q.match(CHECKSUM_RE)) {
- messageElement.empty();
- messageElement.html('Please enter a valid SHA-1');
- } else {
- searchForm.submit();
- }
- });
- }
-});
-
-
-var nameList = []; /** Avoid adding the same file twice **/
-
-/**
- * Start reading the supplied files to hash them and add them to the form,
- * and add their names to the file lister pre-search.
- * Args:
- * myfiles: the file array
- * fileLister: the element that will receive the file names
- * searchForm: the form to which we add hidden inputs with the
- * correct values
- */
-function handleFiles(myfiles, fileLister, searchForm) {
- for (var i = 0; i < myfiles.length; i++) {
- var file = myfiles.item(i);
- if (nameList.indexOf(file.name) == -1) {
- nameList.push(file.name);
- var fr = new FileReader();
- fileLister.append(make_row(file.name));
- bind_reader(fr, file.name, searchForm);
- fr.readAsArrayBuffer(file);
- }
- }
-};
-
-/**
- * Bind a given FileReader to hash the file contents when the file
- * has been read
- * Args:
- * filereader: the FileReader object
- * filename: the name of the file being read by the FileReader
- * searchForm: the form the corresponding hidden input will be
- * appended to
- */
-function bind_reader(filereader, filename, searchForm) {
- filereader.onloadend = function(evt) {
- if (evt.target.readyState == FileReader.DONE){
- return fileReadDone(evt.target.result, filename, searchForm);
- }
- };
-}
-
-function make_row(name) {
- return "
"+name+"
";
-}
-
-/**
- * Hash the buffer contents with CryptoJS's SHA1 implementation, and
- * append the result to the given form for submission.
- * Args:
- * buffer: the buffer to be hashed
- * fname: the file name corresponding to the buffer
- * searchForm: the form the inputs should be appended to
- */
-function fileReadDone(buffer, fname, searchForm) {
- var wordArray = CryptoJS.lib.WordArray.create(buffer);
- var sha1 = CryptoJS.SHA1(wordArray);
- /**
- var git_hd = "blob " + wordArray.length + "\0";
- var git_Array = CryptoJS.enc.utf8.parse(git_hd).concat(wordArray);
- var sha256 = CryptoJS.SHA256(wordArray);
- var sha1_git = CryptoJS.SHA1(wordArray);
- **/
- searchForm.append($("", {type: "hidden",
- name: fname,
- value: sha1}
- ));
-}
diff --git a/swh/web/ui/static/js/search.js b/swh/web/ui/static/js/search.js
new file mode 100644
--- /dev/null
+++ b/swh/web/ui/static/js/search.js
@@ -0,0 +1,242 @@
+/**
+ * Search page management
+ * Args:
+ * textForm: the form containing the text input, if any
+ * fileForm: the form containing the file input, if any
+ * messageElem: the element that should display search messages
+ */
+var SearchFormController = function(textForm, fileForm, messageElem)
+
+{
+ this.textForm = textForm;
+ this.fileForm = fileForm;
+ this.messageElem = messageElem;
+
+ // List of hashes to check against files being processed
+ this.hashed_already = {
+ 'sha1': {},
+ 'sha256': {},
+ 'sha1_git': {}
+ };
+ this.algos = ['sha1', 'sha256', 'sha1_git'];
+ this.CHECKSUM_RE = /^([0-9a-f]{40}|[0-9a-f]{64})$/i;
+ var self = this;
+
+ /**
+ * Show search messages on the page
+ * Args:
+ * msg: the message to show
+ */
+ this.searchMessage = function(msg) {
+ self.messageElem.empty();
+ self.messageElem.text(msg);
+ };
+
+ /**
+ * Setup the text field
+ * Args:
+ * textFormInput: the text form's input
+ */
+ this.setupTextForm = function(textFormInput) {
+ self.textForm.submit(function(event) {
+ var q = textFormInput.val();
+ if (!q) {
+ event.preventDefault();
+ self.searchMessage("Please enter a SHA-1 or SHA-256 checksum.");
+ }
+ else if (q && !q.match(self.CHECKSUM_RE)) {
+ event.preventDefault();
+ self.searchMessage("Invalid SHA-1 or SHA-256 checksum");
+ }
+ });
+ };
+
+ /**
+ * Setup the file drag&drop UI and hashing support.
+ * Args:
+ * fileDropElem: the element receptive to drag & drop
+ * hashedListerElem: the element that receives the hased file descriptions
+ * fileFormInput: the input that actually receives files
+ * clearButton: the button used to clear currently hashed files
+ */
+ this.setupFileForm = function(fileDropElem, hashedListerElem, fileFormInput, clearButton) {
+ if (!FileReader || !CryptoJS) {
+ self.searchMessage("Client-side file hashing is not available for your browser.");
+ return;
+ }
+
+ // Enable clicking on the text element for file picker
+ fileDropElem.click(function(event) {
+ event.preventDefault();
+ fileFormInput.click();
+ });
+
+ // Enable drag&drop
+ var makeDroppable = function(fileReceptionElt) {
+ var fileshovering = false;
+
+ fileReceptionElt.on('dragover', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ fileReceptionElt.on('dragenter', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (!fileshovering) {
+ fileReceptionElt.css("border-style", "solid");
+ fileReceptionElt.css("box-shadow", "inset 0 3px 4px");
+ fileshovering = true;
+ }
+ });
+
+ fileReceptionElt.on('dragover', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (!fileshovering) {
+ fileReceptionElt.css("border-style", "solid");
+ fileReceptionElt.css("box-shadow", "inset 0 3px 4px");
+ fileshovering = true;
+ }
+ });
+
+ fileReceptionElt.on('dragleave', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (fileshovering) {
+ fileReceptionElt.css("border-style", "dashed");
+ fileReceptionElt.css("box-shadow", "none");
+ fileshovering = false;
+ }
+ });
+
+ fileReceptionElt.on('drop', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (fileshovering) {
+ fileReceptionElt.css("border-style", "dashed");
+ fileReceptionElt.css("box-shadow", "none");
+ fileshovering = false;
+ }
+ var myfiles = event.originalEvent.dataTransfer.files;
+ readAndHash(myfiles);
+ });
+ };
+ makeDroppable(fileDropElem);
+
+ // Connect input change and rehash
+ var makeInputChange = function(fileInput) {
+ return fileInput.each(function() {
+ $(this).on('change', function(){
+ readAndHash(this.files);
+ });
+ });
+ };
+ makeInputChange(fileFormInput);
+
+ // Connect clear button
+ var makeClearButton = function(button) {
+ return button.each(function() {
+ $(this).click(function(event) {
+ event.preventDefault();
+ hashedListerElem.empty();
+ self.fileForm.children('.search-hidden').remove();
+ self.hashed_already = {
+ 'sha1': {},
+ 'sha256': {},
+ 'sha1_git': {}
+ };
+ });
+ });
+ };
+ makeClearButton(clearButton);
+
+ var readAndHash = function(filelist) {
+ for (var file_idx = 0; file_idx < filelist.length; file_idx++) {
+ var file = filelist.item(file_idx);
+ var fr = new FileReader();
+ bindReader(fr, file.name);
+ fr.readAsArrayBuffer(file);
+ }
+ };
+
+ var bindReader = function(freader, fname) {
+ freader.onloadend = function(event) {
+ if (event.target.readyState == FileReader.DONE)
+ return dedupAndAdd(event.target.result, fname);
+ else
+ return null;
+ };
+ };
+
+ /**
+ * Hash the buffer with SHA-1, SHA-1_GIT, SHA-256
+ * Args:
+ * buffer: the buffer to hash
+ * fname: the file name corresponding to the buffer
+ * Returns:
+ * a dict of algo_hash: hash
+ */
+ var hashBuffer = function (buffer, fname) {
+ function str2ab(header) {
+ var buf = new ArrayBuffer(header.length);
+ var view = new Uint8Array(buf); // byte view, all we need is ASCII
+ for (var idx = 0, len=header.length; idx < len; idx++)
+ view[idx] = header.charCodeAt(idx);
+ return buf;
+ }
+
+ var content_array = CryptoJS.lib.WordArray.create(buffer);
+ var git_hd_str = 'blob ' + buffer.byteLength + '\0';
+ var git_hd_buffer = str2ab(git_hd_str);
+ var git_hd_array = CryptoJS.lib.WordArray.create(git_hd_buffer);
+
+ var sha1 = CryptoJS.SHA1(content_array);
+ var sha256 = CryptoJS.SHA256(content_array);
+ var sha1_git = CryptoJS.SHA1(git_hd_array.concat(content_array));
+ return {
+ 'sha1': sha1 + '',
+ 'sha256': sha256 + '',
+ 'sha1_git': sha1_git + ''
+ };
+ };
+
+ /**
+ * Hash the buffer and add it to the form if it is unique
+ * If not, display which file has the same content
+ * Args:
+ * buffer: the buffer to hash
+ * fname: the file name corresponding to the buffer
+ */
+ var dedupAndAdd = function(buffer, fname) {
+ var hashes = hashBuffer(buffer);
+ var has_duplicate = false;
+ for (var algo_s in hashes) {
+ if (self.hashed_already[algo_s][hashes[algo_s]] != undefined) {
+ // Duplicate content -- fileLister addition only, as duplicate
+ hashedListerElem.append($('
')
+ .addClass('span3')
+ .text(fname + ': duplicate of ' + self.hashed_already[algo_s][hashes[algo_s]]));
+ has_duplicate = true;
+ break;
+ }
+ }
+ // First file read with this content -- fileLister and form addition
+ if (!has_duplicate) {
+ // Add to hashed list
+ for (var algo_c in self.hashed_already)
+ self.hashed_already[algo_c][hashes[algo_c]] = fname;
+ hashedListerElem.append($('