ycl

Artifact [dab8fef302]
Login

Artifact [dab8fef302]

Artifact dab8fef302405278d26ea794b528f608fabbf36b:


#! /bin/env tclsh

package require tls
package require http
package require sha256
package require tdom

package require fileutil

package require {ycl file}
namespace import [yclprefix]::file::cat
namespace import [yclprefix]::file::cmp
namespace import [yclprefix]::file::puts
rename puts fputs
package require {ycl exec}
namespace import [yclprefix]::exec::exec
package require {ycl knit}
namespace import [yclprefix]::knit::knar
package require {ycl list}
namespace import [yclprefix]::list::sl
package require {ycl ns}
package require {ycl iapi com kforce finish}
namespace import [yclprefix]::iapi::com::kforce::finish::finish
package require {ycl iapi com kforce timecard}
package require {ycl iapi com kforce util}
package require {ycl proc}
namespace import [yclprefix]::proc::checkargs
namespace import [yclprefix]::iapi::com::kforce::util::clean
namespace import [yclprefix]::iapi::com::kforce::util::filename
namespace import [yclprefix]::ns
package require {ycl shelf}

variable scriptvers 0.1

[yclprefix] shelf shelf [namespace current]

[namespace current] subcmd clean
[namespace current] subcmd filename


proc kforcesocket args {
	lassign [lrange $args end-1 end] host port
	set opts [lrange $args 0 end-2]
	::tls::socket -ssl3 0 -ssl2 0 -tls1 1 -tls1.2 0 -tls1.1 0 \
		-servername $host {*}$opts $host $port
}

proc checkbackup {dirfilename backupname suffix} {
	puts stderr "wrote $dirfilename.$suffix"
	if {$backupname ne {}} {
		puts stderr "backed up to $backupname"
		puts "comparing $backupname and $dirfilename.$suffix"
		set ftype [::fileutil::fileType $dirfilename.$suffix] 
		switch $ftype {
			{text pdf} {
				if {[pdftext $backupname] eq [pdftext $dirfilename.$suffix]} {
					set cmpres -1
				} else {
					set cmpres 0
				}
			}
			text {
				set cmpres [cmp $backupname $dirfilename.$suffix] 
			}
			default {
				return -code error [list {unknown file type} $ftype]
			}
		}
		if {$cmpres > -1} {
			puts stderr [list {text files did not match} $cmpres]
		} else {
			puts stderr "removing $backupname"
			file delete -force $backupname
		}
	}
}


proc cookies {_ t} {
	set cookies {} 
	foreach {k v} [http::meta $t] {
		if {[string tolower $k] eq "set-cookie"} {
			set v [split $v \;]
			set v [lindex $v 0]
			set v [lassign [split $v =] key]
			set v [join $v =]
			if {$v eq {}} continue
			dict set cookies $key $v
		}
	}
	return $cookies
}
[namespace current] method cookies

proc downloadTimecards _ {
	while 1 {
		set entry [$_ eval history_coro1]
		if {[namespace which ${_}::history_coro1] eq {}} {
			break
		}
		#if {[string match {Submitted*} [dict get $entry status]]} {
		#	puts stderr [list not retrieving $entry]
		#	continue
		#}
		set finish [$_ getfinish $entry]
		set timecard [$_ gettimecard $entry]
		$timecard parse
		set meta [dict merge $entry $finish [$timecard $ meta]]
		$timecard $ meta $meta
		$_ processTimecard $timecard 
	}
	#foreach timecard $timecards {
	#	set sig [::sha2::sha256 -hex $timecard]
	#	set chan [open $sig w]
	#	puts $chan $timecard
	#	close $chan
	#}
}
[namespace current] method downloadTimecards


proc fields {_ args} {
	upvar 0 [$_ $.locate fields] fields
	foreach {key val} $args {
		dict set fields $key $val
	}
	return $fields
}
[namespace current] method fields

