timesheet source code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

462 line
17KB

  1. <?php
  2. /* xero integration oauth 2 */
  3. /* required by XeroOauth2 in 2021 */
  4. namespace Biukop;
  5. require_once(dirname(__FILE__) . '/vendor/autoload.php');
  6. // require_once(ABSPATH . 'wp-includes/pluggable.php');
  7. require_once(dirname(__FILE__) . '/Storage.php');
  8. use \Carbon_Fields\Container;
  9. use \Carbon_Fields\Field;
  10. use \Carbon_Fields\Carbon_Fields;
  11. use phpDocumentor\Reflection\DocBlock\Tags\Method;
  12. class XeroOAuth2
  13. {
  14. private $office; // parent
  15. private $clientID = '83CC79EEC6A54B4E8C2CA7AD61D1BF69';
  16. private $clientSecret = 'axgKF-Ri60D89conDFhqZsi1wu7uLdQFGvMpino9nI-nfO3f';
  17. private $minimum_sync_interval_in_seconds = 600;
  18. public $provider;
  19. public $options = [
  20. 'scope' => ['openid email profile offline_access assets projects accounting.settings accounting.transactions accounting.contacts accounting.journals.read accounting.reports.read accounting.attachments payroll.employees payroll.payruns payroll.payslip payroll.timesheets payroll.settings files']
  21. ];
  22. public $storage;
  23. public $config;
  24. public $apiAccountingInstance; //accounting instance
  25. public $apiPayrollInstance; // payroll au instance
  26. public $xeroTenantId;
  27. private $shortcodes;
  28. private $sync;
  29. public function __construct($office)
  30. {
  31. $this->office = $office;
  32. $this->shortcodes = new XeroOauth2ShortCode($this);
  33. $this->sync = new XeroOauth2Sync($this);
  34. $this->storage = new StorageClass();
  35. add_action('init', array($this, 'init'));
  36. add_action( 'plugins_loaded', array( '\\Carbon_Fields\\Carbon_Fields', 'boot' ) );
  37. add_action('carbon_fields_register_fields', array($this, 'build_settings_page'));
  38. add_action('carbon_fields_container_activated', array($this, 'field_activated'));
  39. add_action('parse_request', array($this, 'xero_callback'));
  40. }
  41. public function __call($method, $args) {
  42. error_log("$method is not defined" );
  43. }
  44. public function sync_users($mininterval, $employeeonly, $clientsonly) {
  45. $this->sync->sync_users($mininterval, $employeeonly, $clientsonly);
  46. }
  47. public function init()
  48. {
  49. $this->provider = $this->create_provider();
  50. if ($this->refresh_token() && is_user_logged_in()) {
  51. $this->instant_sync();
  52. }
  53. }
  54. public function getTenantId() {
  55. return $this->xeroTenantId;
  56. }
  57. private function create_provider() {
  58. $provider = new \League\OAuth2\Client\Provider\GenericProvider([
  59. 'clientId' => $this->clientID,
  60. 'clientSecret' => $this->clientSecret,
  61. 'redirectUri' => $this->getRedirectURL(),
  62. 'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
  63. 'urlAccessToken' => 'https://identity.xero.com/connect/token',
  64. 'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
  65. ]);
  66. return $provider;
  67. }
  68. public function load_storage()
  69. {
  70. //$this->storage->read_value();
  71. }
  72. public function boot_carbon()
  73. {
  74. \Carbon_Fields\Carbon_Fields::boot();
  75. }
  76. function build_settings_page()
  77. {
  78. Container::make('theme_options', __('Xero Integration'))
  79. ->set_page_parent('options-general.php')
  80. ->add_fields(array(
  81. Field::make('text', 'xero_oauth2state', 'Xero Oauth2 State')
  82. ->set_attribute('maxLength', 2048)
  83. ->set_attribute('readOnly', true)
  84. ->set_default_value($this->storage->getOauth2State()),
  85. Field::make('text', 'xero_token', 'Xero Token')
  86. ->set_attribute('maxLength', 2048)
  87. ->set_attribute('readOnly', true)
  88. ->set_default_value($this->storage->getSession()['token']),
  89. Field::make('text', 'xero_refresh_token', 'RefreshToken')
  90. ->set_attribute('readOnly', true)
  91. ->set_default_value($this->storage->getSession()['refresh_token']),
  92. Field::make('text', 'xero_id_token', 'ID Token')
  93. ->set_attribute('readOnly', true)
  94. ->set_default_value($this->storage->getSession()['id_token']),
  95. Field::make('text', 'xero_tenant_id', 'Tenant ID')
  96. ->set_attribute('readOnly', true)
  97. ->set_default_value($this->storage->getSession()['tenant_id']),
  98. Field::make('text', 'xero_expires', 'Expires')
  99. ->set_attribute('readOnly', true)
  100. ->set_default_value($this->storage->getSession()['expires']),
  101. Field::make('text', 'xero_expires_human', 'Expires')
  102. ->set_attribute('readOnly', true)
  103. ->set_default_value($this->storage->getSession()['expires_human']),
  104. Field::make('html', 'crb_information_text')
  105. ->set_html('<h1>Connect/Reconnect</h2><p>if the above field is empty,
  106. or the expire date looks suspicous, please reconnect to XeroOauth2 </p>
  107. <form action="/">
  108. <input type="hidden" name="xero_reauth" value="1" />
  109. <input type="submit" class="button button-primary button-large" value="Xero Connect" />
  110. </form>')
  111. ));
  112. $this->storage->read_value();
  113. }
  114. function field_activated()
  115. {
  116. $this->storage->read_value();
  117. $xeroTenantId = (string)$this->storage->getSession()['tenant_id'];
  118. if ($xeroTenantId == "") {
  119. $this->startAuthorization();
  120. } else {
  121. $this->refresh_token();
  122. }
  123. }
  124. public function startAuthorization()
  125. {
  126. // This returns the authorizeUrl with necessary parameters applied (e.g. state).
  127. $authorizationUrl = $this->provider->getAuthorizationUrl($this->options);
  128. // Save the state generated for you and store it to the session.
  129. // For security, on callback we compare the saved state with the one returned to ensure they match.
  130. $this->storage->setOauth2State($this->provider->getState());
  131. // Redirect the user to the authorization URL.
  132. header('Location: ' . $authorizationUrl);
  133. exit();
  134. }
  135. function xero_callback()
  136. {
  137. if (isset($_GET['xero_reauth'])) {
  138. $this->startAuthorization();
  139. return;
  140. }
  141. if (!isset($_GET['xero_callback'])) {
  142. return;
  143. }
  144. // If we don't have an authorization code then get one
  145. if (!isset($_GET['code'])) {
  146. echo "Something went wrong, no authorization code found";
  147. exit("Something went wrong, no authorization code found");
  148. // Check given state against previously stored one to mitigate CSRF attack
  149. } elseif (empty($_GET['state']) || ($_GET['state'] !== $this->storage->getOauth2State())) {
  150. echo "Invalid State";
  151. $this->storage->setOauth2State("");
  152. exit('Invalid state');
  153. } else {
  154. try {
  155. // Try to get an access token using the authorization code grant.
  156. $accessToken = $this->provider->getAccessToken('authorization_code', [
  157. 'code' => $_GET['code']
  158. ]);
  159. $config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken((string)$accessToken->getToken());
  160. $identityInstance = new \XeroAPI\XeroPHP\Api\IdentityApi(
  161. new \GuzzleHttp\Client(),
  162. $config
  163. );
  164. $result = $identityInstance->getConnections();
  165. // Save my tokens, expiration tenant_id
  166. $this->storage->setToken(
  167. $accessToken->getToken(),
  168. $accessToken->getExpires(),
  169. $result[0]->getTenantId(),
  170. $accessToken->getRefreshToken(),
  171. $accessToken->getValues()["id_token"]
  172. );
  173. //we have successfully updated token, now start auto refresh
  174. update_option( "xero2_auth_refreshing_token_auto", false, true );
  175. //allow refresh token automatically on each call back
  176. // related to $this->build_settings_page
  177. $url = "/wp-admin/options-general.php?page=crb_carbon_fields_container_xero_integration.php";
  178. header('Location: ' . $url);
  179. exit();
  180. } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
  181. echo "Xero Callback failed";
  182. exit();
  183. }
  184. }
  185. }
  186. private function getRedirectURL() {
  187. return get_site_url() . "/?xero_callback";
  188. }
  189. function refresh_token($enforced=false) : bool
  190. {
  191. $refreshing_token = get_option("xero2_auth_refreshing_token_auto" , false);
  192. if ($refreshing_token &&!$enforced) {
  193. //prevent loop in refresh token when browser redirect pages
  194. return false;
  195. }
  196. $this->storage->read_value();
  197. $this->xeroTenantId = (string)$this->storage->getSession()['tenant_id'];
  198. update_option( "xero2_auth_refreshing_token_auto", true, true ); //mark a transaction
  199. if ($this->storage->getHasExpired() || $enforced ) {
  200. $this->provider = $this->create_provider();
  201. try {
  202. $newAccessToken = $this->provider->getAccessToken('refresh_token', [
  203. 'refresh_token' => $this->storage->getRefreshToken()
  204. ]);
  205. } catch (\Exception $e) {
  206. if ($enforced) {
  207. $this->email_xero_oauth2_exceptions($e, "refresh_token");
  208. echo "refresh xero security token failed, please ensure you have paid Xero subscription, " .
  209. "and then contact site administrator ";
  210. exit();
  211. }else if ( current_user_can( 'manage_options' ) ) {
  212. //Something in the case of admin,
  213. $this->startAuthorization();
  214. }else{ //normal user
  215. $current_user = wp_get_current_user();
  216. $this->email_xero_oauth2_exceptions($e,
  217. "refresh_token - non admin user: id=$current_user->Id, name=$current_user->name" );
  218. }
  219. return false;
  220. }
  221. // Save my token, expiration and refresh token
  222. $this->storage->setToken(
  223. $newAccessToken->getToken(),
  224. $newAccessToken->getExpires(),
  225. $this->xeroTenantId,
  226. $newAccessToken->getRefreshToken(),
  227. $newAccessToken->getValues()["id_token"]);
  228. }
  229. update_option( "xero2_auth_refreshing_token_auto", false, true ); // we are not refreshing token
  230. return true;
  231. }
  232. private function email_xero_oauth2_exceptions(\Exception $e, $source) {
  233. $message = "Xero Auth2 exception \n" . print_r($e, true) ;
  234. $admin_email = get_bloginfo( "admin_email" );
  235. $headers = ['Bcc: patrick@biukop.com.au'];
  236. $subject = " XeroOauth2 Exception from: $source";
  237. wp_mail($admin_email, $subject, $message, $headers);
  238. }
  239. public function get_accounting_instance() {
  240. $this->refresh_token();
  241. if ($this->apiAccountingInstance == null) {
  242. $this->config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken(
  243. (string)$this->storage->getSession()['token']);
  244. //enable debug for XeroRate issue
  245. // it cause create invoice faile as AJAX returns debug info, ruining the JSON response
  246. $this->apiAccountingInstance = new \XeroAPI\XeroPHP\Api\AccountingApi(
  247. new \GuzzleHttp\Client(['debug' => false]),
  248. $this->config
  249. );
  250. }
  251. return $this->apiAccountingInstance;
  252. }
  253. public function get_payroll_au_instance() {
  254. $this->refresh_token();
  255. if ($this->apiPayrollInstance == null) {
  256. $this->config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken(
  257. (string)$this->storage->getSession()['token']);
  258. //$this->config->setDebug(true);
  259. //$this->config->setDebugFile("/home/acaresydneycom/public_html/wp-content/plugins/ts/debug.log");
  260. $this->apiPayrollInstance = new \XeroAPI\XeroPHP\Api\PayrollAuApi(
  261. new \GuzzleHttp\Client(),
  262. $this->config
  263. );
  264. }
  265. return $this->apiPayrollInstance;
  266. }
  267. //
  268. //TS implementation
  269. //
  270. /* sync xero to wp options */
  271. public function instant_sync(){
  272. try{
  273. $this->sync_pay_item();
  274. $this->sync_payroll_calendar();
  275. }catch(\Exception $e){
  276. $this->office->log("XeroAuth2\\instant_sync() has exception :" . $e->getMessage());
  277. }
  278. }
  279. private function sync_pay_item() {
  280. if ($this->too_close_to_sync_payitem()){
  281. return;
  282. }
  283. $api = $this->get_payroll_au_instance();
  284. $xeroTenantId = $this->xeroTenantId;
  285. $page = 1;
  286. try {
  287. $result = $api->getPayItems($xeroTenantId, null, null, null, $page);
  288. foreach ($result->getPayItems()->getEarningsRates() as $e){
  289. // "EarningsRateID": "34e17d08-237a-4ae2-8115-375d1ff8a9ed",
  290. // "Name": "Overtime Hours (exempt from super)",
  291. // "EarningsType": "OVERTIMEEARNINGS",
  292. // "RateType": "MULTIPLE",
  293. // "AccountCode": "477",
  294. // "Multiplier": 1.5,
  295. // "IsExemptFromTax": true,
  296. // "IsExemptFromSuper": true,
  297. // "AccrueLeave": false,
  298. // "IsReportableAsW1": true,
  299. // "UpdatedDateUTC": "2019-03-16T13:18:19+00:00",
  300. // "CurrentRecord": false
  301. if ($e->getCurrentRecord() == "true"){
  302. $payitem_options[]= array(
  303. 'EarningsRateID' => $e->getEarningsRateID(),
  304. 'Name'=> $e->getName(),
  305. 'EarningsType'=> $e->getEarningstype(),
  306. 'RatePerUnit' => $e->getRatePerUnit(),
  307. 'RateType' => $e->getRateType(),
  308. 'AccountCode' => $e->getAccountCode(),
  309. "Multiplier"=> $e->getMultiplier(),
  310. "IsExemptFromTax" => $e->getIsExemptFromTax(),
  311. "IsExemptFromSuper"=> $e->getIsExemptFromSuper(),
  312. "AccrueLeave" => $e->getAccrueLeave(),
  313. "TypeOfUnits" => $e->getTypeOfUnits(),
  314. "CurrentRecord"=> $e->getCurrentRecord(),
  315. );
  316. }
  317. }
  318. update_option('bts_payitem_earnings_rate', $payitem_options);
  319. update_option('bts_payitem_last_sync', time());
  320. } catch (Exception $e) {
  321. echo 'Exception when calling PayrollAuApi->getPayItems: ', $e->getMessage(), PHP_EOL;
  322. }
  323. }
  324. public function sync_payroll_calendar() {
  325. if ($this->too_close_to_sync_payroll_calendar()){
  326. return;
  327. }
  328. $pc = $this->get_payroll_calendar();
  329. $start = $pc->getStartDateAsDate()->format('Y-m-d');
  330. $finish = new \DateTime($start);
  331. $finish = $finish->modify("+13 days")->format('Y-m-d');
  332. $paydate = $pc->getPaymentDateAsDate()->format('Y-m-d');
  333. $calendar["start"] = $start;
  334. $calendar["finish"] = $finish;
  335. $calendar["paydate"] = $paydate;
  336. update_option('bts_pay_roll_calendar', $calendar);
  337. update_option('bts_pay_roll_calendar_last_sync', time());
  338. }
  339. private function too_close_to_sync_payitem(){
  340. $lastsync = get_option('bts_payitem_last_sync', 0);
  341. $now = time();
  342. $diff = $now - (int) $lastsync;
  343. return $diff < $this->minimum_sync_interval_in_seconds; //default 10 minutes
  344. }
  345. private function too_close_to_sync_payroll_calendar(){
  346. $lastsync = get_option('bts_pay_roll_calendar_last_sync', 0);
  347. $now = time();
  348. $diff = $now - (int) $lastsync;
  349. return $diff < 3.0 * $this->minimum_sync_interval_in_seconds; //default 1.3 * 10 minutes
  350. }
  351. public function getClients($contact_group_id = null) {
  352. $ret = $this->sync->getClients($contact_group_id);
  353. $this->shortcodes->updateClientsShortCode($ret);
  354. return $ret;
  355. }
  356. public function getEmployees(){
  357. $ret = $this->sync->getEmployees();
  358. $this->shortcodes->updateEmployeeShortCode($ret);
  359. return $ret;
  360. }
  361. public function devGetEmployees() {
  362. $this->sync->sync_users(600);
  363. }
  364. /*
  365. *
  366. * @param XeroAPI\Model\Accounting\Contact $client
  367. */
  368. public function getClientAddress($client) {
  369. return $this->sync->get_client_post_address($client);
  370. }
  371. public function getEmployeeAddress($employee) {
  372. return $this->sync->get_employee_home_address($employee);
  373. }
  374. /*
  375. * operation get_payroll_calendar
  376. * @throws \XeroAPI\XeroPHP\ApiException on non-2xx response
  377. * @throws \InvalidArgumentException
  378. */
  379. public function get_payroll_calendar()
  380. {
  381. $id = "33dc7df5-3060-4d76-b4da-57c20685d77d"; //fortnightly
  382. $api = $this->get_payroll_au_instance();
  383. $calendars = $api->getPayrollCalendar($this->xeroTenantId, $id);
  384. return $calendars[0];
  385. }
  386. }