├── .gitignore ├── LICENSE ├── README.md ├── base.php ├── buildrequesturl.php ├── config.php ├── example.php ├── exchangecodefortokens.php ├── googlespreadsheet ├── api.php ├── api │ ├── parser.php │ └── parser │ │ └── simpleentry.php └── cellitem.php └── oauth2 ├── googleapi.php └── token.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.tokendata 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Peter Mescalchin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Spreadsheets PHP API 2 | PHP library allowing read/write access to existing Google Spreadsheets and their data. Uses the [version 3 API](https://developers.google.com/sheets/api/v3/), which is now on a deprecation path (as of February 2017) in favor of a version 4 API. 3 | 4 | Since this API uses [OAuth2](https://oauth.net/2/) for client authentication a *very lite* (and somewhat incomplete) set of [classes for obtaining OAuth2 tokens](oauth2) is included. 5 | 6 | - [Requires](#requires) 7 | - [Methods](#methods) 8 | - [API()](#api) 9 | - [API()->getSpreadsheetList()](#api-getspreadsheetlist) 10 | - [API()->getWorksheetList($spreadsheetKey)](#api-getworksheetlistspreadsheetkey) 11 | - [API()->getWorksheetDataList($spreadsheetKey,$worksheetID)](#api-getworksheetdatalistspreadsheetkeyworksheetid) 12 | - [API()->getWorksheetCellList($spreadsheetKey,$worksheetID[,$cellCriteriaList])](#api-getworksheetcelllistspreadsheetkeyworksheetidcellcriterialist) 13 | - [API()->updateWorksheetCellList($spreadsheetKey,$worksheetID,$worksheetCellList)](#api-updateworksheetcelllistspreadsheetkeyworksheetidworksheetcelllist) 14 | - [API()->addWorksheetDataRow($spreadsheetKey,$worksheetID,$rowDataList)](#api-addworksheetdatarowspreadsheetkeyworksheetidrowdatalist) 15 | - [Example](#example) 16 | - [Setup](#setup) 17 | - [Known issues](#known-issues) 18 | - [Reference](#reference) 19 | 20 | ## Requires 21 | - PHP 5.4 (uses [anonymous functions](http://php.net/manual/en/functions.anonymous.php) extensively). 22 | - [cURL](https://php.net/curl). 23 | - Expat [XML Parser](http://docs.php.net/manual/en/book.xml.php). 24 | 25 | ## Methods 26 | 27 | ### API() 28 | Constructor accepts an instance of `OAuth2\GoogleAPI()`, which handles OAuth2 token fetching/refreshing and generation of HTTP authorization headers used with all Google spreadsheet API calls. 29 | 30 | The included [`example.php`](example.php) provides [usage](example.php#L25-L36) [examples](base.php#L12-L22). 31 | 32 | ### API()->getSpreadsheetList() 33 | Returns a listing of available spreadsheets for the requesting client. 34 | 35 | ```php 36 | $OAuth2GoogleAPI = new OAuth2\GoogleAPI(/* URLs and client identifiers */); 37 | $OAuth2GoogleAPI->setTokenData(/* Token data */); 38 | $OAuth2GoogleAPI->setTokenRefreshHandler(/* Token refresh handler callback */); 39 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 40 | 41 | print_r( 42 | $spreadsheetAPI->getSpreadsheetList() 43 | ); 44 | 45 | /* 46 | [SPREADSHEET_KEY] => Array 47 | ( 48 | [ID] => 'https://spreadsheets.google.com/feeds/spreadsheets/private/full/...' 49 | [updated] => UNIX_TIMESTAMP 50 | [name] => 'Spreadsheet name' 51 | ) 52 | */ 53 | ``` 54 | 55 | [API reference](https://developers.google.com/sheets/api/v3/worksheets#retrieve_a_list_of_spreadsheets) 56 | 57 | ### API()->getWorksheetList($spreadsheetKey) 58 | Returns a listing of defined worksheets for a given `$spreadsheetKey`. 59 | 60 | ```php 61 | $OAuth2GoogleAPI = new OAuth2\GoogleAPI(/* URLs and client identifiers */); 62 | $OAuth2GoogleAPI->setTokenData(/* Token data */); 63 | $OAuth2GoogleAPI->setTokenRefreshHandler(/* Token refresh handler callback */); 64 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 65 | 66 | print_r( 67 | $spreadsheetAPI->getWorksheetList('SPREADSHEET_KEY') 68 | ); 69 | 70 | /* 71 | [WORKSHEET_ID] => Array 72 | ( 73 | [ID] => 'https://spreadsheets.google.com/feeds/...' 74 | [updated] => UNIX_TIMESTAMP 75 | [name] => 'Worksheet name' 76 | [columnCount] => TOTAL_COLUMNS 77 | [rowCount] => TOTAL_ROWS 78 | ) 79 | */ 80 | ``` 81 | 82 | [API reference](https://developers.google.com/sheets/api/v3/worksheets#retrieve_information_about_worksheets) 83 | 84 | ### API()->getWorksheetDataList($spreadsheetKey,$worksheetID) 85 | Returns a read only 'list based feed' of data for a given `$spreadsheetKey` and `$worksheetID`. 86 | 87 | List based feeds have a specific format as defined by Google - see the [API reference](https://developers.google.com/sheets/api/v3/data#retrieve_a_list-based_feed) for details. Data is returned as an array with two keys - defined headers and the data body. 88 | 89 | ```php 90 | $OAuth2GoogleAPI = new OAuth2\GoogleAPI(/* URLs and client identifiers */); 91 | $OAuth2GoogleAPI->setTokenData(/* Token data */); 92 | $OAuth2GoogleAPI->setTokenRefreshHandler(/* Token refresh handler callback */); 93 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 94 | 95 | print_r( 96 | $spreadsheetAPI->getWorksheetDataList('SPREADSHEET_KEY','WORKSHEET_ID') 97 | ); 98 | 99 | /* 100 | Array 101 | ( 102 | [headerList] => Array 103 | ( 104 | [0] => 'Header name #1' 105 | [1] => 'Header name #2' 106 | [x] => 'Header name #x' 107 | ) 108 | 109 | [dataList] => Array 110 | ( 111 | [0] => Array 112 | ( 113 | ['Header name #1'] => VALUE 114 | ['Header name #2'] => VALUE 115 | ['Header name #x'] => VALUE 116 | ) 117 | 118 | [1]... 119 | ) 120 | ) 121 | */ 122 | ``` 123 | 124 | [API reference](https://developers.google.com/sheets/api/v3/data#retrieve_a_list-based_feed) 125 | 126 | ### API()->getWorksheetCellList($spreadsheetKey,$worksheetID[,$cellCriteriaList]) 127 | Returns a listing of individual worksheet cells for an entire sheet, or a specific range (via `$cellCriteriaList`) for a given `$spreadsheetKey` and `$worksheetID`. 128 | 129 | - Cells returned as an array of [`GoogleSpreadsheet\CellItem()`](googlespreadsheet/cellitem.php) instances, indexed by cell reference (e.g. `B1`). 130 | - Cell instances can be modified and then passed into [`API()->updateWorksheetCellList()`](#api-updateworksheetcelllist) to update source spreadsheet. 131 | - An optional `$cellCriteriaList` boolean option of `returnEmpty` determines if method will return empty cell items. 132 | 133 | ```php 134 | $OAuth2GoogleAPI = new OAuth2\GoogleAPI(/* URLs and client identifiers */); 135 | $OAuth2GoogleAPI->setTokenData(/* Token data */); 136 | $OAuth2GoogleAPI->setTokenRefreshHandler(/* Token refresh handler callback */); 137 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 138 | 139 | // fetch first 20 rows from third column (C) to the end of the sheet 140 | // if $cellCriteria not passed then *all* cells for the spreadsheet will be returned 141 | $cellCriteria = [ 142 | 'returnEmpty' = true, 143 | 'columnStart' => 3 144 | 'rowStart' => 1 145 | 'rowEnd' => 20 146 | ]; 147 | 148 | print_r( 149 | $spreadsheetAPI->getWorksheetCellList( 150 | 'SPREADSHEET_KEY','WORKSHEET_ID', 151 | $cellCriteria 152 | ) 153 | ); 154 | 155 | /* 156 | Array 157 | ( 158 | [CELL_REFERENCE] => GoogleSpreadsheet\CellItem Object 159 | ( 160 | getRow() 161 | getColumn() 162 | getReference() 163 | getValue() 164 | setValue() 165 | isDirty() 166 | ) 167 | 168 | [CELL_REFERENCE]... 169 | ) 170 | */ 171 | ``` 172 | 173 | [API reference](https://developers.google.com/sheets/api/v3/data#retrieve_a_cell-based_feed) 174 | 175 | ### API()->updateWorksheetCellList($spreadsheetKey,$worksheetID,$worksheetCellList) 176 | Accepts an array of `GoogleSpreadsheet\CellItem()` instances as `$worksheetCellList` for a given `$spreadsheetKey` and `$worksheetID`, updating target spreadsheet where cell values have been modified from source via the [`GoogleSpreadsheet\CellItem()->setValue()`](googlespreadsheet/cellitem.php#L62-L65) method. 177 | 178 | Given cell instances that _have not_ been modified are skipped (no work to do). 179 | 180 | ```php 181 | $OAuth2GoogleAPI = new OAuth2\GoogleAPI(/* URLs and client identifiers */); 182 | $OAuth2GoogleAPI->setTokenData(/* Token data */); 183 | $OAuth2GoogleAPI->setTokenRefreshHandler(/* Token refresh handler callback */); 184 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 185 | 186 | $cellList = $spreadsheetAPI->getWorksheetCellList('SPREADSHEET_KEY','WORKSHEET_ID'); 187 | $cellList['CELL_REFERENCE']->setValue('My updated value'); 188 | 189 | $spreadsheetAPI->updateWorksheetCellList( 190 | 'SPREADSHEET_KEY','WORKSHEET_ID', 191 | $cellList 192 | ); 193 | ``` 194 | 195 | [API reference](https://developers.google.com/sheets/api/v3/data#update_multiple_cells_with_a_batch_request) 196 | 197 | ### API()->addWorksheetDataRow($spreadsheetKey,$worksheetID,$rowDataList) 198 | Add a new data row to an existing worksheet, directly after the last row. The last row is considered the final containing any non-empty cells. 199 | 200 | Accepts a single row for insert at the bottom as an array via `$rowDataList`, where each array key matches a row header. 201 | 202 | ```php 203 | $OAuth2GoogleAPI = new OAuth2\GoogleAPI(/* URLs and client identifiers */); 204 | $OAuth2GoogleAPI->setTokenData(/* Token data */); 205 | $OAuth2GoogleAPI->setTokenRefreshHandler(/* Token refresh handler callback */); 206 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 207 | 208 | $dataList = $spreadsheetAPI->getWorksheetDataList($spreadsheetKey,$worksheetID); 209 | print_r($dataList); 210 | 211 | /* 212 | Array 213 | ( 214 | [headerList] => Array 215 | ( 216 | [0] => firstname 217 | [1] => lastname 218 | [2] => jobtitle 219 | [3] => emailaddress 220 | ) 221 | 222 | [dataList] => Array 223 | ( 224 | ... existing data ... 225 | ) 226 | ) 227 | */ 228 | 229 | $spreadsheetAPI->addWorksheetDataRow($spreadsheetKey,$worksheetID,[ 230 | 'firstname' => 'Bob', 231 | 'lastname' => 'Jones', 232 | 'jobtitle' => 'UX developer', 233 | 'emailaddress' => 'bob.jones@domain.com' 234 | ]); 235 | ``` 236 | 237 | [API reference](https://developers.google.com/sheets/api/v3/data#add_a_list_row) 238 | 239 | ## Example 240 | The provided [`example.php`](example.php) CLI script will perform the following tasks: 241 | - Fetch all available spreadsheets for the requesting client and display. 242 | - For the first spreadsheet found, fetch all worksheets and display. 243 | - Fetch a data listing of the first worksheet. 244 | - Fetch a range of cells for the first worksheet. 245 | - Finally, modify the content of the first cell fetched (commented out in example). 246 | 247 | ### Setup 248 | - Create a new project at: https://console.developers.google.com/projectcreate. 249 | - Generate set of OAuth2 tokens via `API Manager -> Credentials`: 250 | - Click `Create credentials` drop down. 251 | - Select `OAuth client ID` then `Web application`. 252 | - Enter friendly name for client ID. 253 | - Enter an `Authorized redirect URI` - this *does not* need to be a real URI for the example. 254 | - Note down both generated `client ID` and `client secret` values. 255 | - Modify [`config.php`](config.php) entering `redirect` URI, `clientID` and `clientSecret` values generated above. 256 | - Visit the [Allow Risky Access Permissions By Unreviewed Apps](https://groups.google.com/forum/#!forum/risky-access-by-unreviewed-apps) Google group and `Join group` using your Google account. 257 | - **Note:** For long term access it would be recommended to submit a "OAuth Developer Verification" request to Google. 258 | - Execute [`buildrequesturl.php`](buildrequesturl.php) and visit generated URL in a browser. 259 | - After accepting access terms and taken back to redirect URI, note down the `?code=` query string value (minus the trailing `#` character). 260 | - Execute [`exchangecodefortokens.php`](exchangecodefortokens.php), providing `code` from the previous step. This step should be called within a short time window before `code` expires. 261 | - Received OAuth2 token credentials will be saved to `./.tokendata`. 262 | - **Note:** In a production application this sensitive information should be saved in a secure form to datastore/database/etc. 263 | 264 | Finally, run `example.php` to view the result. 265 | 266 | **Note:** If OAuth2 token details stored in `./.tokendata` require a refresh (due to expiry), the function handler set by [`OAuth2\GoogleAPI->setTokenRefreshHandler()`](oauth2/googleapi.php#L36-L39) will be called to allow the re-save of updated token data back to persistent storage. 267 | 268 | ## Known issues 269 | The Google spreadsheet API documents suggest requests can [specify the API version](https://developers.google.com/sheets/api/v3/authorize#specify_a_version). Attempts to do this cause the [cell based feed](https://developers.google.com/sheets/api/v3/data#retrieve_a_cell-based_feed) response to avoid providing the cell version slug in `` nodes - making it impossible to issue an update of cell values. So for now, I have left out sending the API version HTTP header. 270 | 271 | ## Reference 272 | - OAuth2 273 | - https://tools.ietf.org/html/rfc6749 274 | - https://developers.google.com/accounts/docs/OAuth2WebServer 275 | - https://developers.google.com/oauthplayground/ 276 | - Google Spreadsheets API version 3.0 277 | - https://developers.google.com/sheets/api/v3/ 278 | -------------------------------------------------------------------------------- /base.php: -------------------------------------------------------------------------------- 1 | config = $config; 10 | } 11 | 12 | protected function getOAuth2GoogleAPIInstance() { 13 | 14 | $OAuth2URLList = $this->config['OAuth2URL']; 15 | 16 | return new OAuth2\GoogleAPI( 17 | sprintf('%s/%s',$OAuth2URLList['base'],$OAuth2URLList['token']), 18 | $OAuth2URLList['redirect'], 19 | $this->config['clientID'], 20 | $this->config['clientSecret'] 21 | ); 22 | } 23 | 24 | protected function saveOAuth2TokenData(array $data) { 25 | 26 | file_put_contents( 27 | $this->config['tokenDataFile'], 28 | serialize($data) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /buildrequesturl.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | buildURL([ 17 | GoogleSpreadsheet\API::API_BASE_URL 18 | ]) 19 | )); 20 | } 21 | 22 | private function buildURL(array $scopeList) { 23 | 24 | $OAuth2URLList = $this->config['OAuth2URL']; 25 | 26 | // ensure all scopes have trailing forward slash 27 | foreach ($scopeList as &$scopeItem) { 28 | $scopeItem = rtrim($scopeItem,'/') . '/'; 29 | } 30 | 31 | $buildQuerystring = function(array $list) { 32 | 33 | $querystringList = []; 34 | foreach ($list as $key => $value) { 35 | $querystringList[] = rawurlencode($key) . '=' . rawurlencode($value); 36 | } 37 | 38 | return implode('&',$querystringList); 39 | }; 40 | 41 | return sprintf( 42 | "%s/%s?%s\n\n", 43 | $OAuth2URLList['base'],$OAuth2URLList['auth'], 44 | $buildQuerystring([ 45 | 'access_type' => 'offline', 46 | 'approval_prompt' => 'force', 47 | 'client_id' => $this->config['clientID'], 48 | 'redirect_uri' => $OAuth2URLList['redirect'], 49 | 'response_type' => 'code', 50 | 'scope' => implode(' ',$scopeList) 51 | ]) 52 | ); 53 | } 54 | } 55 | 56 | 57 | (new BuildRequestURL(require('config.php')))->execute(); 58 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'base' => 'https://accounts.google.com/o/oauth2', 5 | 'auth' => 'auth', // for Google authorization 6 | 'token' => 'token', // for OAuth2 token actions 7 | 'redirect' => 'https://domain.com/oauth' 8 | ], 9 | 10 | 'clientID' => '', 11 | 'clientSecret' => '', 12 | 'tokenDataFile' => '.tokendata' 13 | ]; 14 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | loadOAuth2TokenData()) === false) { 19 | return; 20 | } 21 | 22 | // setup Google OAuth2 handler 23 | $OAuth2GoogleAPI = $this->getOAuth2GoogleAPIInstance(); 24 | 25 | $OAuth2GoogleAPI->setTokenData( 26 | $tokenData['accessToken'], 27 | $tokenData['tokenType'], 28 | $tokenData['expiresAt'], 29 | $tokenData['refreshToken'] 30 | ); 31 | 32 | $OAuth2GoogleAPI->setTokenRefreshHandler(function(array $tokenData) { 33 | 34 | // save updated OAuth2 token data back to file 35 | $this->saveOAuth2TokenData($tokenData); 36 | }); 37 | 38 | $spreadsheetAPI = new GoogleSpreadsheet\API($OAuth2GoogleAPI); 39 | 40 | // fetch all available spreadsheets and display 41 | $spreadsheetList = $spreadsheetAPI->getSpreadsheetList(); 42 | print_r($spreadsheetList); 43 | 44 | if (!$spreadsheetList) { 45 | echo("Error: No spreadsheets found\n"); 46 | exit(); 47 | } 48 | 49 | // fetch key of first spreadsheet 50 | $spreadsheetKey = array_keys($spreadsheetList)[0]; 51 | 52 | // fetch all worksheets and display 53 | $worksheetList = $spreadsheetAPI->getWorksheetList($spreadsheetKey); 54 | print_r($worksheetList); 55 | 56 | // fetch ID of first worksheet from list 57 | $worksheetID = array_keys($worksheetList)[0]; 58 | 59 | // fetch worksheet data list and display 60 | print_r($spreadsheetAPI->getWorksheetDataList( 61 | $spreadsheetKey, 62 | $worksheetID 63 | )); 64 | 65 | // fetch worksheet cell list and display 66 | $cellList = $spreadsheetAPI->getWorksheetCellList( 67 | $spreadsheetKey, 68 | $worksheetID,[ 69 | 'columnStart' => 2, 70 | 'columnEnd' => 10 71 | ] 72 | ); 73 | 74 | print_r($cellList); 75 | 76 | // update content of first cell 77 | /* 78 | $cellIndex = array_keys($cellList)[0]; 79 | $cellList[$cellIndex]->setValue( 80 | $cellList[$cellIndex]->getValue() . ' updated' 81 | ); 82 | 83 | $spreadsheetAPI->updateWorksheetCellList( 84 | $spreadsheetKey, 85 | $worksheetID, 86 | $cellList 87 | ); 88 | */ 89 | 90 | // add data row to worksheet 91 | /* 92 | $dataList = $spreadsheetAPI->getWorksheetDataList($spreadsheetKey,$worksheetID); 93 | 94 | $rowData = []; 95 | foreach ($dataList['headerList'] as $index => $headerName) { 96 | $rowData[$headerName] = 'column: ' . $index; 97 | } 98 | 99 | $spreadsheetAPI->addWorksheetDataRow($spreadsheetKey,$worksheetID,$rowData); 100 | */ 101 | } 102 | 103 | private function loadOAuth2TokenData() { 104 | 105 | $tokenDataFile = $this->config['tokenDataFile']; 106 | 107 | if (!is_file($tokenDataFile)) { 108 | echo(sprintf( 109 | "Error: unable to locate token file [%s]\n", 110 | $tokenDataFile 111 | )); 112 | 113 | return false; 114 | } 115 | 116 | // load file, return data as PHP array 117 | return unserialize(file_get_contents($tokenDataFile)); 118 | } 119 | } 120 | 121 | 122 | (new Example(require('config.php')))->execute(); 123 | -------------------------------------------------------------------------------- /exchangecodefortokens.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getAuthCodeFromCLI()) === false) { 14 | return; 15 | } 16 | 17 | $OAuth2GoogleAPI = $this->getOAuth2GoogleAPIInstance(); 18 | 19 | // make request for OAuth2 tokens 20 | echo(sprintf( 21 | "Requesting OAuth2 tokens via authorization code: %s\n", 22 | $authCode 23 | )); 24 | 25 | try { 26 | $tokenData = $OAuth2GoogleAPI->getAccessTokenFromAuthCode($authCode); 27 | 28 | // save token data to disk 29 | echo(sprintf( 30 | "Success! Saving token data to [%s]\n", 31 | $this->config['tokenDataFile'] 32 | )); 33 | 34 | $this->saveOAuth2TokenData($tokenData); 35 | // all done 36 | 37 | } catch (Exception $e) { 38 | // token fetch error 39 | echo(sprintf("Error: %s\n",$e->getMessage())); 40 | } 41 | } 42 | 43 | private function getAuthCodeFromCLI() { 44 | 45 | // attempt to get code from CLI 46 | if (!($optList = getopt('c:'))) { 47 | // no -c switch or switch without value 48 | echo(sprintf("Usage: %s -c AUTHORIZATION_CODE\n",basename(__FILE__))); 49 | return false; 50 | } 51 | 52 | // validate authorization code format 53 | $authCode = $optList['c']; 54 | if (!preg_match('/^[0-9A-Za-z._\/-]{30,62}$/',$authCode)) { 55 | echo("Error: invalid authorization code format\n\n"); 56 | return false; 57 | } 58 | 59 | // all valid text wise 60 | return $authCode; 61 | } 62 | } 63 | 64 | 65 | (new ExchangeCodeForTokens(require('config.php')))->execute(); 66 | -------------------------------------------------------------------------------- /googlespreadsheet/api.php: -------------------------------------------------------------------------------- 1 | 'max-col', 22 | 'columnStart' => 'min-col', 23 | 'returnEmpty' => 'return-empty', 24 | 'rowEnd' => 'max-row', 25 | 'rowStart' => 'min-row' 26 | ]; 27 | 28 | private $OAuth2GoogleAPI; 29 | 30 | 31 | public function __construct(\OAuth2\GoogleAPI $OAuth2GoogleAPI) { 32 | 33 | $this->OAuth2GoogleAPI = $OAuth2GoogleAPI; 34 | } 35 | 36 | public function getSpreadsheetList() { 37 | 38 | // init XML parser 39 | $parser = new API\Parser\SimpleEntry('/\/(?P[a-zA-Z0-9-_]+)$/'); 40 | $hasResponseData = false; 41 | 42 | // make request 43 | list($responseHTTPCode,$responseBody) = $this->OAuth2Request( 44 | self::API_BASE_URL . '/spreadsheets/private/full', 45 | null, 46 | function($data) use ($parser,&$hasResponseData) { 47 | 48 | $parser->process($data); 49 | $hasResponseData = true; 50 | } 51 | ); 52 | 53 | // end of XML parse 54 | $parser->close(); 55 | 56 | // HTTP code always seems to be 200 - so check for empty response body when in error 57 | if (!$hasResponseData) { 58 | throw new \Exception('Unable to retrieve spreadsheet listing'); 59 | } 60 | 61 | return $parser->getList(); 62 | } 63 | 64 | public function getWorksheetList($spreadsheetKey) { 65 | 66 | // init XML parser 67 | $parser = new API\Parser\SimpleEntry( 68 | '/\/(?P[a-z0-9]+)$/',[ 69 | 'FEED/ENTRY/GS:COLCOUNT' => 'columnCount', 70 | 'FEED/ENTRY/GS:ROWCOUNT' => 'rowCount' 71 | ] 72 | ); 73 | 74 | // make request 75 | list($responseHTTPCode,$responseBody) = $this->OAuth2Request( 76 | sprintf( 77 | '%s/worksheets/%s/private/full', 78 | self::API_BASE_URL, 79 | $spreadsheetKey 80 | ), 81 | null, 82 | function($data) use ($parser) { $parser->process($data); } 83 | ); 84 | 85 | // end of XML parse 86 | $parser->close(); 87 | 88 | $this->checkAPIResponseError( 89 | $responseHTTPCode,$responseBody, 90 | 'Unable to retrieve worksheet listing' 91 | ); 92 | 93 | return $parser->getList(); 94 | } 95 | 96 | public function getWorksheetDataList($spreadsheetKey,$worksheetID) { 97 | 98 | // supporting code for XML parse 99 | $worksheetHeaderList = []; 100 | $worksheetDataList = []; 101 | $dataItem = []; 102 | $addDataItem = function(array $dataItem) use (&$worksheetHeaderList,&$worksheetDataList) { 103 | 104 | if ($dataItem) { 105 | // add headers found to complete header list 106 | foreach ($dataItem as $headerName => $void) { 107 | $worksheetHeaderList[$headerName] = true; 108 | } 109 | 110 | // add list item to collection 111 | $worksheetDataList[] = $dataItem; 112 | } 113 | }; 114 | 115 | // init XML parser 116 | $parser = new API\Parser( 117 | function($name,$elementPath) use ($addDataItem,&$dataItem) { 118 | 119 | if ($elementPath == 'FEED/ENTRY') { 120 | // store last data row and start new row 121 | $addDataItem($dataItem); 122 | $dataItem = []; 123 | } 124 | }, 125 | function($elementPath,$data) use (&$dataItem) { 126 | 127 | // looking for a header element type 128 | if (preg_match('/^FEED\/ENTRY\/GSX:(?P[^\/]+)$/',$elementPath,$match)) { 129 | $dataItem[strtolower($match['name'])] = trim($data); 130 | } 131 | } 132 | ); 133 | 134 | // make request 135 | list($responseHTTPCode,$responseBody) = $this->OAuth2Request( 136 | sprintf( 137 | '%s/list/%s/%s/private/full', 138 | self::API_BASE_URL, 139 | $spreadsheetKey, 140 | $worksheetID 141 | ), 142 | null, 143 | function($data) use ($parser) { $parser->process($data); } 144 | ); 145 | 146 | // end of XML parse - add final parsed data row 147 | $parser->close(); 148 | $addDataItem($dataItem); 149 | 150 | $this->checkAPIResponseError( 151 | $responseHTTPCode,$responseBody, 152 | 'Unable to retrieve worksheet data listing' 153 | ); 154 | 155 | // return header and data lists 156 | return [ 157 | 'headerList' => array_keys($worksheetHeaderList), 158 | 'dataList' => $worksheetDataList 159 | ]; 160 | } 161 | 162 | public function getWorksheetCellList($spreadsheetKey,$worksheetID,array $cellCriteriaList = []) { 163 | 164 | // build cell fetch range criteria for URL if given 165 | $cellRangeCriteriaQuerystringList = []; 166 | if ($cellCriteriaList) { 167 | // ensure all given keys are valid 168 | if ($invalidCriteriaList = array_diff( 169 | array_keys($cellCriteriaList), 170 | array_keys($this->RANGE_CRITERIA_MAP_COLLECTION) 171 | )) { 172 | // invalid keys found 173 | throw new \Exception('Invalid cell range criteria key(s) [' . implode(',',$invalidCriteriaList) . ']'); 174 | } 175 | 176 | // all valid, build querystring 177 | foreach ($this->RANGE_CRITERIA_MAP_COLLECTION as $key => $mapTo) { 178 | if (isset($cellCriteriaList[$key])) { 179 | $value = $cellCriteriaList[$key]; 180 | $cellRangeCriteriaQuerystringList[] = ($key == 'returnEmpty') 181 | ? sprintf('%s=%s',$mapTo,($value) ? 'true' : 'false') 182 | : sprintf('%s=%d',$mapTo,$value); 183 | } 184 | } 185 | } 186 | 187 | // supporting code for XML parse 188 | $worksheetCellList = []; 189 | $cellItemData = []; 190 | $addCellItem = function(array $cellItemData) use (&$worksheetCellList) { 191 | 192 | if (isset( 193 | $cellItemData['ref'], 194 | $cellItemData['value'], 195 | $cellItemData['URL'] 196 | )) { 197 | // add cell item instance to list 198 | $cellReference = strtoupper($cellItemData['ref']); 199 | 200 | $worksheetCellList[$cellReference] = new CellItem( 201 | $cellItemData['URL'], 202 | $cellReference, 203 | $cellItemData['value'] 204 | ); 205 | } 206 | }; 207 | 208 | // init XML parser 209 | $parser = new API\Parser( 210 | function($name,$elementPath,array $attribList) use ($addCellItem,&$cellItemData) { 211 | 212 | switch ($elementPath) { 213 | case 'FEED/ENTRY': 214 | // store last data row and start new row 215 | $addCellItem($cellItemData); 216 | $cellItemData = []; 217 | break; 218 | 219 | case 'FEED/ENTRY/LINK': 220 | if ( 221 | (isset($attribList['REL'],$attribList['HREF'])) && 222 | ($attribList['REL'] == 'edit') 223 | ) { 224 | // store versioned cell url 225 | $cellItemData['URL'] = $attribList['HREF']; 226 | } 227 | 228 | break; 229 | } 230 | }, 231 | function($elementPath,$data) use (&$cellItemData) { 232 | 233 | switch ($elementPath) { 234 | case 'FEED/ENTRY/TITLE': 235 | $cellItemData['ref'] = $data; // cell reference (e.g. 'B1') 236 | break; 237 | 238 | case 'FEED/ENTRY/CONTENT': 239 | $cellItemData['value'] = $data; // cell value 240 | break; 241 | } 242 | } 243 | ); 244 | 245 | // make request 246 | list($responseHTTPCode,$responseBody) = $this->OAuth2Request( 247 | sprintf( 248 | '%s/cells/%s/%s/private/full%s', 249 | self::API_BASE_URL, 250 | $spreadsheetKey, 251 | $worksheetID, 252 | ($cellRangeCriteriaQuerystringList) 253 | ? '?' . implode('&',$cellRangeCriteriaQuerystringList) 254 | : '' 255 | ), 256 | null, 257 | function($data) use ($parser) { $parser->process($data); } 258 | ); 259 | 260 | // end of XML parse - add final cell item 261 | $parser->close(); 262 | $addCellItem($cellItemData); 263 | 264 | $this->checkAPIResponseError( 265 | $responseHTTPCode,$responseBody, 266 | 'Unable to retrieve worksheet cell listing' 267 | ); 268 | 269 | // return cell list 270 | return $worksheetCellList; 271 | } 272 | 273 | public function updateWorksheetCellList($spreadsheetKey,$worksheetID,array $worksheetCellList) { 274 | 275 | // scan cell list - at least one cell must be in 'dirty' state 276 | $hasDirty = false; 277 | foreach ($worksheetCellList as $cellItem) { 278 | if ($cellItem->isDirty()) { 279 | $hasDirty = true; 280 | break; 281 | } 282 | } 283 | 284 | if (!$hasDirty) { 285 | // no work to do 286 | return false; 287 | } 288 | 289 | // make request 290 | $cellIDIndex = -1; 291 | $excessBuffer = false; 292 | $finalCellSent = false; 293 | 294 | list($responseHTTPCode,$responseBody) = $this->OAuth2Request( 295 | sprintf( 296 | '%s/cells/%s/%s/private/full/batch', 297 | self::API_BASE_URL, 298 | $spreadsheetKey, 299 | $worksheetID 300 | ), 301 | function($bytesWriteMax) 302 | use ( 303 | $spreadsheetKey,$worksheetID, 304 | &$worksheetCellList,&$cellIDIndex,&$excessBuffer,&$finalCellSent 305 | ) { 306 | 307 | if ($finalCellSent) { 308 | // end of data 309 | return ''; 310 | } 311 | 312 | if ($excessBuffer !== false) { 313 | // send more buffer from previous run 314 | list($writeBuffer,$excessBuffer) = $this->splitBuffer($bytesWriteMax,$excessBuffer); 315 | return $writeBuffer; 316 | } 317 | 318 | if ($cellIDIndex < 0) { 319 | // emit XML header 320 | $cellIDIndex = 0; 321 | 322 | return sprintf( 323 | '' . 326 | '%s/cells/%s/%s/private/full', 327 | self::XMLNS_ATOM, 328 | self::XMLNS_GOOGLE_SPREADSHEET, 329 | self::API_BASE_URL, 330 | $spreadsheetKey,$worksheetID 331 | ); 332 | } 333 | 334 | // find next cell update to send 335 | $cellItem = false; 336 | while ($worksheetCellList) { 337 | $cellItem = array_shift($worksheetCellList); 338 | if ($cellItem->isDirty()) { 339 | // found cell to be updated 340 | break; 341 | } 342 | 343 | $cellItem = false; 344 | } 345 | 346 | if ($cellItem === false) { 347 | // no more cells 348 | $finalCellSent = true; 349 | return ''; 350 | } 351 | 352 | $cellIDIndex++; 353 | list($writeBuffer,$excessBuffer) = $this->splitBuffer( 354 | $bytesWriteMax, 355 | $this->updateWorksheetCellListBuildBatchUpdateEntry( 356 | $spreadsheetKey,$worksheetID, 357 | $cellIDIndex,$cellItem 358 | ) 359 | ); 360 | 361 | // send write buffer 362 | return $writeBuffer; 363 | } 364 | ); 365 | 366 | $this->checkAPIResponseError( 367 | $responseHTTPCode,$responseBody, 368 | 'Unable to update worksheet cell(s)' 369 | ); 370 | 371 | // all done 372 | return true; 373 | } 374 | 375 | public function addWorksheetDataRow($spreadsheetKey,$worksheetID,array $rowDataList) { 376 | 377 | $rowHeaderNameList = array_keys($rowDataList); 378 | $rowDataIndex = -1; 379 | $excessBuffer = false; 380 | $finalRowDataSent = false; 381 | 382 | list($responseHTTPCode,$responseBody) = $this->OAuth2Request( 383 | sprintf( 384 | '%s/list/%s/%s/private/full', 385 | self::API_BASE_URL, 386 | $spreadsheetKey, 387 | $worksheetID 388 | ), 389 | function($bytesWriteMax) 390 | use ( 391 | $spreadsheetKey,$worksheetID, 392 | $rowDataList,$rowHeaderNameList, 393 | &$rowDataIndex,&$excessBuffer,&$finalRowDataSent 394 | ) { 395 | 396 | if ($finalRowDataSent) { 397 | // end of data 398 | return ''; 399 | } 400 | 401 | if ($excessBuffer !== false) { 402 | // send more buffer from previous run 403 | list($writeBuffer,$excessBuffer) = $this->splitBuffer($bytesWriteMax,$excessBuffer); 404 | return $writeBuffer; 405 | } 406 | 407 | if ($rowDataIndex < 0) { 408 | // emit XML header 409 | $rowDataIndex = 0; 410 | 411 | return sprintf( 412 | '', 413 | self::XMLNS_ATOM, 414 | self::XMLNS_GOOGLE_SPREADSHEET 415 | ); 416 | } 417 | 418 | if ($rowDataIndex >= count($rowHeaderNameList)) { 419 | // no more row column data 420 | $finalRowDataSent = true; 421 | return ''; 422 | } 423 | 424 | $headerName = $rowHeaderNameList[$rowDataIndex]; 425 | list($writeBuffer,$excessBuffer) = $this->splitBuffer( 426 | $bytesWriteMax, 427 | sprintf( 428 | '%2$s', 429 | $headerName, 430 | htmlspecialchars($rowDataList[$headerName]) 431 | ) 432 | ); 433 | 434 | $rowDataIndex++; 435 | 436 | // send write buffer 437 | return $writeBuffer; 438 | } 439 | ); 440 | 441 | $this->checkAPIResponseError( 442 | $responseHTTPCode,$responseBody, 443 | 'Unable to add worksheet data row' 444 | ); 445 | } 446 | 447 | private function updateWorksheetCellListBuildBatchUpdateEntry( 448 | $spreadsheetKey,$worksheetID, 449 | $cellBatchID,CellItem $cellItem 450 | ) { 451 | 452 | $cellBaseURL = sprintf( 453 | '%s/cells/%s/%s/private/full/R%dC%d', 454 | self::API_BASE_URL, 455 | $spreadsheetKey,$worksheetID, 456 | $cellItem->getRow(), 457 | $cellItem->getColumn() 458 | ); 459 | 460 | return sprintf( 461 | '' . 462 | 'batchItem%d' . 463 | '' . 464 | '%s' . 465 | '' . 466 | '' . 467 | '', 468 | $cellBatchID, 469 | $cellBaseURL, 470 | self::CONTENT_TYPE_ATOMXML, 471 | $cellBaseURL,$cellItem->getVersion(), 472 | $cellItem->getRow(),$cellItem->getColumn(), 473 | htmlspecialchars($cellItem->getValue()) 474 | ); 475 | } 476 | 477 | private function OAuth2Request( 478 | $URL, 479 | callable $writeHandler = null, 480 | callable $readHandler = null 481 | ) { 482 | 483 | $responseHTTPCode = false; 484 | $responseBody = ''; 485 | 486 | // build option list 487 | $optionList = [ 488 | CURLOPT_BUFFERSIZE => self::CURL_BUFFER_SIZE, 489 | CURLOPT_HEADER => false, 490 | CURLOPT_HTTPHEADER => [ 491 | 'Accept: ', 492 | 'Expect: ', // added by CURLOPT_READFUNCTION 493 | 494 | // Google OAuth2 credentials 495 | implode(': ',$this->OAuth2GoogleAPI->getAuthHTTPHeader()) 496 | ], 497 | CURLOPT_RETURNTRANSFER => ($readHandler === null), // only return response from curl_exec() directly if no $readHandler given 498 | CURLOPT_URL => $URL 499 | ]; 500 | 501 | // add optional write/read data handlers 502 | if ($writeHandler !== null) { 503 | // POST data with XML content type if using a write handler 504 | $optionList += [ 505 | CURLOPT_CUSTOMREQUEST => 'POST', 506 | CURLOPT_PUT => true, // required to enable CURLOPT_READFUNCTION 507 | CURLOPT_READFUNCTION => 508 | // don't need curl instance/stream resource - so proxy handler in closure to remove 509 | function($curlConn,$stream,$bytesWriteMax) use ($writeHandler) { 510 | return $writeHandler($bytesWriteMax); 511 | } 512 | ]; 513 | 514 | $optionList[CURLOPT_HTTPHEADER][] = 'Content-Type: ' . self::CONTENT_TYPE_ATOMXML; 515 | } 516 | 517 | if ($readHandler !== null) { 518 | $optionList[CURLOPT_WRITEFUNCTION] = 519 | // proxy so we can capture HTTP response code before using given write handler 520 | function($curlConn,$data) use ($readHandler,&$responseHTTPCode,&$responseBody) { 521 | 522 | // fetch HTTP response code if not known yet 523 | if ($responseHTTPCode === false) { 524 | $responseHTTPCode = curl_getinfo($curlConn,CURLINFO_HTTP_CODE); 525 | } 526 | 527 | if ($responseHTTPCode == self::HTTP_CODE_OK) { 528 | // call handler 529 | $readHandler($data); 530 | 531 | } else { 532 | // bad response - put all response data into $responseBody 533 | $responseBody .= $data; 534 | } 535 | 536 | // return the byte count/size processed back to curl 537 | return strlen($data); 538 | }; 539 | } 540 | 541 | $curlConn = curl_init(); 542 | curl_setopt_array($curlConn,$optionList); 543 | 544 | // make request, close curl session 545 | // mute curl warnings that could fire from read/write handlers that throw exceptions 546 | set_error_handler(function() {},E_WARNING); 547 | $curlExecReturn = curl_exec($curlConn); 548 | restore_error_handler(); 549 | 550 | if ($responseHTTPCode === false) { 551 | $responseHTTPCode = curl_getinfo($curlConn,CURLINFO_HTTP_CODE); 552 | } 553 | 554 | curl_close($curlConn); 555 | 556 | // return HTTP code and response body 557 | return [ 558 | $responseHTTPCode, 559 | ($readHandler === null) ? $curlExecReturn : $responseBody 560 | ]; 561 | } 562 | 563 | private function splitBuffer($bytesWriteMax,$buffer) { 564 | 565 | if (strlen($buffer) > $bytesWriteMax) { 566 | // split buffer at max write bytes and remainder 567 | return [ 568 | substr($buffer,0,$bytesWriteMax), 569 | substr($buffer,$bytesWriteMax) 570 | ]; 571 | } 572 | 573 | // can send the full buffer at once 574 | return [$buffer,false]; 575 | } 576 | 577 | private function checkAPIResponseError($HTTPCode,$body,$errorMessage) { 578 | 579 | if ( 580 | ($HTTPCode != self::HTTP_CODE_OK) && 581 | ($HTTPCode != self::HTTP_CODE_CREATED) 582 | ) { 583 | // error with API call - throw error with returned message 584 | $body = trim(htmlspecialchars_decode($body,ENT_QUOTES)); 585 | 586 | throw new \Exception( 587 | $errorMessage . 588 | (($body != '') ? ' - ' . $body : '') 589 | ); 590 | } 591 | 592 | // all good 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /googlespreadsheet/api/parser.php: -------------------------------------------------------------------------------- 1 | XMLParser = xml_parser_create(); 15 | $elementPathList = []; 16 | $elementPath = ''; 17 | $elementData = false; 18 | 19 | // setup element start/end handlers 20 | xml_set_element_handler( 21 | $this->XMLParser, 22 | function($parser,$name,array $attribList) 23 | use ($elementStartHandler,&$elementPathList,&$elementPath,&$elementData) { 24 | 25 | // update element path (level down), start catching element data 26 | $elementPathList[] = $name; 27 | $elementPath = implode('/',$elementPathList); 28 | $elementData = ''; 29 | 30 | // call $elementStartHandler with open element details 31 | $elementStartHandler($name,$elementPath,$attribList); 32 | }, 33 | function($parser,$name) 34 | use ($dataHandler,&$elementPathList,&$elementPath,&$elementData) { 35 | 36 | if ($elementData !== false) { 37 | // call $dataHandler with element path and data 38 | $dataHandler($elementPath,$elementData); 39 | } 40 | 41 | // update element path (level up), stop catching element data 42 | array_pop($elementPathList); 43 | $elementPath = implode('/',$elementPathList); 44 | $elementData = false; 45 | } 46 | ); 47 | 48 | // setup element data handler 49 | xml_set_character_data_handler( 50 | $this->XMLParser, 51 | function($parser,$data) use (&$elementData) { 52 | 53 | // the function here can be called multiple times for a single open element if linefeeds are found 54 | if ($elementData !== false) { 55 | $elementData .= $data; 56 | } 57 | } 58 | ); 59 | } 60 | 61 | public function process($data) { 62 | 63 | if (!xml_parse( 64 | $this->XMLParser, 65 | ($data === true) ? '' : $data, // ($data === true) to signify final chunk of XML 66 | ($data === true) 67 | )) { 68 | // throw XML parse exception 69 | throw new \Exception( 70 | 'XML parse error: ' . 71 | xml_error_string(xml_get_error_code($this->XMLParser)) 72 | ); 73 | } 74 | 75 | $this->XMLParsedChunk = true; 76 | } 77 | 78 | public function close() { 79 | 80 | if ($this->XMLParsedChunk) { 81 | // used to signify the final chunk of XML to be parsed 82 | $this->process(true); 83 | } 84 | 85 | xml_parser_free($this->XMLParser); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /googlespreadsheet/api/parser/simpleentry.php: -------------------------------------------------------------------------------- 1 | indexRegexp = $indexRegexp; 20 | $this->additionalElementSaveList = $additionalElementSaveList; 21 | 22 | // init XML parser 23 | parent::__construct( 24 | function($name,$elementPath) { 25 | 26 | if ($elementPath == 'FEED/ENTRY') { 27 | // store last entry and start next 28 | $this->addItem($this->entryItem); 29 | $this->entryItem = []; 30 | } 31 | }, 32 | function($elementPath,$data) { 33 | 34 | switch ($elementPath) { 35 | case 'FEED/ENTRY/ID': 36 | $this->entryItem['ID'] = $data; 37 | break; 38 | 39 | case 'FEED/ENTRY/UPDATED': 40 | $this->entryItem['updated'] = strtotime($data); 41 | break; 42 | 43 | case 'FEED/ENTRY/TITLE': 44 | $this->entryItem['name'] = $data; 45 | break; 46 | 47 | default: 48 | // additional elements to save 49 | if ( 50 | $this->additionalElementSaveList && 51 | (isset($this->additionalElementSaveList[$elementPath])) 52 | ) { 53 | // found one - add to stack 54 | $this->entryItem[$this->additionalElementSaveList[$elementPath]] = $data; 55 | } 56 | } 57 | } 58 | ); 59 | } 60 | 61 | public function getList() { 62 | 63 | // add final parsed entry and return list 64 | $this->addItem($this->entryItem); 65 | return $this->entryList; 66 | } 67 | 68 | private function addItem(array $entryItem) { 69 | 70 | if (!isset( 71 | $entryItem['ID'], 72 | $entryItem['updated'], 73 | $entryItem['name'] 74 | )) { 75 | // required entry properties not found 76 | return; 77 | } 78 | 79 | // if additional element save critera - ensure they were found for entry 80 | $saveEntryOK = true; 81 | if ($this->additionalElementSaveList) { 82 | foreach ($this->additionalElementSaveList as $entryKey) { 83 | if (!isset($entryItem[$entryKey])) { 84 | // not found - skip entry 85 | $saveEntryOK = false; 86 | break; 87 | } 88 | } 89 | } 90 | 91 | // extract the entry index from the ID to use as array index 92 | if ( 93 | $saveEntryOK && 94 | preg_match($this->indexRegexp,$entryItem['ID'],$match) 95 | ) { 96 | $this->entryList[$match['index']] = $entryItem; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /googlespreadsheet/cellitem.php: -------------------------------------------------------------------------------- 1 | [0-9]+)C(?P[0-9]+)\/(?P[a-z0-9]+)$/', 20 | $URL,$matchList 21 | )) { 22 | // invalid Google spreadsheet cell URL format 23 | throw new \Exception('Invalid spreadsheet cell item URL format'); 24 | return; 25 | } 26 | 27 | // save data items from URL 28 | $this->cellRow = $matchList['row']; 29 | $this->cellColumn = $matchList['column']; 30 | $this->cellVersion = $matchList['version']; 31 | 32 | // save cell reference (e.g. 'B1') and current cell value 33 | $this->cellReference = $cellReference; 34 | $this->valueInitial = $this->value = $value; 35 | } 36 | 37 | public function getRow() { 38 | 39 | return $this->cellRow; 40 | } 41 | 42 | public function getColumn() { 43 | 44 | return $this->cellColumn; 45 | } 46 | 47 | public function getVersion() { 48 | 49 | return $this->cellVersion; 50 | } 51 | 52 | public function getReference() { 53 | 54 | return $this->cellReference; 55 | } 56 | 57 | public function getValue() { 58 | 59 | return $this->value; 60 | } 61 | 62 | public function setValue($value) { 63 | 64 | $this->value = $value; 65 | } 66 | 67 | public function isDirty() { 68 | 69 | return ($this->value !== $this->valueInitial); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /oauth2/googleapi.php: -------------------------------------------------------------------------------- 1 | clientID = $clientID; 25 | $this->clientSecret = $clientSecret; 26 | } 27 | 28 | public function setTokenData($access,$type,$expiresAt,$refresh = false) { 29 | 30 | $this->accessToken = $access; 31 | $this->tokenType = $type; 32 | $this->expiresAt = intval($expiresAt); 33 | $this->refreshToken = $refresh; 34 | } 35 | 36 | public function setTokenRefreshHandler(callable $handler) { 37 | 38 | $this->refreshTokenHandler = $handler; 39 | } 40 | 41 | public function getAuthHTTPHeader() { 42 | 43 | // ensure we have the right bits of OAuth2 data available/previously set 44 | if ( 45 | ($this->accessToken === false) || 46 | ($this->tokenType === false) || 47 | ($this->expiresAt === false) 48 | ) { 49 | // missing data 50 | throw new \Exception ('Unable to build header - missing OAuth2 token information'); 51 | } 52 | 53 | if (time() >= ($this->expiresAt - self::OAUTH2_TOKEN_EXPIRY_WINDOW)) { 54 | // token is considered expired 55 | if ($this->refreshToken === false) { 56 | // we don't have a refresh token - can't build OAuth2 HTTP header 57 | return false; 58 | } 59 | 60 | // get new access token (will be stored in $this->accessToken) 61 | $tokenData = $this->getAccessTokenFromRefreshToken($this->refreshToken); 62 | 63 | // if callback handler defined for token refresh events call it now 64 | if ($this->refreshTokenHandler !== null) { 65 | // include the refresh token in call to handler 66 | $handler = $this->refreshTokenHandler; 67 | $handler($tokenData + ['refreshToken' => $this->refreshToken]); 68 | } 69 | } 70 | 71 | // return OAuth2 HTTP header as a name/value pair 72 | return [ 73 | 'Authorization', 74 | sprintf('%s %s',$this->tokenType,$this->accessToken) 75 | ]; 76 | } 77 | 78 | public function getAccessTokenFromAuthCode($code) { 79 | 80 | return $this->storeTokenData( 81 | parent::requestAccessTokenFromAuthCode( 82 | $code, 83 | $this->getAuthCredentialList() 84 | ), 85 | true 86 | ); 87 | } 88 | 89 | public function getAccessTokenFromRefreshToken($token) { 90 | 91 | return $this->storeTokenData( 92 | parent::requestAccessTokenFromRefreshToken( 93 | $token, 94 | $this->getAuthCredentialList() 95 | ) 96 | ); 97 | } 98 | 99 | private function getAuthCredentialList() { 100 | 101 | // all Google OAuth2 API requests need 'client id' and 'client secret' in their payloads 102 | return [ 103 | 'client_id' => $this->clientID, 104 | 'client_secret' => $this->clientSecret 105 | ]; 106 | } 107 | 108 | private function storeTokenData(array $data,$hasRefreshKey = false) { 109 | 110 | // save values - false for any key(s) that don't exist 111 | $getValue = function($key) use ($data) { 112 | 113 | return (isset($data[$key])) ? $data[$key] : false; 114 | }; 115 | 116 | $this->accessToken = $getValue('accessToken'); 117 | $this->tokenType = $getValue('tokenType'); 118 | $this->expiresAt = $getValue('expiresAt'); 119 | 120 | if ($hasRefreshKey) { 121 | // only save refresh token if expecting it 122 | $this->refreshToken = $getValue('refreshToken'); 123 | } 124 | 125 | // return $data for chaining 126 | return $data; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /oauth2/token.php: -------------------------------------------------------------------------------- 1 | tokenURL = $tokenURL; 18 | $this->redirectURL = $redirectURL; 19 | } 20 | 21 | public function getAccessTokenFromAuthCode($code) { 22 | 23 | $this->requestAccessTokenFromAuthCode($code); 24 | } 25 | 26 | public function getAccessTokenFromRefreshToken($token) { 27 | 28 | $this->requestAccessTokenFromRefreshToken($token); 29 | } 30 | 31 | protected function requestAccessTokenFromAuthCode($code,array $parameterList = []) { 32 | 33 | // build POST parameter list 34 | $POSTList = [ 35 | 'code' => $code, 36 | 'grant_type' => self::GRANT_TYPE_AUTH, 37 | 'redirect_uri' => $this->redirectURL 38 | ]; 39 | 40 | // add additional parameters 41 | $POSTList += $parameterList; 42 | 43 | // make request, parse JSON response 44 | $dataJSON = $this->HTTPRequestGetJSON( 45 | $this->HTTPRequest($POSTList) 46 | ); 47 | 48 | // ensure required keys exist 49 | // note: key 'expires_in' is recommended but not required in RFC 6749 50 | foreach (['access_token','expires_in','token_type'] as $key) { 51 | if (!isset($dataJSON[$key])) { 52 | throw new \Exception('OAuth2 access token response expected [' . $key . ']'); 53 | } 54 | } 55 | 56 | // return result - include refresh token if given 57 | $tokenData = $this->buildBaseTokenReturnData($dataJSON); 58 | if (isset($dataJSON['refresh_token'])) { 59 | $tokenData['refreshToken'] = $dataJSON['refresh_token']; 60 | } 61 | 62 | return $tokenData; 63 | } 64 | 65 | protected function requestAccessTokenFromRefreshToken($token,array $parameterList = []) { 66 | 67 | // build POST parameter list 68 | $POSTList = [ 69 | 'grant_type' => self::GRANT_TYPE_REFRESH, 70 | 'refresh_token' => $token 71 | ]; 72 | 73 | // add additional parameters 74 | $POSTList += $parameterList; 75 | 76 | // make request, parse JSON response 77 | $dataJSON = $this->HTTPRequestGetJSON( 78 | $this->HTTPRequest($POSTList) 79 | ); 80 | 81 | // ensure required keys exist 82 | foreach (['access_token','expires_in','token_type'] as $key) { 83 | if (!isset($dataJSON[$key])) { 84 | throw new \Exception('OAuth2 refresh access token response expected [' . $key . ']'); 85 | } 86 | } 87 | 88 | // return result 89 | return $this->buildBaseTokenReturnData($dataJSON); 90 | } 91 | 92 | private function HTTPRequest(array $POSTList) { 93 | 94 | $curlConn = curl_init(); 95 | 96 | curl_setopt_array( 97 | $curlConn,[ 98 | CURLOPT_HEADER => false, 99 | CURLOPT_POST => true, 100 | CURLOPT_POSTFIELDS => $POSTList, 101 | CURLOPT_RETURNTRANSFER => true, 102 | CURLOPT_URL => $this->tokenURL 103 | ] 104 | ); 105 | 106 | // make request, close connection 107 | $responseBody = curl_exec($curlConn); 108 | $responseHTTPCode = curl_getinfo($curlConn,CURLINFO_HTTP_CODE); 109 | curl_close($curlConn); 110 | 111 | // return HTTP code and response body 112 | return [$responseHTTPCode,$responseBody]; 113 | } 114 | 115 | private function HTTPRequestGetJSON(array $responseData) { 116 | 117 | list($HTTPCode,$body) = $responseData; 118 | 119 | if ($HTTPCode != self::HTTP_CODE_OK) { 120 | // request error 121 | throw new \Exception('OAuth2 request access token failed'); 122 | } 123 | 124 | // convert JSON response to array 125 | $JSON = json_decode($body,true); 126 | if ($JSON === null) { 127 | // bad JSON data 128 | throw new \Exception('OAuth2 request access token malformed response'); 129 | } 130 | 131 | return $JSON; 132 | } 133 | 134 | private function buildBaseTokenReturnData(array $dataJSON) { 135 | 136 | return [ 137 | 'accessToken' => $dataJSON['access_token'], 138 | 'expiresAt' => time() + intval($dataJSON['expires_in']), // convert to unix timestamp 139 | 'tokenType' => $dataJSON['token_type'] 140 | ]; 141 | } 142 | } 143 | --------------------------------------------------------------------------------