proc findTimecards _ {
	upvar 0 [$_ namespace]::cookies cookies 
	#while 1 {
	#	set entry [$_ eval history_coro1]
	#	if {[namespace which ${_}::history_coro1] eq {}} {
	#		break
	#	}
	#	set id [dict get $entry id]
	#	if {![info exists start] || $id > $start} {
	#		set start $id 
	#	}
	#}

	if 0 {
		set start 1828496
		set start 1817355
		set start 1813893
		set start 1802833
		set start 1795616
		set start 1784481 
		set start 1531015 
	}

		#temporary, to get may 2015
		#set start 1848921

		#set start 1827506
		#set start 1745667
		set start 1411824 

	while {$start} {
		for {set i 0} {$i < 60} {incr i} {
			if {![set fail [catch {
				set token [http::geturl [
			$_ $ printurl]$start&ReportFormat=html -headers [formatCookies $cookies]]
			} cres copts]]} {
				break
			}
			puts stderr [list sleeping 1000]
			after 1000
		}

		if {$fail} {
			return -opts $copts $cres
		}
		set data [http::data $token]
		if {[string match \
			{*You are not authorized to view this timecard.*} $data]} {
			puts [list $start {not authorized}]
		} elseif {[string match {*There was an error processing/printing this timecard.*} $data]} {
			puts [list $start {processing error}]
		} elseif {[string match {*Object moved to <a href*>here</a>*} $data]} {
			$_ login
			# Retry the current id
			incr start
		} else {
			puts [list $start {authorized}]
			set timecard [$_ gettimecard [dict create id $start status unknown]]
			set entry [dict create id $start]
			set finish [$_ getfinish $entry]
			$timecard parse
			set meta [dict merge $entry $finish [$timecard $ meta]]
			$timecard $ meta $meta
			$_ processTimecard $timecard 
		}
		incr start -1
	}
}
[namespace current] method findTimecards

proc form token {
	set data [http::data $token]
	dom parse -html $data html
	$html documentElement element
	$element normalize
	set fields [dict create]
	foreach input [$element getElementsByTagName input] {
		if {[$input hasAttribute name]} {
			set name [$input getAttribute name]
		} elseif {[$input hasAttribute id]} {
			set name [$input getAttribute id]
		}
		#puts stderr "input: [$input asList]"
		if {[catch {set value [$input getAttribute value]}]} {
			set value ""
		}
		if {[info exists name]} {
			dict set fields $name $value 
		}
	}
	return [dict create data $data html $html fields $fields ]
}

proc formatCookies cookies {
	set res {}
	dict for {key val} $cookies {
		lappend res $key=$val
	}
	return [list Cookie [join $res "; "]]
}

proc getfinish {_ meta} {
	upvar 0 [$_ namespace]::cookies cookies 
	set id [dict get $meta id]
	set token [http::geturl [string map [list {{{id}}} $id] [
		$_ $ finishurl]] -headers [formatCookies $cookies]]
	set finish [finish spawn {}] 
	$finish $ data [http::data $token]

	set parsed [$finish parse]
	http::cleanup $token
	rename $finish {}
	return $parsed 
}
[namespace current] method getfinish


proc gethistory {_ args} {
	if {[$_ eval {namespace which history_coro1}] eq "[$_ namespace]::history_coro1"} {
		error [list {history coroutine already in progress}]
	}
	coroutine [$_ namespace]::history_coro1 $_ history_coro
}
[namespace current] method gethistory

proc gettimecard {_ meta} {
	upvar 0 [$_ namespace]::cookies cookies 
	set id [dict get $meta id]
	set token [http::geturl [
		$_ $ printurl]$id&ReportFormat=html -headers [formatCookies $cookies]]
	set timecard [[timecard spawn {}] init meta $meta]
	$timecard $ data [http::data $token]
	http::cleanup $token
	return $timecard
}
[namespace current] method gettimecard

proc gettimecardpdf {_ id} {
	upvar 0 [$_ namespace]::cookies cookies 
	set token [http::geturl [
		$_ $ printurl]$id&ReportFormat=pdf -headers [formatCookies $cookies]]
	return [http::data $token]
}
[namespace current] method gettimecardpdf

proc findtimecards _ {
}

