Skip to content
This repository has been archived by the owner on Oct 15, 2022. It is now read-only.

Commit

Permalink
Merge branch 'jag/whois'
Browse files Browse the repository at this point in the history
  • Loading branch information
jagtalon committed Sep 26, 2014
2 parents 08d5a23 + 460209d commit beb4114
Show file tree
Hide file tree
Showing 5 changed files with 463 additions and 0 deletions.
112 changes: 112 additions & 0 deletions lib/DDG/Spice/Whois.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package DDG::Spice::Whois;
# ABSTRACT: Returns an internet domain's availability and whois information.

use DDG::Spice;

# Metadata for this spice
name 'Whois';
source 'Whois API';
description 'Whois info and registration links for web domains';
primary_example_queries 'whois duckduckgo.com', 'whois http://duckduckgo.com';
secondary_example_queries 'domain duckduckgo.com', 'who owns duckduckgo.com', 'duckduckgo.com available';
category 'programming';
topics 'computing', 'geek', 'programming', 'sysadmin';
code_url 'https://github.com/duckduckgo/zeroclickinfo-spice/blob/master/lib/DDG/Spice/Whois.pm';

attribution twitter => 'bjennelle',
github => ["b1ake", 'Blake Jennelle'];


# turns on/off debugging output
my $is_debug = 0;

# regex for allowed TLDS (grabbed from DDG core repo, in /lib/DDG/Util/Constants.pm)
my $tlds_qr = qr/(?:c(?:o(?:m|op)?|at?|[iykgdmnxruhcfzvl])|o(?:rg|m)|n(?:et?|a(?:me)?|[ucgozrfpil])|e(?:d?u|[gechstr])|i(?:n(?:t|fo)?|[stqldroem])|m(?:o(?:bi)?|u(?:seum)?|i?l|[mcyvtsqhaerngxzfpwkd])|g(?:ov|[glqeriabtshdfmuywnp])|b(?:iz?|[drovfhtaywmzjsgbenl])|t(?:r(?:avel)?|[ncmfzdvkopthjwg]|e?l)|k[iemygznhwrp]|s[jtvberindlucygkhaozm]|u[gymszka]|h[nmutkr]|r[owesu]|d[kmzoej]|a(?:e(?:ro)?|r(?:pa)?|[qofiumsgzlwcnxdt])|p(?:ro?|[sgnthfymakwle])|v[aegiucn]|l[sayuvikcbrt]|j(?:o(?:bs)?|[mep])|w[fs]|z[amw]|f[rijkom]|y[eut]|qa)/i;

# regex for parsing URLs
my $url_qr = qr/(?:http:\/\/)? # require http
([^\s\.]*\.)* # capture any subdomains
([^\s\.]*?) # capture the domain
\.($tlds_qr) # capture the tld, such as .com
(\:?[0-9]{1,4})? # look for a port, such as :3000
([^\s]*)/x; # look for an extended path, such as /pages/about.htm

# additional keywords that trigger this spice
my $whois_keywords_qr = qr/whois|lookup|(?:is\s|)domain|(?:is\s|)available|register|owner(?:\sof|)|who\sowns|(?:how\sto\s|)buy/i;

# trigger this spice when either:
# - query contains only a URL
# - query contains starts or end with any of the whois keywords
#
# note that there are additional guards in the handle() function that
# narrow this spice's query space.
#
triggers query_raw =>
# allow the naked url with leading and trailing spaces
qr/^\s*$url_qr\s*$/,

# allow the whois keywords at the beginning or end of the string
# with leading or trailing spaces.
#
# if at the end of the string, allow a trailing question mark.
qr/^\s*$whois_keywords_qr
|$whois_keywords_qr[?]?\s*$/x;

# API call details for Whois API (http://www.whoisxmlapi.com/)
spice to => 'http://www.whoisxmlapi.com/whoisserver/WhoisService?domainName=$1&outputFormat=JSON&callback={{callback}}&username={{ENV{DDG_SPICE_WHOIS_USERNAME}}}&password={{ENV{DDG_SPICE_WHOIS_PASSWORD}}}';

