├── .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 |
--------------------------------------------------------------------------------