proc history_coro _ {
	yield [info coroutine]
	upvar 0 [$_ $.locate historyurl] historyurl 
	upvar 0 [$_ $.locate verbose] verbose 
	upvar 0 [$_ namespace]::cookies cookies

	set token [http::geturl $historyurl -headers [formatCookies $cookies]]
	#set token [http::geturl https://timeentry.kforce.com/TimeEntry.Web/Default.aspx -headers [formatCookies $cookies]]
	set cookies [dict merge $cookies [$_ cookies $token]]
	if {$verbose} {
		puts stderr "\ncookies3: $cookies"
	}
	set data [http::data $token]
	set fields [dict get [form $token] fields]
	http::cleanup $token

	set newfields {
		__EVENTARGUMENT {FireCommand:ctl00$ContentPlaceHolder1$grdTimecardHistory$ctl00;PageSize;50}
		__EVENTTARGET {ctl00$ContentPlaceHolder1$grdTimecardHistory}
		ctl00$ContentPlaceHolder1$grdTimecardHistory$ctl00$ctl03$ctl01$PageSizeComboBox {50}
		ctl00_ContentPlaceHolder1_grdTimecardHistory_ctl00_ctl03_ctl01_PageSizeComboBox_ClientState {{"logEntries":[],"value":"50","text":"50","enabled":true}}
		ctl00_ContentPlaceHolder1_historyDetailView_ClientState {}
	}
	set fields [dict merge $fields $newfields]
	puts stderr "\ncookies4: $cookies"
	set token [http::geturl $historyurl -headers [
		formatCookies $cookies] -query [http::formatQuery {*}$fields]]


	set data [http::data $token]
	http::cleanup $token
	dom parse -html $data html
	$html documentElement root
	$root normalize
	set timecard_history [$root getElementById ctl00_ContentPlaceHolder1_grdTimecardHistory_ctl00]
	puts [list doople $timecard_history]
	set timecard_history [$timecard_history selectNodes tbody]
	set timecard_history [$timecard_history selectNodes tr]
	set columns {company assignment status notes}

	foreach row $timecard_history {
		set timecard {}
		set fields [lassign [$row selectNodes td] id unknown unknown date]
		dict set timecard id [$id text]
		dict set timecard date [$_ isotime [
			clock scan [[$date selectNodes a] text] -format %N/%d/%Y -timezone :UTC]]
		for {set i 0} {$i<[llength $columns]} {incr i} {
			dict set timecard [lindex $columns $i] [[lindex $fields $i] text]
		}
		yield [$_ clean $timecard]
	}
}

[namespace current] method history_coro
proc isotime {_ time} {
	clock format $time -format %Y-%m-%d -timezone :UTC
}
[namespace current] method isotime

proc login {_} {
	upvar 0 [$_ $.locate verbose] verbose
	upvar 0 [$_ $.locate loginurl] loginurl
	upvar 0 [$_ namespace]::cookies cookies

	set token [http::geturl $loginurl]
	set cookies [$_ cookies $token]
	if {$verbose} {
		puts stderr "\ncookies1: $cookies"
	}
	set fields [dict get [form $token] fields]
	http::cleanup $token

	set formfields [dict merge $fields [$_ $ fields]]

	dict unset formfields {ctl00$ContentPlaceHolder1$ImageButton2}
	set formdata [http::formatQuery {*}$formfields]
	set token [http::geturl $loginurl -headers [formatCookies $cookies] -query $formdata]
	set cookies [dict merge $cookies [$_ cookies $token]]
	if {$verbose} {
		puts stderr "\ncookies2: $cookies"
	}
	http::cleanup $token
}
[namespace current] method login