handle sub {
my ($query) = @_;
return if !$query; # do not trigger this spice if the query is blank

# trim any leading and trailing spaces
$query = trim($query);

# remove any trailing question marks, which are allowed
# but can disrupt the regexs
$query =~ s/\?$//;

# parse the URL into its parts
my ($subdomains, $domain, $tld, $port, $resource_path) = $query =~ $url_qr;

# debugging output
warn 'query: ', $query, "\t", 'sub: ', $subdomains || '', "\t", 'domain: ', $domain || '', "\t", 'tld: ', $tld || '', "\t", 'port: ', $port || '', "\t", 'resource path: ', $resource_path || '' if $is_debug;

# get the non-URL text from the query by combining the text before and after the match
my $non_url_text = $` . $'; #' <-- closing tick added for syntax highlighting

# is the string a naked domain, i.e. is there any text besides the domain?
my $is_naked_domain = trim($non_url_text) eq '';

# skip if we're missing a domain or a tld
return if !defined $domain || $domain eq '' || !defined $tld || $tld eq '';

# skip if we have naked domain that contains a non-www subdomain, a port or a resource_path.
# e.g. continue: 'http://duckduckgo.com' is allowed
# skip: 'http://blog.duckduckgo.com'
# skip: 'http://duckduckgo.com:8080'
# skip: 'http://blog.duckduckgo.com/hello.html'
#
# note that if the user includes a whois keyword to any of these,
# such as 'whois http://blog.duckduckgo.com', they we continue.
#
# this signals to us that the user wants a whois result, and isn't just
# trying to nav to the URL they typed.
#
return if $is_naked_domain
&& ( (defined $subdomains && $subdomains !~ /^www.$/)
|| (defined $port && $port ne '')
|| (defined $resource_path && $resource_path ne ''));

# return the combined domain + tld (after adding a period in between)
return lc "$domain.$tld";
};

# Returns a string with leading and trailing spaces removed.
sub trim {
my ($str) = @_;
$str =~ s/^\s*(.*)?\s*$/$1/;
return $str;
}

1;
3 changes: 3 additions & 0 deletions share/spice/whois/available.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<strong>The domain '<strong>{{domainName}}</strong>' may be available!</strong><br />
To register, try <a href="https://domai.nr/{{domainName}}" target="_blank">Domainr</a>, <a href="https://www.namecheap.com/domains/registration/results.aspx?domain={{domainName}}" target="_blank">NameCheap</a> or <a href="https://www.101domain.com/domain-availability-search.htm?q={{domainName}}" target="_blank">101Domains</a><br />
<br />
11 changes: 11 additions & 0 deletions share/spice/whois/whois.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

@media (max-width: 600px) {
.zci--whois table {
table-layout: fixed;
width: 100%;
}

.zci--whois table td {
word-wrap: break-word;
}
}
198 changes: 198 additions & 0 deletions share/spice/whois/whois.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
(function (env) {
"use strict";

// turns on/off debugging output
var is_debug = false;

// spice callback function
env.ddg_spice_whois = function(raw_api_result) {

// for debugging
if(is_debug) console.log('in start of JS, raw_api_result:', raw_api_result);

// normalize the api output
var api_result = normalize_api_result(raw_api_result);

if(is_debug) console.log('normalized api_result:', api_result || 'empty');

// Check for API error and exit early if found
// (with error message when in debug mode)
if (!api_result) {
if(is_debug) console.log('Error with whois API. raw_api_result:', raw_api_result || 'empty', ', normalized api_result:', api_result || 'empty');

return Spice.failed('whois');
}

// is the domain available?
var is_avail = api_result.available;

// if the domain isn't available, do we want to show
// whois information?
var is_whois_allowed = is_whois_query(DDG.get_query());

// for debugging
if(is_debug) console.log("is_avail:", is_avail, "is_whois_allowed:", is_whois_allowed);

// decide which template to show, if any
if(is_avail) {
// show message saying the domain is available
show_available(api_result);
} else if(is_whois_allowed) {
// show whois info for the domain
show_whois(api_result);
} else {
// by default, show nothing
}

};

// Returns whether we should show whois data if this
// domain is not available.
var is_whois_query = function(query) {
// show whois results except when the query contains only the domain
// and no other keywords, which we test by looking for a space in the query.
return /\s/.test($.trim(query));
};

// parse the api response into a standard format
var normalize_api_result = function(api_result) {

// return nothing if no api_result, if error, or if WhoisRecord object is missing
if(!api_result || api_result.error || !api_result.WhoisRecord) return;

// use only the 'WhoisRecord' portion, because that's where
// all the data is stored.
api_result = api_result.WhoisRecord;

// sometimes the data is nested inside the 'registryData' object
if(!api_result.createdDate
&& api_result.registryData
&& (api_result.registryData.createdDate || api_result.registryData.expiresDate) ){

api_result = api_result.registryData;
}

// store the domain's various contacts in an array.
//
// we'll iterate through this array in order, using
// info from the first contact that contains the field we want.
var contacts = [
api_result.registrant,
api_result.administrativeContact,
api_result.technicalContact
];

// return the normalized output as a hash
var normalized = {

// these first fields are not displayed
// (hence the camelCase, which the user will not see)

'domainName': api_result.domainName,
'available': is_domain_available(api_result),

// the remaining fields are displayed
// (hence the user-friendly capitalization and spaces)

'Status': is_domain_available(api_result) ? 'Available' : 'Registered',
'Registered to': get_first_by_key(contacts, 'name'),
'Email': get_first_by_key(contacts, 'email'),

// trim dates so they are shown without times
// (if no time was found, the replace() call will return undef,
// so we need to fallback to the original string)
'Last updated': api_result.updatedDate
&& api_result.updatedDate.replace(/^(.*)?\s(.*)?$/, '$1'),

'Expires': api_result.expiresDate
&& api_result.expiresDate.replace(/^(.*)?\s(.*)?$/, '$1'),
};

// return nothing if all key whois data is missing
if( !normalized['Registered to']
&& !normalized['Email']
&& !normalized['Last updated']
&& !normalized['Expires']) {
return;
}

return normalized;
}

// Returns whether the domain is registered to someone, based on the API result.
var is_domain_available = function(api_result) {
return api_result.dataError && api_result.dataError === 'MISSING_WHOIS_DATA';
};

// Searches an array of objects for the first value
// at the specified key.
var get_first_by_key = function(arr, key) {
if(!arr || arr.length == 0) return;

// find the first object in the array that has a non-empty value at the key
var first;
$.each(arr, function(index, obj) {
// get the value at the specified key
// (which could be undefined)
var value = obj && obj[key];

// update the first var if the value is truthy
// and first hasn't already been found
if(!first && value) {
first = value;
}
});

// return first, which could still be empty
return first;
}

// Data that's shared between the two Spice.add calls.
var get_shared_spice_data = function(api_result) {
return {
id: "whois",
name: "Whois",
meta: {
sourceName: "Whois API",
sourceUrl: 'http://www.whoisxmlapi.com/whois-api-doc.php#whoisserver/WhoisService?rid=2&domainName='
+ api_result.domainName
+ '&outputFormat=json&target=raw'
},
templates: {
group: 'base',
options:{
moreAt: true
}
}
};
};

// Show message saying that the domain is available.
var show_available = function(api_result) {
var shared_spice_data = get_shared_spice_data(api_result);

// add the attributes specific to this template
shared_spice_data.data = api_result;
shared_spice_data.templates.options.content = Spice.whois.available;

Spice.add(shared_spice_data);
};

// Show whois info for the domain using the 'record' template.
var show_whois = function(api_result) {
var shared_spice_data = get_shared_spice_data(api_result);

// add the attributes specific to this template
shared_spice_data.data = {
'record_data': api_result,
'record_keys': ['Status', 'Registered to', 'Email', 'Last updated', 'Expires']
};
shared_spice_data.templates.options.content = 'record';
shared_spice_data.templates.options.keySpacing = true;

Spice.add(shared_spice_data);
};



}(this));
Loading

0 comments on commit beb4114

Please sign in to comment.