lib/google_apps/transport.rb in google_apps-0.5 vs lib/google_apps/transport.rb in google_apps-0.9
- old
+ new
@@ -2,71 +2,55 @@
require 'openssl'
require 'rexml/document'
module GoogleApps
class Transport
- attr_reader :request, :response, :domain, :feeds
- attr_accessor :auth, :user, :group, :nickname, :export, :group, :requester, :migration
+ attr_reader :domain, :token
+ attr_reader :user, :group, :nickname, :export, :pubkey, :requester, :migration
- SUCCESS_CODES = [200, 201, 202]
BOUNDARY = "=AaB03xDFHT8xgg"
PAGE_SIZE = {
user: 100,
group: 200
}
+ FEEDS_ROOT = 'https://apps-apis.google.com/a/feeds'
- def initialize(domain, targets = {})
- @feeds_root = 'https://apps-apis.google.com/a/feeds'
- @auth = targets[:auth] || "https://www.google.com/accounts/ClientLogin"
- @user = targets[:user] || "#{@feeds_root}/#{domain}/user/2.0"
- @pubkey = targets[:pubkey] || "#{@feeds_root}/compliance/audit/publickey/#{domain}"
- @migration = targets[:migration] || "#{@feeds_root}/migration/2.0/#{domain}"
- @group = targets[:group] || "#{@feeds_root}/group/2.0/#{domain}"
- @nickname = targets[:nickname] || "#{@feeds_root}/#{domain}/nickname/2.0"
- @audit = "#{@feeds_root}/compliance/audit/mail"
- @export = targets[:export] || "#{@audit}/export/#{domain}"
- @monitor = targets[:monitor] || "#{@audit}/monitor/#{domain}"
- @domain = domain
- @requester = AppsRequest || targets[:requester]
- @doc_handler = DocumentHandler.new format: (targets[:format] || :atom)
- @token = nil
- @response = nil
- @request = nil
- @feeds = []
- end
+ def initialize(options)
+ @domain = options[:domain]
+ @token = options[:token]
+ @refresh_token = options[:refresh_token]
+ @token_changed_callback = options[:token_changed_callback]
+ @user = "#{FEEDS_ROOT}/#{@domain}/user/2.0"
+ @pubkey = "#{FEEDS_ROOT}/compliance/audit/publickey/#{@domain}"
+ @migration = "#{FEEDS_ROOT}/migration/2.0/#{@domain}"
+ @group = "#{FEEDS_ROOT}/group/2.0/#{@domain}"
+ @nickname = "#{FEEDS_ROOT}/#{@domain}/nickname/2.0"
- # authenticate will take the provided account and
- # password and attempt to authenticate them with
- # Google
- #
- # authenticate 'username@domain', 'password'
- #
- # authenticate returns the HTTP response received
- # from Google
- def authenticate(account, pass)
- add(@auth, nil, auth_body(account, pass), :auth)
+ audit_root = "#{FEEDS_ROOT}/compliance/audit/mail"
+ @export = "#{audit_root}/export/#{@domain}"
+ @monitor = "#{audit_root}/monitor/#{@domain}"
- set_auth_token
-
- @response
+ @requester = AppsRequest
+ @doc_handler = DocumentHandler.new
end
-
# request_export performs the GoogleApps API call to
# generate a mailbox export. It takes the username
# and an GoogleApps::Atom::Export instance as
# arguments
#
# request_export 'username', document
#
# request_export returns the request ID on success or
# the HTTP response object on failure.
def request_export(username, document)
- result = add(@export + "/#{username}", :export_response, document)
+ response = add(export + "/#{username}", document)
+ process_response(response)
+ export = create_doc(response.body, :export_response)
- result.find('//apps:property').inject(nil) do |request_id, node|
+ export.find('//apps:property').inject(nil) do |request_id, node|
node.attributes['name'] == 'requestId' ? node.attributes['value'].to_i : request_id
end
end
@@ -77,30 +61,32 @@
# export_status 'username', 847576
#
# export_status will return the body of the HTTP response
# from Google
def export_status(username, req_id)
- get(@export + "/#{username}", :export_status, req_id)
+ response = get(export + "/#{username}", req_id)
+ process_response(response)
+ create_doc(response.body, :export_status)
end
+ def create_doc(response_body, type = nil)
+ @doc_handler.create_doc(response_body, type)
+ end
# export_ready? checks the export_status response for the
# presence of an apps:property element with a fileUrl name
# attribute.
#
- # export_ready? 'lholcomb2', 82834
+ # export_ready?(export_status('username', 847576))
#
# export_ready? returns true if there is a fileUrl present
# in the response and false if there is no fileUrl present
# in the response.
- def export_ready?(username, req_id)
- export_status(username, req_id)
-
- !(export_file_urls.empty?)
+ def export_ready?(export_status_doc)
+ export_file_urls(export_status_doc).any?
end
-
# fetch_export downloads the mailbox export from Google.
# It takes a username, request id and a filename as
# arguments. If the export consists of more than one file
# the file name will have numbers appended to indicate the
# piece of the export.
@@ -108,12 +94,13 @@
# fetch_export 'lholcomb2', 838382, 'lholcomb2'
#
# fetch_export reutrns nil in the event that the export is
# not yet ready.
def fetch_export(username, req_id, filename)
- if export_ready?(username, req_id)
- download_export(filename).each_with_index { |url, index| url.gsub!(/.*/, "#{filename}#{index}")}
+ export_status_doc = export_status(username, req_id)
+ if export_ready?(export_status_doc)
+ download_export(export_status_doc, filename).each_with_index { |url, index| url.gsub!(/.*/, "#{filename}#{index}")}
else
nil
end
end
@@ -121,14 +108,14 @@
# download makes a get request of the provided url
# and writes the body to the provided filename.
#
# download 'url', 'save_file'
def download(url, filename)
- @request = @requester.new :get, URI(url), headers(:other)
+ request = requester.new :get, URI(url), headers(:other)
File.open(filename, "w") do |file|
- file.puts @request.send_request.body
+ file.puts request.send_request.body
end
end
# get is a generic target for method_missing. It is
@@ -137,17 +124,15 @@
# endpoint and an id as arguments.
#
# get 'endpoint', 'username'
#
# get returns the HTTP response received from Google.
- def get(endpoint, type, id = nil)
+ def get(endpoint, id = nil)
id ? uri = URI(endpoint + build_id(id)) : uri = URI(endpoint)
- @request = @requester.new :get, uri, headers(:other)
+ request = requester.new :get, uri, headers(:other)
- @response = @request.send_request
-
- process_response(type)
+ request.send_request
end
# get_users retrieves as many users as specified from the
# domain. If no starting point is given it will grab all the
@@ -156,54 +141,42 @@
#
# get_users start: 'lholcomb2'
#
# get_users returns the final response from google.
def get_users(options = {})
- get_all :users, options
- end
+ limit = options[:limit] || 1000000
+ response = get(user + "?startUsername=#{options[:start]}")
+ process_response(response)
+ pages = fetch_pages(response, limit, :feed)
+ return_all(pages)
+ end
+
# get_groups retrieves all the groups from the domain
#
# get_groups
#
# get_groups returns the final response from Google.
def get_groups(options = {})
- get_all :groups, options
- end
+ limit = options[:limit] || 1000000
+ response = get(group + "#{options[:extra]}" + "?startGroup=#{options[:start]}")
+ process_response(response, :feed)
+ pages = fetch_pages(response, limit, :feed)
-
- # get_all retrieves a batch of records of the specified type
- # from google. You must specify the type of object you want
- # to retreive. You can also specify a start point and a limit.
- #
- # get_all 'users', start: 'lholcomb2', limit: 300
- #
- # get_all returns the HTTP response received from Google.
- def get_all(type, options = {})
- @feeds, page = [], 0
- type = normalize_type type
-
- options[:limit] ? limit = options[:limit] : limit = 1000000
- options[:start] ? get(instance_variable_get("@#{type}") + "#{options[:extra]}" + "?#{start_query(type)}=#{options[:start]}", :feed) : get(instance_variable_get("@#{type}") + "#{options[:extra]}", :feed)
-
- fetch_feed(page, limit, :feed)
-
- #@response
- return_all
+ return_all(pages)
end
-
# Retrieves the members of the requested group.
#
# @param [String] group_id the Group ID in the Google Apps Environment
#
# @visibility public
# @return
def get_members_of(group_id, options = {})
options[:extra] = "/#{group_id}/member"
- get_all :groups, options
+ get_groups options
end
# TODO: Refactor add tos.
@@ -214,22 +187,24 @@
#
# add_member_to 'test', document
#
# add_member_to returns the response received from Google.
def add_member_to(group_id, document)
- add(@group + "/#{group_id}/member", nil, document)
+ response = add(group + "/#{group_id}/member", document)
+ process_response(response)
+ create_doc(response.body)
end
#
# @param [String] group_id The ID for the group being modified
# @param [GoogleApps::Atom::GroupOwner] document The XML document with the owner address
#
# @visibility public
# @return
def add_owner_to(group_id, document)
- add(@group + "/#{group_id}/owner", nil, document)
+ add(group + "/#{group_id}/owner", nil, document)
end
# TODO: Refactor delete froms.
# delete_member_from removes a member from a group in the
@@ -237,22 +212,20 @@
#
# delete_member_from 'test_group', 'member@cnm.edu'
#
# delete_member_from returns the respnse received from Google.
def delete_member_from(group_id, member_id)
- delete(@group + "/#{group_id}/member", member_id)
+ delete(group + "/#{group_id}/member", member_id)
end
-
- #
# @param [String] group_id Email address of group
# @param [String] owner_id Email address of owner to remove
#
# @visibility public
# @return
def delete_owner_from(group_id, owner_id)
- delete(@group + "/#{group_id}/owner", owner_id)
+ delete(group + "/#{group_id}/owner", owner_id)
end
# get_nicknames_for retrieves all the nicknames associated
# with the requested user. It takes the username as a string.
@@ -271,38 +244,34 @@
# and a GoogleApps::Atom document as arguments.
#
# add 'endpoint', document
#
# add returns the HTTP response received from Google.
- def add(endpoint, type, document, header_type = nil)
+ def add(endpoint, document, header_type = nil)
header_type = :others unless header_type
uri = URI(endpoint)
- @request = @requester.new :post, uri, headers(header_type)
- @request.add_body document.to_s
+ request = requester.new :post, uri, headers(header_type)
+ request.add_body document.to_s
- @response = @request.send_request
-
- process_response type
+ request.send_request
end
# update is a generic target for method_missing. It is
# intended to handle the general case of updating an
# item that already exists in your GoogleApps Domain.
# It takes an API endpoint and a GoogleApps::Atom document
# as arguments.
#
- # update 'endpoint', document
+ # update 'endpoint', target, document
#
# update returns the HTTP response received from Google
- def update(endpoint, type, target, document)
+ def update(endpoint, target, document)
uri = URI(endpoint + "/#{target}")
- @request = @requester.new :put, uri, headers(:other)
- @request.add_body document.to_s
+ request = requester.new :put, uri, headers(:other)
+ request.add_body document.to_s
- @response = @request.send_request
-
- process_response type
+ request.send_request
end
# delete is a generic target for method_missing. It is
# intended to handle the general case of deleting an
# item from your GoogleApps Domain. delete takes an
@@ -311,13 +280,13 @@
# delete 'endpoint', 'id'
#
# delete returns the HTTP response received from Google.
def delete(endpoint, id)
uri = URI(endpoint + "/#{id}")
- @request = @requester.new :delete, uri, headers(:other)
+ request = requester.new :delete, uri, headers(:other)
- @response = @request.send_request
+ request.send_request
end
# migration performs mail migration from a local
# mail environment to GoogleApps. migrate takes a
# username a GoogleApps::Atom::Properties dcoument
@@ -325,171 +294,118 @@
#
# migrate 'user', properties, message
#
# migrate returns the HTTP response received from Google.
def migrate(username, properties, message)
- @request = @requester.new(:post, URI(@migration + "/#{username}/mail"), headers(:migration))
- @request.add_body multi_part(properties.to_s, message)
+ request = requester.new(:post, URI(migration + "/#{username}/mail"), headers(:migration))
+ request.add_body multi_part(properties.to_s, message)
- @request.send_request
+ request.send_request
end
-
-
def method_missing(name, *args)
super unless name.match /([a-z]*)_([a-z]*)/
case $1
when "new", "add"
- self.send(:add, instance_variable_get("@#{$2}"), $2, *args)
+ response = self.send(:add, send($2), *args)
+ process_response(response)
+ create_doc(response.body, $2)
when "delete"
- self.send(:delete, instance_variable_get("@#{$2}"), *args)
+ response = self.send(:delete, send($2), *args)
+ process_response(response)
+ create_doc(response.body, $2)
when "update"
- self.send(:update, instance_variable_get("@#{$2}"), $2, *args)
+ response = self.send(:update, send($2), *args)
+ process_response(response)
+ create_doc(response.body, $2)
when "get"
- self.send(:get, instance_variable_get("@#{$2}"), $2, *args)
+ response = self.send(:get, send($2), *args)
+ process_response(response)
+ create_doc(response.body, $2)
else
super
end
end
-
private
-
- # auth_body generates the body for the authentication
- # request made by authenticate.
- #
- # auth_body 'username@domain', 'password'
- #
- # auth_body returns a string in the form of HTTP
- # query parameters.
- def auth_body(account, pass)
- "&Email=#{CGI::escape(account)}&Passwd=#{CGI::escape(pass)}&accountType=HOSTED&service=apps"
- end
-
-
# build_id checks the id string. If it is formatted
# as a query string it is returned as is. If not
# a / is prepended to the id string.
def build_id(id)
id =~ /^\?/ ? id : "/#{id}"
end
-
- # export_file_urls searches @response for any apps:property elements with a
+ # export_file_urls searches an export status doc for any apps:property elements with a
# fileUrl name attribute and returns an array of the values.
- def export_file_urls
- Atom::XML::Document.string(@response.body).find('//apps:property').inject([]) do |urls, prop|
- urls << prop.attributes['value'] if prop.attributes['name'].match 'fileUrl'
- urls
+ def export_file_urls(export_status_doc)
+ export_status_doc.find("//apps:property[contains(@name, 'fileUrl')]").collect do |prop|
+ prop.attributes['value']
end
end
-
- def download_export(filename)
- export_file_urls.each_with_index do |url, index|
+ def download_export(export, filename)
+ export_file_urls(export).each_with_index do |url, index|
download(url, filename + "#{index}")
end
end
# process_response takes the HTTPResponse and either returns a
# document of the specified type or in the event of an error it
# returns the HTTPResponse.
- def process_response(doc_type = nil)
- case doc_type
- when nil
- success_response? ? true : raise("Error: #{response.code}, #{response.message}")
- else
- success_response? ? @doc_handler.create_doc(@response.body, doc_type) : raise("Error: #{response.code}, #{response.message}")
- end
+ def process_response(response)
+ raise("Error: #{response.code}, #{response.message}") unless success_response?(response)
end
-
- # error_response? checks to see if Google Responded with a success
- # code.
- def success_response?
- SUCCESS_CODES.include?(@response.code.to_i)
+ def success_response?(response)
+ response.kind_of?(Net::HTTPSuccess)
end
-
- #
-
# Takes all the items in each feed and puts them into one array.
#
# @visibility private
# @return Array of Documents
- def return_all
- @feeds.inject([]) do |results, feed|
+ def return_all(pages)
+ pages.inject([]) do |results, feed|
results | feed.items
end
end
-
- # Grab the auth token from the response body
- def set_auth_token
- @response.body.split("\n").grep(/auth=(.*)/i)
-
- @token = $1
- end
-
-
# get_next_page retrieves the next page in the response.
- def get_next_page(type)
- get @feeds.last.next_page, type
- add_feed
+ def get_next_page(next_page_url, type)
+ response = get(next_page_url)
+ process_response(response)
+ GoogleApps::Atom.feed(response.body)
end
# fetch_feed retrieves the remaining pages in the request.
# It takes a page and a limit as arguments.
- def fetch_feed(page, limit, type)
- add_feed
- page += 1
+ def fetch_pages(response, limit, type)
+ pages = [GoogleApps::Atom.feed(response.body)]
- while (@feeds.last.next_page) and (page * PAGE_SIZE[:user] < limit)
- get_next_page type
- page += 1
+ while (pages.last.next_page) and (pages.count * PAGE_SIZE[:user] < limit)
+ pages << get_next_page(pages.last.next_page, type)
end
+ pages
end
-
- # start_query builds the value for the starting point
- # query string used for retrieving batches of objects
- # from Google.
- def start_query(type)
- case type
- when 'user'
- "startUsername"
- when 'group'
- "startGroup"
- end
+ def singularize(type)
+ type.to_s.gsub(/s$/, '')
end
-
- def normalize_type(type)
- type.to_s.gsub!(/\w*s$/) { |match| match[0..-2] }
- end
-
-
- # add_feed adds a feed to the @feeds array.
- def add_feed
- @feeds << GoogleApps::Atom.feed(@response.body)
- end
-
-
def headers(category)
case category
when :auth
[['content-type', 'application/x-www-form-urlencoded']]
when :migration
- [['content-type', "multipart/related; boundary=\"#{BOUNDARY}\""], ['authorization', "GoogleLogin auth=#{@token}"]]
+ [['content-type', "multipart/related; boundary=\"#{BOUNDARY}\""], ['Authorization', "OAuth #{@token}"]]
else
- [['content-type', 'application/atom+xml'], ['authorization', "GoogleLogin auth=#{@token}"]]
+ [['content-type', 'application/atom+xml'], ['Authorization', "OAuth #{@token}"]]
end
end
-
def multi_part(properties, message)
post_body = []
post_body << "--#{BOUNDARY}\n"
post_body << "Content-Type: application/atom+xml\n\n"
\ No newline at end of file