variable doc::init {
	args {
		_ {
			description {
				positional
			}
		}
		finishurl {
			default {}
			process {
				$_ $ finishurl $finishurl
			}
		}
		historyurl {
			default {}
			process {
				$_ $ historyurl $historyurl
			}
		}
		interactive {
			default {
				lindex 0
			}
		}
		password {
			default {}
		}
		loginurl {
			default {}
			process {
				$_ $ loginurl $loginurl
			}
		}
		printurl {
			default {}
			process {
				$_ $ printurl $printurl
			}
		}
		useragent {
			default {}
			process {
				$_ useragent $useragent
			}
		}
		username {
			default {}
		}
		verbose {
			default {lindex 0}
			process {
				$_ $ verbose $verbose
			}
		}
	}
}
proc init {_ args} {
	checkargs [set doc::[namespace tail [lindex [info level 0] 0]]] {*}$args
	if {![info exists username]} {
		if {$interactive} {
			puts stderr "enter username: "
			gets stdin username
		}
	}
	if {[info exists username]} {
		$_ username $username
	}
	if {![info exists password]} {
		if {$interactive} {
			puts stderr "enter password: "
			gets stdin password
		}
	}
	if {[info exists password]} {
		$_ password $password
	}
	$_ $ cookies {}
	return $_
}
[namespace current] method init


variable doc::main {
	args {
		cmd {
			description {
				Name of command to run
			}
			default {lindex downloadTimecards }
		}
		extra {
			default {lindex {}}
		}
	}
	extra extra
}
proc main args {
	checkargs [set doc::[namespace tail [lindex [info level 0] 0]]] {*}$args
	set new [[[namespace current] spawn {}] init interactive 1 {*}$extra]
	$new login
	$new gethistory
	$new $cmd
}
[namespace current] subcmd main


proc password {_ args} {
	if {[llength $args]} {
		$_ $ password [lindex $args 0]
		$_ fields {ctl00$ContentPlaceHolder1$txtPassword} [$_ $ password]
	}
	$_ $ password
}
[namespace current] method password


proc pdftext fname {
	set strings [exec | [list pdftotext -layout $fname -]]
}


proc processTimecard {_ timecard} {
	set tcdata [$timecard $ timecard]
	set filename [$_ filename $timecard]
	set dirfilename [string range $filename 0 3]/${filename}
	file mkdir [file dirname $dirfilename]
	set fdata [list timecard  $tcdata]\n[list meta [$timecard $ meta]]
	set fname [fputs $dirfilename.dict backup 1 data $fdata newline 0]
	checkbackup $dirfilename $fname dict
	set pdf [$_ gettimecardpdf [dict get [$timecard $ meta] id]]
	rename $timecard {}
	set fname [
		fputs ${dirfilename}.pdf access wb data $pdf backup 1 newline 0]
	checkbackup $dirfilename $fname pdf
}
[namespace current] method processTimecard


proc timecard_id node {
	set href [$node getAttribute href]
	regexp {/TimeEntry\.Web/Shared/Timecard.aspx\?ID=([^&]+)&} $href -> id
	return $id
}


proc useragent {_ args} {
	if {[llength $args]} {
		$_ $ useragent [lindex $args 0]
		http::config -useragent [$_ $ useragent]
	}
	$_ $ useragent
}
[namespace current] method useragent

proc username {_ args} {
	if {[llength $args]} {
		$_ $ username [lindex $args 0]
		$_ fields {ctl00$ContentPlaceHolder1$txtUserName} [$_ $ username]
	}
	$_ $ username
}
[namespace current] method username


variable fields {
	{ctl00$ContentPlaceHolder1$ImageButton2.x} 0
	{ctl00$ContentPlaceHolder1$ImageButton2.y} 0
}

variable verbose 0

[namespace current] init {*}[sl {
	historyurl https://timeentry.kforce.com/TimeEntry.Web/Consultant/TimecardHistory.aspx 
	loginurl https://timeentry.kforce.com/TimeEntry.Web/Login.aspx 
	printurl https://timeentry.kforce.com/TimeEntry.Web/Shared/PrintTimecard.aspx?TimecardID= 
	useragent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:43.0) Gecko/20100101 Firefox/43.0"
	finishurl https://timeentry.kforce.com/TimeEntry.Web/Shared/Timecard.aspx?ID={{id}}&Mode=Finish
}]

	#useragent "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0" 
	#printurl https://timeentry.kforce.com/TimeEntry.Web/Shared/Timecard.aspx?ID=[dict get $timecard id]&Mode=Edit 


http::register https 443 [namespace which kforcesocket]