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($('
') + .addClass('span3') + .text(fname)); + var hashstring = JSON.stringify(hashes).replace('\"', '\''); + self.fileForm.append($("", {type: 'hidden', + class: 'search-hidden', + name: fname, + value: hashes['sha1']}// hashstring} + )); + + } + }; + }; +};