You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@whimsical.apache.org by ru...@apache.org on 2017/07/02 23:11:17 UTC

[whimsy] 01/02: bit-for-bit copy of mlreq.cgi to start with

This is an automated email from the ASF dual-hosted git repository.

rubys pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/whimsy.git

commit e98435e4424a33d8e4158dadc982f62cb4e6bc72
Author: Sam Ruby <ru...@intertwingly.net>
AuthorDate: Sun Jul 2 18:58:16 2017 -0400

    bit-for-bit copy of mlreq.cgi to start with
    
    copied from https://svn.apache.org/repos/infra/infrastructure/trunk/projects/infra/www/officers/mlreq.cgi
---
 www/officers/mlreq.cgi | 518 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 518 insertions(+)

diff --git a/www/officers/mlreq.cgi b/www/officers/mlreq.cgi
new file mode 100755
index 0000000..157c563
--- /dev/null
+++ b/www/officers/mlreq.cgi
@@ -0,0 +1,518 @@
+#!/usr/bin/ruby1.9.1
+require 'wunderbar'
+require 'shellwords'
+require 'mail'
+require 'whimsy/asf'
+require 'whimsy/asf/podlings'
+require 'whimsy/asf/site'
+
+$SAFE = 1
+
+# This is a version number check embedded in the json files.
+# 
+# The script started generating format numbers on 2012-08-28 but had been
+# in production for some number before that.
+FORMAT_NUMBER = 4
+
+user = ASF::Person.new($USER)
+AUTHORIZED = (user.asf_member? or ASF.pmc_chairs.include?(user) or $USER=='ea')
+if !AUTHORIZED && env['REQUEST_METHOD'].to_s != 'GET'
+  print "Status: 401 Unauthorized\r\n"
+  print "WWW-Authenticate: Basic realm=\"ASF Members and Officers\"\r\n\r\n"
+  exit
+end
+
+lists = ASF::Mail.lists
+pmcs = ASF::Committee.list.map(&:mail_list)
+pmcs.delete_if {|pmc| not lists.include? "#{pmc}-private"}
+
+# INFRA-11555
+# The validation done by this script must agree with the validation done by
+# the script that processes the json files:
+# https://svn.apache.org/repos/infra/infrastructure/trunk/mlreq/queuerun.py
+  
+MLID_PAT = '^[a-z0-9]+(-[a-z0-9]+)?$'
+# TLPs may include '-' in name e.g. empire-db
+# TODO tighten RE to match only a single non-leading '-'
+PROJ_PAT = '^[a-z][-a-z0-9]+$'
+# Podlings cannot include '-' (don't want any more hyphenated names)
+POD_PAT = '^[a-z][a-z0-9]+$'
+
+_html do
+
+  incubator = (env['PATH_INFO'].to_s.include? 'incubator')
+
+  _head_ do
+    if incubator
+      _title 'ASF Incubator Mailing List Request'
+    else
+      _title 'ASF Mailing List Request'
+    end
+    _script src: '/jquery-min.js'
+    _style %{
+      textarea, .mod, label {display: block}
+      input[type=submit] {display: block; margin-top: 1em}
+      input[name=podling], input[type=checkbox], input[type=radio], p, .mod, textarea {margin-left: 2em}
+      .subdomain, .domain {color: #000}
+      legend {background: #141; color: #DFD; padding: 0.4em}
+      .name {width: 6em}
+      ._stdin {color: #C000C0; margin-top: 1em}
+      ._stdout {color: #000}
+      .error, ._stderr {color: #F00}
+      .request {background-color: #BDF}
+    }
+  end
+
+  _body? do
+    if _.post?
+      Dir.chdir '/var/tools/infra/mlreq'
+      `/bin/rm -- *`
+      `svn revert --non-interactive -R ./`
+      `svn update --non-interactive`
+
+      # extract moderators from input fields or text area
+      mods = params.select {|name,value| name =~ /^mod\d+$/ and value != ['']}.
+        values.flatten.join(',')
+      mods = @mods.strip.gsub(/\s+/,',') if @mods
+
+      # build a queue of requests
+      queue = []
+
+      unless incubator
+        queue << {
+          version: FORMAT_NUMBER,
+          type: 'toplevel',
+          private: (@private == 'true' || @localpart == 'private' || @localpart == 'security'),
+          subdomain: @subdomain,
+          localpart: @localpart,
+          domain: @domain || 'apache.org',
+          moderators: mods,
+          muopts: @muopts,
+          replytolist: (@replyto == "true"),
+          notifyee: "private@#{@subdomain}.apache.org"
+        }
+      else # incubator request
+        params.keys.grep(/^suffix\d+/).each do |name|
+          suffix = params[name].first
+          next if suffix.empty?
+          queue << {
+            version: FORMAT_NUMBER,
+            type: 'podling',
+            private: (params[name.sub('suffix','private')].first == 'true' || suffix == 'private' || suffix == 'security'),
+            subdomain: @podling,
+            outhost: "#{@podling}.incubator.apache.org",
+            localpart: suffix,
+            domain: @domain || 'apache.org',
+            moderators: mods,
+            muopts: @muopts,
+            replytolist: (@replyto == "true"),
+            notifyee: "private@incubator.apache.org"
+          }
+        end
+      end
+
+      # build a list of validation errors
+      errors = []
+
+      # TODO this list ought to be synchronized with the patterns applied to the HTML fields
+      checks = {
+        localpart: Regexp.new(MLID_PAT),
+        subdomain: Regexp.new(PROJ_PAT),
+        domain: /^apache[.]org$/,
+        muopts: /^(mu|Mu|mU)$/,
+        notifyee: /^\w+[@]\w+[.]apache[.]org$/
+      }
+
+      queue.each do |vars|
+        checks.each do |name, pattern|
+          if pattern and vars[name] !~ pattern
+            errors << "Invalid #{name}: #{vars[name].inspect}"
+          end
+        end
+
+        vars[:moderators].split(',').each do |email|
+          begin
+            if email != Mail::Address.new(email).address
+              errors << "Invalid email: #{email.inspect}"
+            end
+            if email =~ /@apache\.org$/ and not ASF::Person.find_by_email(email)
+              errors << "Account does not exist: #{email.inspect}"
+            end
+          rescue
+            errors << "Invalid email: #{email.inspect}"
+          end
+        end
+
+        unless incubator or pmcs.include? vars[:subdomain]
+          errors << "Invalid PMC: #{vars[:subdomain]}"
+        end
+
+        mlreq = "#{vars[:subdomain]}-#{vars[:localpart]}".gsub(/[^-\w]/,'_')
+        if File.exist? "#{mlreq.untaint}.json"
+          errors << "Already submitted: " +
+            "#{vars[:localpart]}@#{vars[:subdomain]}.#{vars[:domain]}"
+        end
+      end
+
+      # output requests or errors
+      tocommit = []
+      if errors.empty?
+        _h2_ "Submitted request(s)"
+        queue.each do |vars|
+          mlreq = "#{vars[:subdomain]}-#{vars[:localpart]}".
+                    gsub(/[^-\w]/,'_')
+          mlreq = "input/#{mlreq}"
+          vars[:message] = @message unless @message.empty?
+          request = JSON.pretty_generate(vars) + "\n"
+          _pre.request request
+          vars[:mlreq] = "#{mlreq.untaint}.json"
+          File.open(vars[:mlreq],'w') { |file| file.write request }
+          _.system(['svn', 'add', '--', vars[:mlreq]])
+          tocommit << vars[:mlreq]
+        end
+
+        if incubator
+          # Use '+' so it sorts first.
+          mlreq = "#{queue.first[:subdomain]}".gsub(/[^-\w]/,'_')
+          mlreq = "input/#{mlreq.untaint}+.json"
+          File.open(mlreq, 'w') { |file|
+            file.write JSON.pretty_generate({
+              version: FORMAT_NUMBER,
+              type: 'dirs',
+              subdomain: queue.first[:subdomain],
+            }) + "\n"
+          }
+          _.system(['svn', 'add', '--', mlreq])
+          tocommit << mlreq
+        end
+
+        if queue.length == 1
+          vars = queue.first
+          request = "#{vars[:localpart]}@#{vars[:subdomain]}.apache.org"
+        else
+          request = "#{@podling}-* (podling)"
+        end
+
+        _.system [
+          'svn', 'commit', '--no-auth-cache', '--non-interactive',
+          '-m', "#{request} mailing list request by #{$USER} via " + 
+            env['SERVER_ADDR'],
+          (['--username', $USER, '--password', $PASSWORD] if $PASSWORD),
+          '--', *tocommit
+        ]
+        _p do
+          _strong "Next steps:"
+          _ "We will create the lists and email"
+          _ Hash[queue.map { |vars| [vars[:notifyee],1] }].
+                       keys.sort.join(', ')
+          _ "once we have done that."
+          _{"There is <em>no need</em> to file a JIRA."}
+        end
+      else
+        _h2_.error 'Form not submitted due to errors'
+        _ul do
+          errors.each { |error| _li error }
+        end
+      end
+    end
+
+    unless _.post?
+      _p do
+        if incubator
+          _ "Looking to create a non-Incubator mailing list?  Try"
+          _a "ASF Mailing List Request", href: '../mlreq'
+          _ 'instead.'
+        else
+          _ "Looking to create a Incubator mailing list?  Try"
+          _a "ASF Incubator Mailing List Request", href: 'mlreq/incubator'
+          _ 'instead.'
+        end
+      end
+    end
+    
+    _form method: 'post' do
+      _fieldset do
+        if incubator
+          _legend 'ASF Incubator Mailing List Request'
+
+          _h3_ 'Podling name'
+          _input.name name: 'podling', required: true, pattern: POD_PAT,
+            placeholder: 'name'
+
+          _h3_ 'List name'
+          _div.list do
+            _input type: 'checkbox', name: 'private1', value: 'true'
+            _input.name.list name: 'suffix1', required: true, 
+              placeholder: 'list', pattern: MLID_PAT
+            _ '@'
+            _input.name.podling disabled: true, placeholder: '<podling>'
+            _ '.'
+            _input.name.subdomain value: 'incubator', disabled: true
+            _ '.'
+            _input.name.domain value: 'apache.org', disabled: true
+          end
+          _p "Check box next to lists which are to have private archives."
+        else
+          _legend 'ASF Mailing List Request'
+
+          _h3_ 'List name'
+          _input type: 'checkbox', name: 'private', value: 'true'
+          _input.name name: 'localpart', required: true, pattern: MLID_PAT,
+            placeholder: 'name'
+          _ '@'
+          _select name: 'subdomain' do
+            pmcs.sort.each do |pmc|
+              _option pmc unless pmc == 'incubator'
+            end
+          end
+          _ '.'
+          _input.name.domain value: 'apache.org', disabled: true
+          _p "Check box if list archives are to be private."
+        end
+        _p do
+          _ "Lists named "
+          _code 'private'
+          _ "or"
+          _code 'security'
+          _ "will always have private archives,"
+          _ "whether or not the box is checked."
+        end
+
+        _h3_ 'Replies'
+        _label do
+          _input type: 'checkbox', name: 'replyto', value: 'true', checked: true
+          _ 'Set Reply-To list header?'
+        end
+        _p! do
+          _ "If checked, replies will go to the same list.  "
+          _ "Except for lists named "
+          _code 'commits'
+          _ ", which will direct replies to the corresponding "
+          _code 'dev'
+          _ " list."
+        end
+
+        _h3_ 'Moderation'
+        _label do
+          _input type: "radio", name: "muopts", value: "mu", required: true,
+            checked: true
+          _ 'allow subscribers to post, moderate all others'
+        end
+        _label do
+          _input type: "radio", name: "muopts", value: "Mu"
+          _ 'allow subscribers to post, reject all others'
+        end
+        _label do
+          _input type: "radio", name: "muopts", value: "mU"
+          _ 'moderate all posts'
+        end
+        _p do
+          _ "Lists named"
+          _code 'private'
+          _ "always permit posts by non-subscribers."
+        end
+
+        _h3_ 'Moderators\' addresses'
+        _textarea.mods! name: 'mods'
+
+        _h3_ 'Notes'
+        _textarea name: 'message', cols: 70
+
+        if AUTHORIZED
+          _input type: 'submit', value: 'Submit Request'
+        else
+          _input type: 'submit', value: 'Only ASF Members and Officers may submit mailing list requests', disabled: true
+        end
+      end
+    end
+
+    _script_ %{
+      // replace moderator textarea with two input fields
+      $('#mods').replaceWith('<input type="email" required="required" ' +
+        'class="mod" name="mod0" placeholder="email"/>')
+      $('.mod:last').after('<input type="email" required="required" ' +
+        'class="mod" name="mod1" placeholder="email"/>')
+
+      // initially disable suffix and private (until podling is entered)
+      $('input[name=suffix1]').attr('disabled', true);
+      $('input[name=private1]').attr('disabled', true);
+
+      // process keystrokes for moderator input fields
+      var mkeyup = function() {
+        // when there are no more empty moderator fields, add one more
+        if (!$('.mod').filter(function() {return $(this).val()==''}).length) {
+          var input = $('<input type="email" class="mod" value=""/>');
+          input.attr('name', 'mod' + $('.mod').length);
+          input.bind('input', mkeyup);
+          lastmod.after(input);
+          lastmod = input;
+        }
+
+        // split on commas and spaces
+        var comma = $(this).val().search(/[, ]/);
+        if (comma != -1) {
+          lastmod.val($(this).val().substr(comma+1)).focus().trigger('input');
+          $(this).val($(this).val().substr(0,comma));
+        } else if ($(this).val() == '' && this != lastmod[0]) {
+          if (!$(this).attr('required')) $(this).remove();
+        }
+      }
+
+      // process keystrokes for podling input fields
+      var pkeyup = function() {
+        if ($(this).val() != '') {
+          $('input[type=checkbox]', $(this).parent()).removeAttr('disabled');
+          var div = $(this).parent().clone();
+          var input = $('input:not(:disabled)', div);
+          input.attr('name', 'suffix' + ($('div.list').length+1)).val('').
+            attr('required', false).bind('input', pkeyup);
+          $('input[type=checkbox]', div).attr('disabled', true).
+            prop('checked', false).
+            attr('name', 'private' + ($('div.list').length+1));
+          lastpod.unbind().bind('input', function() {
+            if ($(this).val() == 'private' || $(this).val() == 'security') {
+              $('input[type=checkbox]', $(this).parent()).prop('checked', true);
+            }
+          });
+          lastpod.parent().after(div);
+          lastpod = input;
+        }
+      }
+
+      // initial bind of keystroke handlers
+      var lastmod = $('.mod:last');
+      var lastpod = $('div.list:last input[required]');
+      $('.mod').bind('input', mkeyup);
+      lastpod.bind('input', pkeyup);
+
+      // whenever podling is set, copy values and enable suffix
+      $('input[name=podling]').bind('input', function() {
+        if ($(this).val() != '') {
+          $('input.podling').val($(this).val()).css('color', '#000');
+          $('input[name=suffix1]').removeAttr('disabled');
+        }
+      }).trigger('keyup');
+
+      var message = $('<h2>Validating form fields</h2>');
+      message.hide();
+      $('p:last').after(message);
+      validated = false;
+
+      // prevalidate the form before actual submission
+      $('form').submit(function() {
+        message.show();
+        if (!validated) {
+          $.post('', $('form').serialize(), function(_) {
+            var resubmit = false;
+
+            // perform the server indicated actions
+            if (_.ok) {
+              validated = resubmit = true;
+            } else if (_.confirm) {
+              if (confirm(_.confirm)) {
+                resubmit = true;
+              } else {
+                _.validated = {}
+              }
+            } else {
+              alert(_.alert || _.exception || 'Server error');
+            }
+
+            // mark confirmed and checked fields as validated
+            for (var name in _.validated) {
+              if (!$('input[name='+name+']').length) {
+                $('form').append('<input type="hidden" name="'+name+'"/>');
+              }
+              $('input[name='+name+']').val(_.validated[name]);
+            }
+
+            // complete the action, hide the message, and optionall resubmit
+            if (_.focus) $(_.focus).focus();
+            message.hide();
+            if (resubmit) $('form').submit();
+          }, 'json');
+          return false;
+        };
+      });
+    }
+  end
+end
+
+_json do
+  validated = {}
+  _validated validated
+
+  # confirm if podling is new (has no existing lists)
+  if @podling != @confirmed_podling
+    validated['confirmed_podling'] = @podling
+    if not lists.any? {|list| list.sub(/^incubator-/, '').start_with? "#{@podling}-"}
+
+      # extract the names of podlings (and aliases) from podlings.xml
+      require 'nokogiri'
+      incubator_content = ASF::SVN['asf/incubator/public/trunk/content']
+      current = Nokogiri::XML(File.read("#{incubator_content}/podlings.xml")).
+        search('podling[status=current]')
+      podlings = current.map {|podling| podling['resource']}
+      podlings += current.map {|podling| podling['resourceAliases']}.compact.
+        map {|names| names.split(/[, ]+/)}.flatten
+
+      if not podlings.include? @podling
+        _confirm "Podling #{@podling} not found.  Continue?"
+        next _focus 'input[name=podling]'
+      end
+    end
+  end
+
+  # confirm if pmc is unknown
+  if @subdomain != @confirmed_localpart
+    validated['confirmed_localpart'] = @subdomain
+    if not pmcs.include? @subdomain
+      _confirm "PMC #{@subdomain} not found.  Continue?"
+      next _focus 'input[name=subdomain]'
+    end
+  end
+
+  # alert if incubator list requested already exists
+  params.keys.grep(/^suffix\d+$/).each do |param|
+    next if params[param].first.empty?
+    localpart = "#{@podling}-#{params[param].first}"
+    if lists.any? {|list| list == "incubator-#{localpart}"}
+      _alert "List #{localpart}@incubator.apache.org already exists."
+      _focus "input[name=#{param}]"
+      break
+    end
+    if lists.any? {|list| list == localpart}
+      _alert "List #{localpart}.apache.org already exists."
+      _focus "input[name=#{param}]"
+      break
+    end
+  end
+
+  # alert if non-incubator list requested already exists
+  if @localpart
+    if lists.any? {|list| list == "#{@subdomain}-#{@localpart}"}
+      _alert "List #{@localpart}@#{@subdomain}.apache.org already exists."
+      _focus "input[name=localpart]"
+    end
+  end
+
+  next if _['alert']
+
+  # confirm if moderator email is unknown
+  params.keys.grep(/^mod\d+$/).each do |param|
+    email = params[param].first
+    next if email.empty?
+    next if params.any? do |key,value| 
+      key =~ /^confirmed_mod/ && value.first == email
+    end
+
+    validated["confirmed_#{param}"] = email
+    if not ASF::Person.find_by_email(email)
+      _confirm "Unknown E-mail #{email}.  Proceed with a non-committer moderator?"
+      _focus "input[name=#{param}]"
+      break
+    end
+  end
+
+  _ok 'OK' if not _['confirm']
+end

-- 
To stop receiving notification emails like this one, please contact
"commits@whimsical.apache.org" <co...@whimsical.apache.org>.