#! /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]