File Coverage

blib/lib/WWW/Google/Login.pm
Criterion Covered Total %
statement 24 130 18.4
branch 0 14 0.0
condition 0 21 0.0
subroutine 8 19 42.1
pod 5 10 50.0
total 37 194 19.0


line stmt bran cond sub pod time code
1             package WWW::Google::Login;
2              
3 1     1   689 use strict;
  1         2  
  1         34  
4 1     1   528 use Moo 2;
  1         12008  
  1         6  
5 1     1   2381 use WWW::Mechanize::Chrome;
  1         161744  
  1         45  
6 1     1   873 use Log::Log4perl ':easy';
  1         44225  
  1         5  
7              
8 1     1   735 use Filter::signatures;
  1         2  
  1         9  
9 1     1   27 use feature 'signatures';
  1         2  
  1         84  
10 1     1   6 no warnings 'experimental::signatures';
  1         2  
  1         42  
11              
12 1     1   515 use WWW::Google::Login::Status;
  1         3  
  1         1148  
13              
14             our $VERSION = '0.01';
15              
16             =head1 NAME
17              
18             WWW::Google::Login - log a mechanize object into Google
19              
20             =head1 SYNOPSIS
21              
22             my $mech = WWW::Mechanize::Chrome->new(
23             headless => 1,
24             data_directory => tempdir(CLEANUP => 1),
25             user_agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36+',
26             );
27             $mech->viewport_size({ width => 480, height => 640 });
28              
29             $mech->get('https://keep.google.com');
30              
31             my $login = WWW::Google::Login->new(
32             mech => $mech,
33             );
34              
35             if( $login->is_login_page()) {
36             my $res = $login->login(
37             user => 'a.u.thor@gmail.com',
38             password => 'my-secret-password',
39             headless => 1
40             );
41              
42             if( $res->wrong_password ) {
43             # ?
44             } elsif( $res->logged_in ) {
45             # yay
46             } else {
47             # some other error
48             }
49             };
50              
51             =head1 DESCRIPTION
52              
53             This module automates logging in a (Javascript capable) WWW::Mechanize
54             object into Google. This is useful for scraping information from Google
55             applications.
56              
57             Currently, this module only works in conjunction with L,
58             but ideally it will evolve to not requiring Javascript or Chrome at all.
59              
60             =cut
61              
62             has 'logger' => (
63             is => 'ro',
64             default => sub {
65             get_logger(__PACKAGE__),
66             },
67             );
68              
69             has 'mech' => (
70             is => 'ro',
71             is_weak => 1,
72             );
73              
74             has 'console' => (
75             is => 'rw',
76             );
77              
78 0     0 0   sub mask_headless( $self, $mech ) {
  0            
  0            
  0            
79             my $console = $mech->add_listener('Runtime.consoleAPICalled', sub {
80             warn "[] " . join ", ",
81 0   0       map { $_->{value} // $_->{description} }
82 0     0     @{ $_[0]->{params}->{args} };
  0            
83 0           });
84 0           $self->console($console);
85              
86 0           $mech->block_urls(
87             'https://fonts.gstatic.com/*',
88             );
89              
90 0           my $id = $mech->driver->send_message('Page.addScriptToEvaluateOnNewDocument', source => <<'JS' )->get;
91             Object.defineProperty(navigator, 'webdriver', {
92             get: () => false
93             });
94              
95             Object.defineProperty(navigator, 'plugins', {
96             get: () => [1,2,3,4,5]
97             });
98             Object.defineProperty(navigator, 'languages', {
99             get: () => ['en-US', 'en'],
100             });
101              
102             const myChrome = {
103             "app":{"isInstalled":false},
104             "webstore":{"onInstallStageChanged":{},"onDownloadProgress":{}},
105             "runtime": {}
106             };
107             Object.defineProperty(navigator, 'chrome', {
108             get: () => { console.log("chrome property accessed"); myChrome }
109             });
110              
111             const connection = { rtt: 100, downlink: 1.6, effectiveType: "4g", downlinkMax: null };
112             Object.defineProperty(navigator, 'connection', {
113             get: () => (connection),
114             });
115              
116             const originalQuery = window.navigator.permissions.query;
117             window.navigator.permissions.query = (parameters) => {
118             console.log("permission query for " + parameters.name);
119             parameters.name === 'notifications' ?
120             Promise.resolve({ state: Notification.permission }) :
121             originalQuery(parameters)
122             };
123              
124             console.log("Page " + window.location);
125             JS
126              
127             #$mech->agent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36+');
128 0           $mech->get('about:blank');
129             }
130              
131 0     0 0   sub login_headfull( $self, %options ) {
  0            
  0            
  0            
132 0   0       my $l = $options{ logger } || $self->logger;
133 0   0       my $mech = $options{ mech } || $self->mech;
134 0           my $user = $options{ user };
135 0           my $password = $options{ password };
136 0           my $logger = $self->logger;
137 0 0         if( ! $self->is_password_page_headfull ) {
138 0           my @email = $mech->wait_until_visible( selector => '//input[@type="email"]' );
139              
140 0           my $username = $email[0]; # $mech->xpath('//input[@type="email"]', single => 1 );
141 0           $username->set_attribute('value', $user);
142 0           $mech->click({ xpath => '//*[@id="identifierNext"]' });
143             };
144              
145             # Give time for password page to load
146 0           $mech->wait_until_visible( selector => '//input[@type="password"]' );
147 0           my $field = $mech->selector( '//input[@type="password"]', one => 1 );
148             #print $field->get_attribute('id'), "\n";
149             #print $field->get_attribute('name'), "\n";
150             #print $field->get_attribute('outerHTML'), "\n";
151 0           my $password_field =
152             $mech->xpath( '//input[@type="password"]', single => 1 );
153              
154 0           my $password_html = $mech->selector('#password', single => 1 );
155 0           $mech->click( $password_html ); # html "field" to enable the real field
156 0           $mech->sendkeys( string => $password );
157 0           $logger->info("Password entered into field");
158              
159             # Might want to uncheck 'save password' box for future
160 0           $logger->info("Clicking Sign in button");
161              
162 0           $mech->click({ selector => '#passwordNext', single => 1 }); # for headful
163              
164 0           my $error = $mech->xpath( '//*[@aria="assertive"]', maybe => 1 );
165 0 0         if( $error ) {
166 0           return WWW::Google::Login::Status->new(
167             wrong_password => 1
168             );
169             };
170              
171 0           WWW::Google::Login::Status->new(
172             logged_in => 1
173             );
174             }
175              
176 0     0 0   sub login_headless( $self, %options ) {
  0            
  0            
  0            
177 0   0       my $l = $options{ logger } || $self->logger;
178 0   0       my $mech = $options{ mech } || $self->mech;
179 0           my $user = $options{ user };
180 0           my $password = $options{ password };
181 0           my $logger = $self->logger;
182              
183 0 0         if( ! $self->is_password_page_headless ) {
184             # Click in Login Email form field
185 0           warn "Waiting for email entry field";
186 0           $mech->wait_until_visible( selector => '//input[@type="email"]' );
187 0           my $email = $mech->selector( '//input[@type="email"]', single => 1 );
188 0           $logger->info("Clicking and setting value on Email form field");
189              
190 0           $mech->field( Email => $user );
191 0           $mech->sleep(1);
192 0           $logger->info("Clicking Next button");
193 0           my $signIn_button = $mech->xpath( '//*[@name = "signIn"]', single => 1 );
194 0           my $signIn_class = $signIn_button->get_attribute('class');
195             #warn "Button class name is '$signIn_class'";
196 0           $mech->click_button( name => 'signIn' );
197             };
198              
199             # Give time for password page to load
200             #warn "Waiting for password field";
201 0           $mech->wait_until_visible( selector => '//input[@type="password"]' );
202 0           $logger->info("Clicking on Password form field");
203              
204 0           my $password_field =
205             $mech->xpath( '//input[@type="password"]', single => 1 );
206              
207 0           $mech->click($password_field); # when headless
208             #$mech->sleep(10);
209 0           $logger->info("Entering password one character at a time");
210 0           $mech->sendkeys( string => $password );
211 0           $logger->info("Password entered into field");
212              
213             # Might want to uncheck 'save password' box for future
214 0           $logger->info("Clicking Sign in button");
215 0           $mech->dump_forms;
216             #for ($mech->xpath('//form//*[@id]')) {
217             # warn $_->get_attribute('id');
218             #};
219              
220             # We should propably wait until a lot of the scripts have loaded...
221              
222 0           $mech->click({ xpath => '//*[@id = "signIn"]', single => 1 }); # for headless
223              
224 0           $mech->sleep(15);
225 0           $mech->wait_until_invisible(xpath => '//*[contains(text(),"Loading...")]');
226              
227 0           WWW::Google::Login::Status->new(
228             logged_in => 1
229             );
230             }
231              
232             =head2 C<< ->is_password_page >>
233              
234             if( $login->is_password_page ) {
235             $login->login( user => $user, password => $password );
236             };
237              
238             =cut
239              
240 0     0 1   sub is_password_page( $self ) {
  0            
  0            
241 0 0         $self->is_password_page_headless
242             || $self->is_password_page_headfull
243             }
244              
245 0     0 0   sub is_password_page_headfull( $self ) {
  0            
  0            
246             #() = $self->mech->selector( '#passwordNext', maybe => 1 )
247 0           $self->mech->selector( '#hiddenEmail', maybe => 1 )
248             }
249              
250 0     0 0   sub is_password_page_headless( $self ) {
  0            
  0            
251 0           $self->mech->xpath( '//input[@id="signIn"]', maybe => 1 )
252             }
253              
254             =head2 C<< ->is_login_page >>
255              
256             if( $login->is_login_page ) {
257             $login->login( user => $user, password => $password );
258             };
259              
260             =cut
261              
262 0     0 1   sub is_login_page( $self ) {
  0            
  0            
263              
264             #my @elements = $self->mech->xpath('//*[@id]');
265             #for (@elements) {
266             # warn join "\t", $_->get_attribute('id'), $_->get_attribute('type');
267             #};
268              
269 0 0 0       $self->is_login_page_headless
      0        
270             || $self->is_login_page_headfull
271             || $self->is_password_page_headfull
272             || $self->is_password_page_headless
273             }
274              
275             =head2 C<< ->is_login_page_headless >>
276              
277             =cut
278              
279 0     0 1   sub is_login_page_headless( $self ) {
  0            
  0            
280 0           $self->mech->xpath( '//*[@name = "signIn"]', maybe => 1 )
281             }
282              
283             =head2 C<< ->is_login_page_headfull >>
284              
285             =cut
286              
287 0     0 1   sub is_login_page_headfull( $self ) {
  0            
  0            
288 0           $self->mech->xpath( '//*[@id="identifierNext"]', maybe => 1 )
289             }
290              
291             =head2 C<< ->login >>
292              
293             my $res = $login->login(
294             user => 'example@gmail.com',
295             password => 'supersecret',
296             );
297             if( $res->logged_in ) {
298             # yay
299             }
300              
301             =cut
302              
303             # https://accounts.google.com/signin/v2/sl/pwd
304 0     0 1   sub login( $self, %options ) {
  0            
  0            
  0            
305 0           my $res;
306 0 0         if( $self->is_login_page_headless ) {
    0          
307 0           $res = $self->login_headless( %options )
308             } elsif( $self->is_login_page_headfull ) {
309 0           $res = $self->login_headfull( %options )
310             } else {
311 0           $res = $self->login_headfull( %options )
312             }
313 0           $res
314             }
315              
316             1;
317              
318             =head1 FUTURE IMPROVEMENTS
319              
320             =head2 API usage
321              
322             Ideally, this module would switch away from screen scraping to directly
323             automating the API below L.
324             This would make it possible to switch away from L
325             to a plain HTTP client like L or L.
326              
327             =head2 Two-factor authentication
328              
329             Two-factor authentication is not supported at all.
330              
331             =head1 SEE ALSO
332              
333             L - Google Business API
334              
335             This allows a more direct administration of (business) accounts without screen
336             scraping.
337              
338             =head1 REPOSITORY
339              
340             The public repository of this module is
341             L.
342              
343             =head1 SUPPORT
344              
345             The public support forum of this module is L.
346              
347             =head1 AUTHOR
348              
349             Max Maischein C
350              
351             =head1 COPYRIGHT (c)
352              
353             Copyright 2016-2018 by Max Maischein C.
354              
355             =head1 LICENSE
356              
357             This module is released under the same terms as Perl itself.
358              
359             =cut