Commit c218d25ae15ab1d42765960b2601270b369fd188:
Commit 'model'
git-svn-id:
https://svn.apache.org/repos/infra/infrastructure/trunk/projects/whimsy@819384
90ea9780-b833-de11-8433-001ec94261de
Branch: refs/heads/master
Author: Sam Ruby <[email protected]>
Committer: Sam Ruby <[email protected]>
Pusher: rubys <[email protected]>
------------------------------------------------------------
README | +++++++++
asf.rb | +++++++++
asf/auth.rb | ++++++++++++
asf/committee.rb | +++++++++
asf/icla.rb | ++++++++
asf/ldap.rb | ++++++++
asf/mail.rb | ++++++++
asf/member.rb | ++++++++
asf/nominees.rb | ++++++++
asf/svn.rb | +++++++++++
asf/watch.rb | +++++++++++
------------------------------------------------------------
643 changes: 643 additions, 0 deletions.
------------------------------------------------------------
diff --git a/README b/README
new file mode 100644
index 0000000..20b971f
--- /dev/null
+++ b/README
@@ -0,0 +1,9 @@
+This directory has two subdirectories...
+
+1) "asf" contains the "model", i.e., a set of classes which encapsulate access
+ to a number of data sources such as LDAP, ICLAs, auth lists, etc.
+
+2) "www" contains the "view", largely a set of cgi scripts that produce HTML.
+ Generally a cgi script is self contained, including all of the CSS,
+ scripts, AJAX logic (client and server), SVG images, etc. A single script
+ may also produce a set (subtree) of web pages.
diff --git a/asf.rb b/asf.rb
new file mode 100644
index 0000000..f9ac8b2
--- /dev/null
+++ b/asf.rb
@@ -0,0 +1,9 @@
+require File.expand_path('../asf/committee', __FILE__)
+require File.expand_path('../asf/ldap', __FILE__)
+require File.expand_path('../asf/mail', __FILE__)
+require File.expand_path('../asf/svn', __FILE__)
+require File.expand_path('../asf/watch', __FILE__)
+require File.expand_path('../asf/nominees', __FILE__)
+require File.expand_path('../asf/icla', __FILE__)
+require File.expand_path('../asf/auth', __FILE__)
+require File.expand_path('../asf/member', __FILE__)
diff --git a/asf/auth.rb b/asf/auth.rb
new file mode 100644
index 0000000..f98b521
--- /dev/null
+++ b/asf/auth.rb
@@ -0,0 +1,24 @@
+module ASF
+
+ class Authorization
+ include Enumerable
+
+ def self.find_by_id(value)
+ new.select {|auth, ids| ids.include? value}.map(&:first)
+ end
+
+ def each
+ auth = ASF::SVN['infra/infrastructure/trunk/subversion/authorization']
+ File.read("#{auth}/asf-authorization-template").
+ scan(/^([-\w]+)=(\w.*)$/).each do |pmc, ids|
+ yield pmc, ids.split(',')
+ end
+ end
+ end
+
+ class Person
+ def auth
+ @auths ||= ASF::Authorization.find_by_id(name)
+ end
+ end
+end
diff --git a/asf/committee.rb b/asf/committee.rb
new file mode 100644
index 0000000..1eab5a8
--- /dev/null
+++ b/asf/committee.rb
@@ -0,0 +1,69 @@
+module ASF
+
+ class Base
+ end
+
+ class Committee < Base
+ @@aliases = Hash.new {|hash, name| name}
+ @@aliases.merge! \
+ 'community development' => 'comdev',
+ 'conference planning' => 'concom',
+ 'conferences' => 'concom',
+ 'http server' => 'httpd',
+ 'httpserver' => 'httpd',
+ 'java community process' => 'jcp',
+ 'quetzalcoatl' => 'quetz',
+ 'security team' => 'security',
+ 'c++ standard library' => 'stdcxx',
+ 'travel assistance' => 'tac',
+ 'traffic server' => 'trafficserver',
+ 'web services' => 'ws',
+ 'xml graphics' => 'xmlgraphics'
+
+ def self.load_committee_info
+ return @committee_info if @committee_info
+ board = ASF::SVN['private/committers/board']
+ committee = File.read("#{board}/committee-info.txt").split(/^\* /)
+ head = committee.shift.split(/^\d\./)[1]
+ head.scan(/^\s+(\w.*?)\s\s+.*<(\w+)@apache\.org>/).each do |name, id|
+ find(name).chair = ASF::Person.find(id)
+ end
+ @nonpmcs = head.sub(/.*?also has/m,'').
+ scan(/^\s+(\w.*?)\s\s+.*<\w+@apache\.org>/).flatten.uniq.
+ map {|name| find(name)}
+ @committee_info = ASF::Committee.collection.values
+ end
+
+ def self.nonpmcs
+ @nonpmcs
+ end
+
+ def self.find(name)
+ result = super(@@aliases[name.downcase])
+ result.display_name = name if name =~ /[A-Z]/
+ result
+ end
+
+ def chair
+ Committee.load_committee_info
+ @chair
+ end
+
+ def display_name
+ Committee.load_committee_info
+ @display_name || name
+ end
+
+ def display_name=(name)
+ @display_name ||= name
+ end
+
+ def chair=(person)
+ @chair = person
+ end
+
+ def nonpmc?
+ Committee.nonpmcs.include? self
+ end
+ end
+end
diff --git a/asf/icla.rb b/asf/icla.rb
new file mode 100644
index 0000000..808d503
--- /dev/null
+++ b/asf/icla.rb
@@ -0,0 +1,64 @@
+module ASF
+
+ class ICLA
+ include Enumerable
+
+ def self.find_by_id(value)
+ return if value == 'notinavail'
+ new.each do |id, name, email|
+ if id == value
+ return Struct.new(:id, :name, :email).new(id, name, email)
+ end
+ end
+ nil
+ end
+
+ def self.find_by_email(value)
+ value = value.downcase
+ ICLA.new.each do |id, name, email|
+ if email.downcase == value
+ return Struct.new(:id, :name, :email).new(id, name, email)
+ end
+ end
+ nil
+ end
+
+ def self.availids
+ return @availids if @availids
+ availids = []
+ ICLA.new.each {|id, name, email| availids << id unless id ==
'notinavail'}
+ @availids = availids
+ end
+
+ def each(&block)
+ officers = ASF::SVN['private/foundation/officers']
+ iclas = File.read("#{officers}/iclas.txt")
+ iclas.scan(/^(\w+):.*?:(.*?):(.*?):/).each(&block)
+ end
+ end
+
+ class Person
+ def icla
+ @icla ||= ASF::ICLA.find_by_id(name)
+ end
+
+ def icla?
+ ICLA.availids.include? name
+ end
+ end
+
+ def self.search_archive_by_id(value)
+ require 'net/http'
+ require 'nokogiri'
+ committers = 'http://people.apache.org/~rubys/committers.html'
+ doc = Nokogiri::HTML(Net::HTTP.get(URI.parse(committers)))
+ doc.search('tr').each do |tr|
+ tds = tr.search('td')
+ next unless tds.length == 3
+ return tds[1].text if tds[0].text == value
+ end
+ nil
+ rescue
+ nil
+ end
+end
diff --git a/asf/ldap.rb b/asf/ldap.rb
new file mode 100644
index 0000000..df74f3c
--- /dev/null
+++ b/asf/ldap.rb
@@ -0,0 +1,251 @@
+require 'wunderbar'
+
+module ASF
+
+ # determine whether or not the LDAP API can be used
+ def self.init_ldap
+ @ldap = nil
+ begin
+ conf = '/etc/ldap/ldap.conf'
+ host = File.read(conf).scan(/^uri\s+ldaps:\/\/(\S+?):(\d+)/i).first
+ Wunderbar.info "Connecting to LDAP server:
[ldaps://#{host[0]}:#{host[1]}]"
+ rescue Errno::ENOENT
+ host = nil
+ end
+
+ if host
+ begin
+ require 'rubygems'
+ require 'ldap'
+ begin
+ @ldap = LDAP::SSLConn.new(host.first, host.last.to_i)
+ rescue LDAP::ResultError=>re
+ Wunderbar.error "Error binding to LDAP server: message: ["+
re.message + "]"
+ end
+ rescue LoadError=>e
+ Wunderbar.info "ruby-ldap wasn't found; ldapsearch will be used
instead: [" + e.message + "]"
+ end
+ end
+ end
+
+ # emulate the LDAP API by shelling out to ldapsearch and parsing LDIF
+ def self.ldapsearch(base, scope, filter, attrs)
+ attrs = attrs.join(' ') if attrs.respond_to? :join
+ search = `ldapsearch -x -LLL -b #{base} -s #{scope} #{filter} #{attrs}`
+ search.sub!(/\Aversion: \d+\n/, '')
+ search.gsub!(/\n /, '')
+ search.gsub!(/^(\w+):: ([A-Za-z0-9+\/]+=?=?)/) do
+ "#{$1}: #{$2.unpack('m*').join}"
+ end
+ search.force_encoding('utf-8') if search.respond_to? :force_encoding
+ search.split("\n\n").map do |lines|
+ Hash[lines.scan(/^(\w+): (.*)/).group_by(&:first).
+ map {|name, value| [name, value.map(&:last)]}]
+ end
+ end
+
+ # search with a scope of one
+ def self.search_one(base, filter, attrs=nil)
+ init_ldap unless defined? @ldap
+
+ Wunderbar.info "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
+ "#{[attrs].flatten.join(' ')}"
+
+ if @ldap
+ result = @ldap.search2(base, LDAP::LDAP_SCOPE_ONELEVEL, filter, attrs)
+ else
+ result = ldapsearch(base, 'one', filter, attrs)
+ end
+
+ result.map! {|hash| hash[attrs]} if String === attrs
+
+ result
+ end
+
+ def self.pmc_chairs
+ @pmc_chairs ||= Service.find('pmc-chairs').members
+ end
+
+ def self.committers
+ @committers ||= Group.find('committers').members
+ end
+
+ def self.members
+ @members ||= Group.find('member').members
+ end
+
+ class Base
+ attr_reader :name
+
+ def self.base
+ @base
+ end
+
+ def base
+ self.class.base
+ end
+
+ def self.collection
+ @collection ||= Hash.new
+ end
+
+ def self.[] name
+ collection[name] || new(name)
+ end
+
+ def self.find name
+ collection[name] || new(name)
+ end
+
+ def self.new name
+ collection[name] || super
+ end
+
+ def initialize name
+ self.class.collection[name] = self
+ @name = name
+ end
+
+ unless Object.respond_to? :id
+ def id
+ @name
+ end
+ end
+ end
+
+ class LazyHash < Hash
+ def initialize(&initializer)
+ @initializer = initializer
+ end
+
+ def load
+ return unless @initializer
+ merge! @initializer.call || {}
+ @initializer = super
+ end
+
+ def [](key)
+ result = super
+ if not result and not keys.include? key and @initializer
+ merge! @initializer.call || {}
+ @initializer = nil
+ result = super
+ end
+ result
+ end
+ end
+
+ class Person < Base
+ @base = 'ou=people,dc=apache,dc=org'
+
+ def self.list(filter='uid=*')
+ ASF.search_one(base, filter, 'uid').flatten.map {|uid| find(uid)}
+ end
+
+ # pre-fetch a given attribute, for a given list of people
+ def self.preload(attributes, people={})
+ attributes = [attributes].flatten
+
+ if people.empty?
+ filter = "(|#{attributes.map {|attribute| "(#{attribute}=*)"}.join})"
+ else
+ filter = "(|#{people.map {|person| "(uid=#{person.name})"}.join})"
+ end
+
+ zero = Hash[attributes.map {|attribute| [attribute,nil]}]
+
+ data = ASF.search_one(base, filter, attributes + ['uid'])
+ data = Hash[data.map! {|hash| [find(hash['uid'].first), hash]}]
+ data.each {|person, hash| person.attrs.merge!(zero.merge(hash))}
+
+ if people.empty?
+ (collection.values - data.keys).each do |person|
+ person.attrs.merge! zero
+ end
+ end
+ end
+
+ def attrs
+ @attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
+ end
+
+ def public_name
+ cn = [attrs['cn']].flatten.first
+ cn.force_encoding('utf-8') if cn.respond_to? :force_encoding
+ return cn if cn
+ return icla.name if icla
+ ASF.search_archive_by_id(name)
+ end
+
+ def asf_member?
+ ASF::Member.status[name] or ASF.members.include? self
+ end
+
+ def banned?
+ not attrs['loginShell'] or attrs['loginShell'].include? "/usr/bin/false"
+ end
+
+ def mail
+ attrs['mail'] || []
+ end
+
+ def alt_email
+ attrs['asf-altEmail'] || []
+ end
+
+ def pgp_key_fingerprints
+ attrs['asf-pgpKeyFingerprint']
+ end
+
+ def urls
+ attrs['asf-personalURL'] || []
+ end
+
+ def committees
+ Committee.list("member=uid=#{name},#{base}")
+ end
+
+ def groups
+ Group.list("memberUid=#{name}")
+ end
+ end
+
+ class Group < Base
+ @base = 'ou=groups,dc=apache,dc=org'
+
+ def self.list(filter='cn=*')
+ ASF.search_one(base, filter, 'cn').flatten.map {|cn| find(cn)}
+ end
+
+ def members
+ ASF.search_one(base, "cn=#{name}", 'memberUid').flatten.
+ map {|uid| Person.find(uid)}
+ end
+ end
+
+ class Committee < Base
+ @base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
+
+ def self.list(filter='cn=*')
+ ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
+ end
+
+ def members
+ ASF.search_one(base, "cn=#{name}", 'member').flatten.
+ map {|uid| Person.find uid[/uid=(.*?),/,1]}
+ end
+ end
+
+ class Service < Base
+ @base = 'ou=groups,ou=services,dc=apache,dc=org'
+
+ def self.list(filter='cn=*')
+ ASF.search_one(base, filter, 'cn').flatten
+ end
+
+ def members
+ ASF.search_one(base, "cn=#{name}", 'member').flatten.
+ map {|uid| Person.find uid[/uid=(.*?),/,1]}
+ end
+ end
+end
diff --git a/asf/mail.rb b/asf/mail.rb
new file mode 100644
index 0000000..085fb0a
--- /dev/null
+++ b/asf/mail.rb
@@ -0,0 +1,61 @@
+
+module ASF
+
+ class Mail
+ def self.list
+ return @list if @list
+
+ list = Hash.new
+
+ # load info from LDAP
+ ASF::Person.preload(['mail', 'asf-altEmail'])
+ ASF::Person.collection.each do |name, person|
+ (person.mail+person.alt_email).each do |mail|
+ list[mail.downcase] = person
+ end
+ end
+
+ # load all member emails in one pass
+ ASF::Member.each do |id, text|
+ Member.emails(text).each {|mail| list[mail.downcase] ||= Person[id]}
+ end
+
+ # load all ICLA emails in one pass
+ ASF::ICLA.new.each do |id, name, email|
+ list[email.downcase] ||= Person.find(id)
+ next if id == 'notinavail'
+ list["#{id.downcase}@apache.org"] ||= Person.find(id)
+ end
+
+ @list = list
+ end
+ end
+
+ class Person < Base
+ def self.find_by_email(value)
+ value.downcase!
+
+ person = Mail.list[value]
+ return person if person
+ end
+
+ def obsolete_emails
+ return @obsolete_emails if @obsolete_emails
+ result = []
+ if icla
+ unless active_emails.any? {|mail| mail.downcase == icla.email.downcase}
+ result << icla.email
+ end
+ end
+ @obsolete_emails = result
+ end
+
+ def active_emails
+ (mail + alt_email + member_emails).uniq
+ end
+
+ def all_mail
+ active_emails + obsolete_emails
+ end
+ end
+end
diff --git a/asf/member.rb b/asf/member.rb
new file mode 100644
index 0000000..3ac6adb
--- /dev/null
+++ b/asf/member.rb
@@ -0,0 +1,63 @@
+module ASF
+ class Member
+ include Enumerable
+
+ def self.find_text_by_id(value)
+ new.each do |id, text|
+ return text if id==value
+ end
+ nil
+ end
+
+ def self.each(&block)
+ new.each(&block)
+ end
+
+ def self.find_by_email(value)
+ value = value.downcase
+ each do |id, text|
+ emails(text).each do |email|
+ return Person[id] if email.downcase == value
+ end
+ end
+ nil
+ end
+
+ def self.status
+ return @status if @status
+ status = {}
+ foundation = ASF::SVN['private/foundation']
+ sections = File.read("#{foundation}/members.txt").split(/(.*\n===+)/)
+ sections.shift(3)
+ sections.each_slice(2) do |header, text|
+ header.sub!(/s\n=+/,'')
+ text.scan(/Avail ID: (.*)/).flatten.each {|id| status[id] = header}
+ end
+ @status = status
+ end
+
+ def each
+ return unless ASF::Person.new($USER).asf_member?
+ foundation = ASF::SVN['private/foundation']
+ File.read("#{foundation}/members.txt").split(/^ \*\) /).each do |section|
+ id = section[/Avail ID: (.*)/,1]
+ yield id, section.sub(/\n.*\n===+\s*?\n(.*\n)+.*/,'').strip if id
+ end
+ end
+
+ def self.emails(text)
+ emails = text.to_s.scan(/Email: (.*(?:\n\s+\S+@.*)*)/).flatten.
+ join(' ').split(/\s+/).grep(/@/)
+ end
+ end
+
+ class Person
+ def members_txt
+ @members_txt ||= ASF::Member.find_text_by_id(name)
+ end
+
+ def member_emails
+ ASF::Member.emails(members_txt)
+ end
+ end
+end
diff --git a/asf/nominees.rb b/asf/nominees.rb
new file mode 100644
index 0000000..535786d
--- /dev/null
+++ b/asf/nominees.rb
@@ -0,0 +1,31 @@
+module ASF
+
+ class Person < Base
+
+ def self.member_nominees
+ return @member_nominees if @member_nominees
+
+ foundation = ASF::SVN['private/foundation/Meetings']
+ text = File.read "#{foundation}/20120522/nominated-members.txt"
+
+ nominations = text.split(/^\s*---+\s*/)
+ nominations.shift(2)
+
+ nominees = {}
+ nominations.each do |nomination|
+ id = nomination[/^\s?\w+.*<(\S+)@apache.org>/,1]
+ id ||= nomination[/^\s?\w+.*\(([a-z]+)\)/,1]
+
+ next unless id
+
+ nominees[find(id)] = nomination
+ end
+
+ @member_nominees = nominees
+ end
+
+ def member_nomination
+ Person.member_nominees[self]
+ end
+ end
+end
diff --git a/asf/svn.rb b/asf/svn.rb
new file mode 100644
index 0000000..bc6c2ae
--- /dev/null
+++ b/asf/svn.rb
@@ -0,0 +1,21 @@
+require 'uri'
+
+module ASF
+
+ class SVN
+ @base = URI.parse('https://svn.apache.org/repos/')
+
+ def self.repos
+ @repos ||= Hash[Dir['/home/whimsysvn/svn/*'].map { |name|
+ Dir.chdir name.untaint do
+ [`svn info`[/URL: (.*)/,1].sub(/^http:/,'https:'), Dir.pwd.untaint]
+ end
+ }]
+ end
+
+ def self.[](name)
+ repos[(@base+name).to_s]
+ end
+ end
+
+end
diff --git a/asf/watch.rb b/asf/watch.rb
new file mode 100644
index 0000000..8e691aa
--- /dev/null
+++ b/asf/watch.rb
@@ -0,0 +1,41 @@
+module ASF
+
+ class Person < Base
+
+ def self.member_watch_list
+ return @member_watch_list if @member_watch_list
+
+ foundation = ASF::SVN['private/foundation']
+ text = File.read "#{foundation}/potential-member-watch-list.txt"
+
+ nominations = text.scan(/^\s+\*\)\s+\w.*?\n\s*(?:---|\Z)/m)
+
+ i = 0
+ member_watch_list = {}
+ nominations.each do |nomination|
+ id = nil
+ name = nomination[/\*\)\s+(.+?)\s+(\(|\<|$)/,1]
+ id ||= nomination[/\*\)\s.+?\s\((.*?)\)/,1]
+ id ||= nomination[/\*\)\s.+?\s<(.*?)@apache.org>/,1]
+
+ unless id
+ id = "notinavail_#{i+=1}"
+ find(id).attrs['cn'] = name
+ end
+
+ member_watch_list[find(id)] = nomination
+ end
+
+ @member_watch_list = member_watch_list
+ end
+
+ def member_watch
+ text = Person.member_watch_list[self]
+ if text
+ text.sub!(/\A\s*\n/,'')
+ text.sub!(/\n---\Z/,'')
+ end
+ text
+ end
+ end
+end