├── .gitignore ├── .htaccess ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── app └── Application.php ├── composer.json ├── composer.lock ├── docker-compose.yml ├── env.tmpl ├── icon.svg ├── index.php ├── lib ├── Util │ ├── HMAC.php │ └── MessageLog.php └── Weebly │ ├── LICENSE.md │ ├── WeeblyClient.php │ └── readme.md ├── manifest.json └── messages └── messages.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | .env 4 | *.swp 5 | *.swo 6 | *.log 7 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | # RewriteBase / 8 | 9 | RewriteCond %{REQUEST_FILENAME} !-f 10 | RewriteRule ^(.*)$ index.php [QSA,L] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM heroku/php 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Weebly 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-apache2 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-webhook-client 2 | 3 | This is a simple webhook listener client written in PHP as example server-side code for the Weebly Developer Platform. More information about the platform can be found at https://dev.weebly.com. 4 | 5 | The server does two main things: 6 | 7 | 1. Acts as a server to facilitate an OAuth handshake between Weebly and your app 8 | 2. Provides an endpoint for webhook-enabled apps 9 | 10 | ### Requirements 11 | 12 | * Docker installed locally 13 | * Heroku Toolbelt installed locally 14 | * Heroku.com account 15 | * Git installed locally 16 | * Github.com account 17 | * Weebly Free Developer Account 18 | 19 | # Usage 20 | 21 | * [Heroku One-Button Quickstart](#heroku-one-button-quickstart) 22 | * [Running with Heroku Container Registry](#running-with-heroku-container-registry) 23 | * [Heroku Local and ngrok](#running-locally-with-heroku-local-and-ngrok) 24 | 25 | ## Heroku One-Button Quickstart 26 | 27 | Make sure you have the following data available: 28 | 29 | * Weebly App Name 30 | * Weebly App - Client ID 31 | * Weebly App - Secret 32 | 33 | > NOTE: You obtain Weebly API Keys for your app from the [Weebly Developer Admin Portal](https://www.weebly.com/developer-admin/) 34 | 35 | 1. Click on the "Deploy to Heroku" button below, and then update the config vars with the respective values. 36 | 37 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 38 | 39 | 2. Rename `env.tmpl` file to `.env` and edit the file to add the Weebly API Keys, replace the OAuth Callback URL, and Webhook Callback URL. 40 | 41 | In the `.env` file, replace **WEEBLY_CLIENT_ID** and **WEEBLY_CLIENT_SECRET** values respectively (removing the template string tags `{{` `}}` from the values. 42 | 43 | For example, if your client id is **123456789**, you would change this line: 44 | 45 | `"client_id": "{{YOUR_CLIENT_ID}}",` to look like this: `"client_id": "123456789",` 46 | 47 | Save your changes, and close the `.env` file. 48 | 49 | 3. Update `manifest.json` file. 50 | 51 | Use the secured URL you receive for your new Heroku app (once you have finished installing and deploying it), and replace the base URL **callback_url** and **webhook_callback_url** (leave the paths in tact) 52 | 53 | For example, if your heroku app URL is: **https://custom-jello-123.herokuapp.com**, you would change this line: 54 | 55 | `"callback_url": "{{YOUR_SECURE_APP_ROOT_URL}}/oauth/phase-one",` to look like this: `"callback_url": "https://custom-jello-123.herokuapp.com/oauth/phase-one", 56 | 57 | Once you have updated both the `callback_url` and the `webhook_callback_url` values, save and exit the `manifest.json` file. 58 | 59 | 4. Create a ZIP archive of the: `manifest.json`, `icon.svg` files. 60 | 61 | 5. Login to [Weebly Developer Admin Portal](https://www.weebly.com/developer-admin/) and upload a new version of your app (selecting the ZIP file you just created) 62 | 63 | 6. Install the new draft version of your app into your Weebly Developer Test Site, and view the logs in heroku 64 | 65 | `heroku logs --tail` 66 | 67 | 7. You can generate webhook events by logging into and out of your [Weebly Developer Site](https://weebly.com) account. 68 | 69 | 8. Open your heroku app and if you see "It works!" the app is running. 70 | 71 | From here, you would want to update the `app/webhooks-router.js` file with logic to do more than just log the events to the console. 72 | 73 | If you use the Quickstart to run this app, the following directions for use with Heroku Local and Heroku Container Registry may not operate as expected. 74 | 75 | ## Running with Heroku Container Registry 76 | 77 | * [Heroku Container Registry docs](https://devcenter.heroku.com/articles/container-registry-and-runtime) 78 | 79 | 1. Clone the code base to your local machine 80 | 81 | `git clone https://github.com/weebly/node-webhook-client` 82 | 83 | 2. Change your present working directory (PWD) into the newly cloned repository directory 84 | 85 | `cd node-webhook-client/` 86 | 87 | 3. Login to Heroku and Heroku Container, to obtain the URL for your remote Heroku app 88 | 89 | `heroku login` 90 | 91 | `heroku container:login` 92 | 93 | `heroku create` (copy the secured URL beginning with **https://** you will need that to update the `manifest.json` later) 94 | 95 | 4. Rename `env.tmpl` file to `.env` and edit the file to add the Weebly API Keys, replace the OAuth Callback URL, and Webhook Callback URL. 96 | 97 | You obtain Weebly API Keys for your app from the [Weebly Developer Admin Portal](https://www.weebly.com/developer-admin/). In the `.env` file, replace the value for **WEEBLY_CLIENT_ID** with your app's Client ID (making sure to remove template string tags `{{` `}}`` from the values. 98 | 99 | For example, if your client id is **123456789**, you would change this line: 100 | 101 | `"client_id": "{{YOUR_CLIENT_ID}}",` to look like this: `"client_id": "123456789",` 102 | 103 | Update Your Secured App Root URL you obtained from the `heroku create` command and replace the the root URL respectively for: **oauth_callback_url** and **webhook_callback_url** (leave the paths in tact) 104 | 105 | 5. Set your Weebly API Keys as environment variables on the Heroku app 106 | 107 | `heroku config:set WEEBLY_CLIENT_ID=[your_app_client_id]` 108 | `heroku config:set WEEBLY_CLIENT_SECRET=[your_app_secret_key]` 109 | `heroku config:set NODE_ENV=production` 110 | 111 | 6. Build the image and push to Container Registry 112 | 113 | `heroku container:push web` 114 | 115 | 7. Release the image to your app 116 | 117 | `heroku container: release web` 118 | 119 | 8. Open the app in your browser 120 | 121 | `heroku open` 122 | 123 | 9. Update `manifest.json` file. 124 | 125 | Use the secured URL you receive for your new Heroku app (once you have finished installing and deploying it), and replace the base URL **callback_url** and **webhook_callback_url** (leave the paths in tact) 126 | 127 | For example, if your heroku app URL is: **https://custom-jello-123.herokuapp.com**, you would change this line: 128 | 129 | `"callback_url": "{{YOUR_SECURE_APP_ROOT_URL}}/oauth/phase-one",` to look like this: `"callback_url": "https://custom-jello-123.herokuapp.com/oauth/phase-one", 130 | 131 | Once you have updated both the `callback_url` and the `webhook_callback_url` values, save and exit the `manifest.json` file. 132 | 133 | 10. Create a ZIP archive of the: `manifest.json`, `icon.svg` files. 134 | 135 | 11. Login to [Weebly Developer Admin Portal](https://www.weebly.com/developer-admin/) and upload a new version of your app (selecting the ZIP file you just created) 136 | 137 | 12. Install the new draft version of your app into your Weebly Developer Test Site, and view the logs in your console 138 | 139 | 13. You can generate webhook events by logging into and out of your [Weebly Developer Site](https://weebly.com) account. 140 | 141 | 14. Open your heroku app and if you see "It works!" the app is running. 142 | 143 | From here, you would want to update the `app/webhooks-router.js` file with logic to do more than just log the events to the console. 144 | 145 | 146 | 147 | ## Running locally with Heroku Local and ngrok 148 | 149 | Make sure you have an [ngrok](https://ngrok.com) account and that it is installed on your local workstation. 150 | 151 | * [Heroku Local docs](https://devcenter.heroku.com/articles/heroku-local) 152 | * [ngrok docs](https://ngrok.com/docs) 153 | 154 | 1. Clone the code base to your local machine 155 | 156 | `git clone https://github.com/weebly/node-webhook-client` 157 | 158 | 2. Change your present working directory (PWD) into the newly cloned repository directory 159 | 160 | `cd node-webhook-client/` 161 | 162 | 4. Rename `env.tmpl` file to `.env` and edit the file to add the Weebly API Keys, replace the OAuth Callback URL, and Webhook Callback URL. 163 | 164 | You obtain Weebly API Keys for your app from the [Weebly Developer Admin Portal](https://www.weebly.com/developer-admin/). In the `.env` file, replace the value for **WEEBLY_CLIENT_ID** with your app's Client ID (making sure to remove template string tags `{{` `}}`` from the values. 165 | 166 | For example, if your client id is **123456789**, you would change this line: 167 | 168 | `"client_id": "{{YOUR_CLIENT_ID}}",` to look like this: `"client_id": "123456789",` 169 | 170 | Update Your Secured App Root URL you obtained from **ngrok** and replace the the root URL respectively for: **oauth_callback_url** and **webhook_callback_url** (leave the paths in tact) 171 | 172 | 5. Update `manifest.json` file. 173 | 174 | Use the secured URL you receive for your new Heroku app (once you have finished installing and deploying it), and replace the base URL **callback_url** and **webhook_callback_url** (leave the paths in tact) 175 | 176 | For example, if your heroku app URL is: **https://custom-jello-123.herokuapp.com**, you would change this line: 177 | 178 | `"callback_url": "{{YOUR_SECURE_APP_ROOT_URL}}/oauth/phase-one",` to look like this: `"callback_url": "https://custom-jello-123.herokuapp.com/oauth/phase-one", 179 | 180 | Once you have updated both the `callback_url` and the `webhook_callback_url` values, save and exit the `manifest.json` file. 181 | 182 | 6. Create a ZIP archive of the: `manifest.json`, `icon.svg` files. 183 | 184 | 7. Login to [Weebly Developer Admin Portal](https://www.weebly.com/developer-admin/) and upload a new version of your app (selecting the ZIP file you just created) 185 | 186 | 8. Start **ngrok** for your app 187 | 188 | 9. Start your app: `heroku local` (depending upon how you configure ngrok, you may need to change the port used by the app) 189 | 190 | 10. Install the new draft version of your app into your Weebly Developer Test Site, and view the logs in your console 191 | 192 | 11. You can generate webhook events by logging into and out of your [Weebly Developer Site](https://weebly.com) account. 193 | 194 | 12. Open your heroku app and if you see "It works!" the app is running. 195 | 196 | ## Manual Heroku Deployment 197 | 198 | The server is intended to be deployed via Heroku, and will look to Heroku-flavored environment variables for specific keys. 199 | 200 | For Heroku usage, after cloning this repository (and assuming you have the Heroku CLI installed): 201 | 202 | ``` 203 | heroku create 204 | heroku config:set WEEBLY_CLIENT_ID=[your_apps_client_id] 205 | heroku config:set WEEBLY_SECRET_KEY=[your_apps_secret_key] 206 | git push heroku master 207 | heroku ps:scale web=1 208 | ``` 209 | 210 | On receiving a webhook, the server will write to the `messages/messages.txt` file; this file is accessible by navigating to the default route on your heroku instance (`/`), and thus is accessible via `heroku open`. 211 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Php Webhook Client", 3 | "description": "A sample microservice to subscribe and react to Weebly Webhook events.", 4 | "repository": "https://github.com/weebly/php-webhook-client", 5 | "logo": "https://dev.weebly.com/images/weebly_logo.svg", 6 | "keywords": [ 7 | "Square", 8 | "Weebly", 9 | "node", 10 | "webhook", 11 | "webhooks", 12 | "event", 13 | "subscriber", 14 | "subscription", 15 | "sample app" 16 | ], 17 | "env": { 18 | "WEEBLY_APP_NAME": { 19 | "description": "Replace {{PREFIX}} with your first initial and the first three characters of your last name, keep `-env-sub` as is.", 20 | "value": "{{PREFIX}}-evt-sub", 21 | "required": true 22 | }, 23 | "WEEBLY_CLIENT_ID": { 24 | "description": "The `Client ID` for your Weebly app, obtained from the Weebly Developer Admin Portal: https://www.weebly.com/developer-admin/", 25 | "required": true 26 | }, 27 | "WEEBLY_SECRET": { 28 | "description": "The `Secret` for your Weebly app, obtained from the Weebly Developer Admin Portal: https://www.weebly.com/developer-admin/", 29 | "required": true 30 | } 31 | }, 32 | "image": "heroku/php", 33 | "addons": [ 34 | "papertrail:choklad" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /app/Application.php: -------------------------------------------------------------------------------- 1 | messageLog = new MessageLog($root_directory . '/messages/messages.txt'); 53 | $this->client_id = $client_id; 54 | $this->client_secret = $client_secret; 55 | $this->parseRequest($request); 56 | } 57 | 58 | /** 59 | * Route request 60 | */ 61 | public function run() { 62 | // basic router 63 | switch($this->route[0]) { 64 | case '': 65 | // main url 66 | $this->appHome(); 67 | break; 68 | 69 | case 'oauth': 70 | // oauth url 71 | switch($this->route[1]) { 72 | case 'phase_one': 73 | $this->oauthPhaseOne($this->params); 74 | break; 75 | 76 | case 'phase_two': 77 | $this->oauthPhaseTwo($this->params); 78 | break; 79 | }; 80 | break; 81 | 82 | case 'webhooks': 83 | // webhook listener url 84 | switch($this->route[1]) { 85 | case 'callback': 86 | $this->processWebhook(); 87 | break; 88 | }; 89 | break; 90 | 91 | 92 | default: 93 | // Unknown Request 94 | break; 95 | 96 | }; 97 | } 98 | 99 | /** 100 | * Parse Route and GET Parameters from request 101 | * 102 | * @param $request Request URL after the host name 103 | */ 104 | private function parseRequest($request) { 105 | /** 106 | * URL parsing 107 | * 108 | * break the Request URL into route sections and GET parameters 109 | */ 110 | if(stripos($request,'?') === false) { 111 | // no params 112 | $route = explode('/', $request); 113 | $params = []; 114 | } else { 115 | // params included 116 | list($route, $url_params) = explode('?', $request, 2); 117 | $route = explode('/', $route); 118 | $params = []; 119 | parse_str($url_params, $params); 120 | } 121 | 122 | /** 123 | * Route always starts with a / so first element will be blank 124 | * 125 | * remove initial element 126 | */ 127 | $route = array_slice($route, 1); 128 | 129 | $this->route = $route; 130 | $this->params = $params; 131 | } 132 | 133 | /** 134 | * Handle the / route 135 | */ 136 | private function appHome() { 137 | echo << 139 | 140 | PHP Client App 141 | 142 | 143 |

PHP Client App

144 |
145 | Download Log 146 |
147 |

Webhook Output:

148 |
149 | 
150 | HTML;
151 | 
152 |         // output messages file to main page
153 |         echo $this->messageLog->readLog();
154 |         
155 |         echo <<
157 |     
158 | 
159 | HTML;
160 |     }
161 | 
162 |     /**
163 |      * Handle the /oauth/phase_one route
164 |      *
165 |      * @param $params GET params passed via URL
166 |      */
167 |     private function oauthPhaseOne($params) {
168 |         // validate hmac
169 |         $hmac_params = array('user_id' => $params['user_id'], 'timestamp' => $params['timestamp']);
170 |         if(isset($params['site_id'])) {
171 |             $hmac_params['site_id'] = $params['site_id'];
172 |         }
173 | 
174 |         if(HMAC::isHmacValid(http_build_query($hmac_params), $this->client_secret, $params['hmac']) === false) {
175 |             echo "

Unable to verify HMAC. Request is invalid.

"; 176 | exit(); 177 | } 178 | 179 | // redirect to authorize endpoint 180 | $wc = new WeeblyClient($this->client_id, $this->client_secret, $params['user_id'], $params['site_id'], null, $params['version']); 181 | 182 | // build link for next step 183 | $phase_two_link = 'https://' . $_SERVER['HTTP_HOST'] . '/oauth/phase_two'; 184 | 185 | // get OAuth URL 186 | $url = $wc->getAuthorizationUrl([], $phase_two_link, $params['callback_url']); 187 | 188 | // redirect to auth url 189 | header('location: ' . $url); 190 | } 191 | 192 | /** 193 | * Handle the /oauth/phase_two route 194 | * 195 | * @param $params GET params passed via URL 196 | */ 197 | private function oauthPhaseTwo($params) { 198 | // build client instance 199 | $wc = new WeeblyClient($this->client_id, $this->client_secret, $params['user_id'], $params['site_id'], null, null); 200 | 201 | // get token from auth code 202 | $token = $wc->getAccessToken($params['authorization_code'], $params['callback_url']); 203 | 204 | // check for a valid return 205 | if($token->access_token !== null) { 206 | // token retrieved 207 | 208 | /** 209 | * Note: If you were going to make API calls on behalf of your application, 210 | * this is where you would want to store the access_token for reuse later. 211 | * 212 | * TODO: Store access_token for user/site 213 | */ 214 | 215 | // redirect to final endpoint 216 | header("location: {$token->callback_url}"); 217 | } else { 218 | // unable to retrieve access_token 219 | 220 | // display error from server 221 | if($token->error !== null) { 222 | echo "

Error: " . $token->error . "

"; 223 | } else { 224 | echo "

Error: Unable to get Access Token

"; 225 | } 226 | } 227 | } 228 | 229 | /** 230 | * Handle the /webhooks/callback route 231 | */ 232 | private function processWebhook() { 233 | // get request data 234 | $input = file_get_contents('php://input'); 235 | 236 | // decode request data 237 | $request_data = json_decode($input); 238 | 239 | // build comparison data for hmac validation 240 | $comparison = [ 241 | 'client_id' => $request_data->client_id, 242 | 'client_version' => $request_data->client_version, 243 | 'event' => $request_data->event, 244 | 'timestamp' => $request_data->timestamp, 245 | 'data' => $request_data->data 246 | ]; 247 | 248 | // validate the request hmac 249 | if(HMAC::isHmacValid(json_encode($comparison), $this->client_secret, $request_data->hmac) === false) { 250 | // record invalid attempt 251 | $data = "\nA new webhook was received, but it's calculated hmac didn't match what was passed.\n"; 252 | $this->messageLog->writeLog($data); 253 | 254 | // respond with an error HTTP header so the webhook queue knows to retry 255 | header('HTTP/1.1 401 HMAC Invalid'); 256 | exit(); 257 | } else { 258 | $data = "\nA valid webhook was received:\n"; 259 | } 260 | 261 | // build headers we want to track 262 | $headers = [ 263 | 'Content-Length' => $_SERVER['CONTENT_LENGTH'], 264 | 'Content-Type' => $_SERVER['CONTENT_TYPE'], 265 | 'Accept' => $_SERVER['HTTP_ACCEPT'], 266 | 'Host' => $_SERVER['HTTP_HOST'], 267 | 'User-Agent' => $_SERVER['HTTP_USER_AGENT'], 268 | 'X-Weebly-Attempt' => $_SERVER['HTTP_X_WEEBLY_ATTEMPT'] 269 | ]; 270 | 271 | // add headers to output data 272 | $data .= "Headers:\n".var_export($headers, true)."\n"; 273 | 274 | // append request data 275 | $data .= "Data:\n".var_export($request_data, true)."\n"; 276 | 277 | // write data to messages file 278 | $this->messageLog->writeLog($data); 279 | 280 | // respond with 200 so webhook delivery isn't retried 281 | header('HTTP/1.1 200 OK'); 282 | } 283 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weebly/php-webhook-client", 3 | "type": "sample application", 4 | "description": "PHP Webhook client server-side sample application", 5 | "keywords": ["weebly", "webhook"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Weebly", 10 | "email": "developer@weebly.com", 11 | "homepage": "https://www.weebly.com", 12 | "role": "Company" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.4.0", 17 | "firebase/php-jwt": "^3.0", 18 | "vlucas/phpdotenv": "^2.5" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Weebly\\": "lib/Weebly/", 23 | "Util\\": "lib/Util/", 24 | "PHPWebhookClient\\": "app/" 25 | } 26 | }, 27 | "require-dev": { 28 | "heroku/heroku-buildpack-php": "^142.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "e4ad6c80a043c245c2eafbaf38ba3969", 8 | "packages": [ 9 | { 10 | "name": "firebase/php-jwt", 11 | "version": "v3.0.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/firebase/php-jwt.git", 15 | "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/firebase/php-jwt/zipball/fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", 20 | "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0" 25 | }, 26 | "type": "library", 27 | "autoload": { 28 | "psr-4": { 29 | "Firebase\\JWT\\": "src" 30 | } 31 | }, 32 | "notification-url": "https://packagist.org/downloads/", 33 | "license": [ 34 | "BSD-3-Clause" 35 | ], 36 | "authors": [ 37 | { 38 | "name": "Neuman Vong", 39 | "email": "neuman+pear@twilio.com", 40 | "role": "Developer" 41 | }, 42 | { 43 | "name": "Anant Narayanan", 44 | "email": "anant@php.net", 45 | "role": "Developer" 46 | } 47 | ], 48 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 49 | "homepage": "https://github.com/firebase/php-jwt", 50 | "time": "2015-07-22T18:31:08+00:00" 51 | }, 52 | { 53 | "name": "vlucas/phpdotenv", 54 | "version": "v2.5.1", 55 | "source": { 56 | "type": "git", 57 | "url": "https://github.com/vlucas/phpdotenv.git", 58 | "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e" 59 | }, 60 | "dist": { 61 | "type": "zip", 62 | "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", 63 | "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", 64 | "shasum": "" 65 | }, 66 | "require": { 67 | "php": ">=5.3.9" 68 | }, 69 | "require-dev": { 70 | "phpunit/phpunit": "^4.8.35 || ^5.0" 71 | }, 72 | "type": "library", 73 | "extra": { 74 | "branch-alias": { 75 | "dev-master": "2.5-dev" 76 | } 77 | }, 78 | "autoload": { 79 | "psr-4": { 80 | "Dotenv\\": "src/" 81 | } 82 | }, 83 | "notification-url": "https://packagist.org/downloads/", 84 | "license": [ 85 | "BSD-3-Clause" 86 | ], 87 | "authors": [ 88 | { 89 | "name": "Vance Lucas", 90 | "email": "vance@vancelucas.com", 91 | "homepage": "http://www.vancelucas.com" 92 | } 93 | ], 94 | "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", 95 | "keywords": [ 96 | "dotenv", 97 | "env", 98 | "environment" 99 | ], 100 | "time": "2018-07-29T20:33:41+00:00" 101 | } 102 | ], 103 | "packages-dev": [ 104 | { 105 | "name": "heroku/heroku-buildpack-php", 106 | "version": "v142", 107 | "source": { 108 | "type": "git", 109 | "url": "https://github.com/heroku/heroku-buildpack-php.git", 110 | "reference": "6a2c921a0d691223a5b92b764ae986fda15b90d6" 111 | }, 112 | "dist": { 113 | "type": "zip", 114 | "url": "https://api.github.com/repos/heroku/heroku-buildpack-php/zipball/6a2c921a0d691223a5b92b764ae986fda15b90d6", 115 | "reference": "6a2c921a0d691223a5b92b764ae986fda15b90d6", 116 | "shasum": "" 117 | }, 118 | "bin": [ 119 | "bin/heroku-hhvm-apache2", 120 | "bin/heroku-hhvm-nginx", 121 | "bin/heroku-php-apache2", 122 | "bin/heroku-php-nginx" 123 | ], 124 | "type": "library", 125 | "notification-url": "https://packagist.org/downloads/", 126 | "license": [ 127 | "MIT" 128 | ], 129 | "authors": [ 130 | { 131 | "name": "David Zuelke", 132 | "email": "dz@heroku.com" 133 | } 134 | ], 135 | "description": "Toolkit for starting a PHP application locally, with or without foreman, using the same config for PHP/HHVM and Apache2/Nginx as on Heroku", 136 | "homepage": "https://github.com/heroku/heroku-buildpack-php", 137 | "keywords": [ 138 | "apache", 139 | "apache2", 140 | "foreman", 141 | "heroku", 142 | "hhvm", 143 | "nginx", 144 | "php" 145 | ], 146 | "time": "2018-08-08T00:56:54+00:00" 147 | } 148 | ], 149 | "aliases": [], 150 | "minimum-stability": "stable", 151 | "stability-flags": [], 152 | "prefer-stable": false, 153 | "prefer-lowest": false, 154 | "platform": { 155 | "php": ">=5.4.0" 156 | }, 157 | "platform-dev": [] 158 | } 159 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | command: 'bash -c ''vendor/bin/heroku-php-apache2''' 4 | working_dir: /app/user 5 | environment: 6 | PORT: ${PORT} 7 | ports: 8 | - '${PORT}:${PORT}' 9 | shell: 10 | build: . 11 | command: bash 12 | working_dir: /app/user 13 | environment: 14 | PORT: ${PORT} 15 | ports: 16 | - '${PORT}:${PORT}' 17 | volumes: 18 | - '.:/app/user' 19 | -------------------------------------------------------------------------------- /env.tmpl: -------------------------------------------------------------------------------- 1 | # EXECUTION MODE 2 | NODE_ENV=development 3 | PORT=3000 4 | 5 | # CUSTOM APP PREFIX (first initial and first three characters of your last name) 6 | APP_PREFIX={{YOUR_CUSTOM_APP_PREFIX}} 7 | 8 | # WEEBLY API KEYS 9 | WEEBLY_CLIENT_ID={{APP_CLIENT_ID}} 10 | WEEBLY_CLIENT_SECRET={{APP_SECRET}} 11 | 12 | # HEROKU 13 | HEROKU_APP_NAME={{HEROKU_APP_NAME}} 14 | SERVER_URL={{HEROKU_APP_URL}} 15 | 16 | # WEEBLY OAUTH 17 | #-- KEEP PATHS AFTER TLD 18 | WEEBLY_API_BASE_URI=https://api.weebly.com/v1 19 | WEEBLY_APP_NAME=${APP_PREFIX}-evt-sub 20 | WEEBLY_TEST_SITE_URL=https://{{YOUR_WEEBLY_DEVELOPER_TEST_SITE_URL}} 21 | WEEBLY_APP_CALLBACK_URL=${SERVER_URL}/oauth/phase-one 22 | 23 | # WEEBLY WEBHOOKS 24 | #-- KEEP PATHS AFTER TLD 25 | WEEBLY_WEBHOOKS_CALLBACK_URL=${SERVER_URL}/webhooks/callback 26 | WEEBLY_OAUTH_FINAL_DESTINATION=editor 27 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | load(); 9 | } 10 | 11 | /** 12 | * Client ID and Secret need to get set outside of the codebase, expecting environment var to store it 13 | */ 14 | $client_id = getenv('WEEBLY_CLIENT_ID') ? getenv('WEEBLY_CLIENT_ID') : null; 15 | $client_secret = getenv('WEEBLY_CLIENT_SECRET') ? getenv('WEEBLY_CLIENT_SECRET') : null; 16 | 17 | if($client_id === null || $client_secret === null) { 18 | echo "Error: Env vars not set for application."; 19 | exit(); 20 | } 21 | 22 | // Create Application instance 23 | $app = new PHPWebhookClient\Application($client_id, $client_secret, __DIR__, $_SERVER['REQUEST_URI']); 24 | 25 | // remove vars from global scope 26 | unset($client_id); 27 | unset($client_secret); 28 | 29 | $app->run(); 30 | -------------------------------------------------------------------------------- /lib/Util/HMAC.php: -------------------------------------------------------------------------------- 1 | can_access = false; 26 | 27 | $this->file_name = $file_name; 28 | if(file_exists($this->file_name)) { 29 | $this->can_access = true; 30 | } else { 31 | error_log("Unable to access file at $file_name"); 32 | } 33 | } 34 | 35 | /** 36 | * Attempt to write text to file 37 | * 38 | * @param $text data to write to file 39 | * @return bool true if write succeeded, false otherwise 40 | */ 41 | public function writeLog($text) { 42 | // verify file can be accessed 43 | if($this->can_access === false) { 44 | return false; 45 | } 46 | 47 | // write text to file 48 | return (file_put_contents($this->file_name, $text, FILE_APPEND) > 0); 49 | } 50 | 51 | /** 52 | * Attempt to read text from file 53 | * 54 | * @return bool|string false if read failed, otherwise return the file text 55 | */ 56 | public function readLog() { 57 | // verify file can be accessed 58 | if($this->can_access === false) { 59 | return false; 60 | } 61 | 62 | return file_get_contents($this->file_name); 63 | } 64 | } -------------------------------------------------------------------------------- /lib/Weebly/LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | 3 | Copyright (c) 2015, Weebly All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of Weebly nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Weebly, Inc BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /lib/Weebly/WeeblyClient.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 2015-03-30 8 | */ 9 | 10 | namespace Weebly; 11 | 12 | class WeeblyClient 13 | { 14 | /** 15 | * Weebly domain 16 | */ 17 | const WEEBLY_DOMAIN = 'https://www.weebly.com'; 18 | 19 | /** 20 | * Weebly API domain 21 | */ 22 | const WEEBLY_API_DOMAIN = 'https://api.weebly.com/v1'; 23 | 24 | /** 25 | * Weebly User Id 26 | * 27 | * @var $user_id 28 | */ 29 | public $user_id; 30 | 31 | /** 32 | * Weebly Site Id 33 | * 34 | * @var $site_id 35 | */ 36 | public $site_id; 37 | 38 | /** 39 | * Weebly User's API Access token 40 | * 41 | * @var $access_token 42 | */ 43 | private $access_token; 44 | 45 | /** 46 | * Application Client Id 47 | * 48 | * @var $client_id 49 | */ 50 | private $client_id; 51 | 52 | /** 53 | * Application Client Secret 54 | * 55 | * @var $client_secret 56 | */ 57 | private $client_secret; 58 | 59 | /** 60 | * Default Curl Options 61 | * 62 | * @var $default_curl_options 63 | */ 64 | private $default_curl_options = array( 65 | CURLOPT_RETURNTRANSFER => true, 66 | CURLOPT_CONNECTTIMEOUT => 30, 67 | CURLOPT_TIMEOUT => 30, 68 | CURLOPT_USERAGENT => 'weebly/weebly_client' 69 | ); 70 | 71 | /** 72 | * Cached Curl Handler 73 | * 74 | * @var $curl_handler 75 | */ 76 | private $curl_handler; 77 | 78 | 79 | /** 80 | * Creates a new API interaction object. 81 | * 82 | * @param string $client_id Your application client_id 83 | * @param string $client_secret Your application client_secret 84 | * @param (optional) int $user_id The Weebly User Id 85 | * @param (optional) int $site_id The Weebly Site Id 86 | * @param (optional) string $access_token The Weebly User's API access token used for accessing 87 | * data from already permitted users 88 | * 89 | * @return instance 90 | */ 91 | public function __construct($client_id, $client_secret, $user_id=null, $site_id=null, $access_token=null, $version=null) 92 | { 93 | $this->client_id = $client_id; 94 | $this->client_secret = $client_secret; 95 | $this->user_id = $user_id; 96 | $this->site_id = $site_id; 97 | $this->access_token = $access_token; 98 | $this->version = $version; 99 | } 100 | 101 | /** 102 | * Returns the url to redirect to for oauth authentication (Step 1 for you in the OAuth flow) 103 | * This method requires that you provide both the user_id and site_id when constructing the client object. 104 | * 105 | * @param (optional) array $scope An array of the permissions your application is 106 | * requesting i.e (read:user, read:commerce) 107 | * @param (optional) string $redirect_uri The url weebly will redirect to upon user's grant of 108 | * permissions. Defaults to application callback url 109 | * @param (optional) string $callback_url The url provided by weebly to initiate the authorize 110 | * step of the oauth process 111 | * 112 | * 113 | * @return string $authorization_url 114 | */ 115 | public function getAuthorizationUrl($scope=array(), $redirect_uri=null, $callback_url=null) 116 | { 117 | if (isset($callback_url) === true) { 118 | $authorization_url = $callback_url; 119 | } else { 120 | $authorization_url = self::WEEBLY_DOMAIN.'/app-center/oauth/authorize'; 121 | } 122 | 123 | $parameters = '?client_id='.$this->client_id.'&user_id='.$this->user_id; 124 | 125 | if (isset($this->site_id) === true) { 126 | $parameters .= '&site_id='.$this->site_id; 127 | } 128 | 129 | if (isset($redirect_uri) === true) { 130 | $parameters .= '&redirect_uri='.$redirect_uri; 131 | } 132 | 133 | if (is_array($scope) === true && count($scope) > 0) { 134 | $scope_parameters = implode(',', $scope); 135 | $parameters .= '&scope='.$scope_parameters; 136 | } 137 | 138 | if (isset($this->version)) { 139 | $parameters .= '&version='.$this->version; 140 | } 141 | 142 | return $authorization_url.$parameters; 143 | } 144 | 145 | /** 146 | * Makes authenticated API GET Request using provided URL, and parameters 147 | * 148 | * @param string $url 149 | * @param array $parameters 150 | * @return json $result 151 | */ 152 | public function get($url) 153 | { 154 | return $this->makeRequest(self::WEEBLY_API_DOMAIN . $url, [], 'GET'); 155 | } 156 | 157 | /** 158 | * Makes authenticated API POST Request using provided URL, and parameters 159 | * 160 | * @param string $url 161 | * @param array $parameters 162 | * @return json $result 163 | */ 164 | public function post($url, $parameters=[]) 165 | { 166 | return $this->makeRequest(self::WEEBLY_API_DOMAIN.$url, $parameters, 'POST'); 167 | } 168 | 169 | /** 170 | * Makes authenticated API DELETE Request using provided URL, and parameters 171 | * 172 | * @param string $url 173 | * @param array $parameters 174 | * @return json $result 175 | */ 176 | public function delete($url, $parameters=[]) 177 | { 178 | return $this->makeRequest(self::WEEBLY_API_DOMAIN.$url, $parameters, 'DELETE'); 179 | } 180 | 181 | /** 182 | * Makes authenticated API PATCH Request using provided URL, and parameters 183 | * 184 | * @param string $url 185 | * @param array $parameters 186 | * @return json $result 187 | */ 188 | public function patch($url, $parameters=[]) 189 | { 190 | return $this->makeRequest(self::WEEBLY_API_DOMAIN.$url, $parameters, 'PATCH'); 191 | } 192 | 193 | /** 194 | * Makes authenticated API PUT Request using provided URL, and parameters 195 | * 196 | * @param string $url 197 | * @param array $parameters 198 | * @return json $result 199 | */ 200 | public function put($url, $parameters=[]) 201 | { 202 | return $this->makeRequest(self::WEEBLY_API_DOMAIN.$url, $parameters, 'PUT'); 203 | } 204 | 205 | /** 206 | * Exchanges a temporary authorization code for a permanent access_token 207 | * 208 | * @param string $authorization_code The authorization_code sent from weebly after the user has 209 | * granted the application access to their data. 210 | * @param (optional) string $callback_url The url provided by weebly to retrieve the access token 211 | * 212 | * 213 | * @return string $access_token 214 | */ 215 | public function getAccessToken($authorization_code, $callback_url=null) 216 | { 217 | if (isset($callback_url) === true) { 218 | $url = $callback_url; 219 | } else { 220 | $url = self::WEEBLY_DOMAIN.'/app-center/oauth/access_token'; 221 | } 222 | 223 | $result = $this->makeRequest($url, $this->prepareAccessTokenParams($authorization_code)); 224 | return $result; 225 | } 226 | 227 | /** 228 | * Returns an array of the parameters required for retrieving a weebly access token for a user 229 | * 230 | * @param string $authorization_code 231 | * @return array $params 232 | */ 233 | private function prepareAccessTokenParams($authorization_code) 234 | { 235 | $params = array( 236 | 'client_id' => $this->client_id, 237 | 'client_secret' => $this->client_secret, 238 | 'authorization_code' => $authorization_code 239 | ); 240 | return $params; 241 | } 242 | 243 | /** 244 | * Internal function used for making curl requests to api 245 | * 246 | * @param string $url URL to make request to 247 | * @param (optional) array $paramenters Array of parameters to pass 248 | * @param (optional) string $method HTTP method, defaults to 'POST' 249 | * 250 | * @return array $response 251 | */ 252 | private function makeRequest($url, $parameters = array(), $method = 'POST') 253 | { 254 | $curl_handler = $this->getCurlHandler(); 255 | 256 | if ($method === 'GET') { 257 | // The HTTP method may have been set to POST. Reset the POST options on the handler to default to GET. 258 | curl_reset($curl_handler); 259 | $options = []; 260 | } else { 261 | $options = array( 262 | CURLOPT_CUSTOMREQUEST => $method, 263 | CURLOPT_POSTFIELDS => json_encode($parameters) 264 | ); 265 | } 266 | 267 | $header = array(); 268 | $header[] = 'Content-type: application/json'; 269 | if ($this->access_token) { 270 | $header[] = 'X-Weebly-Access-Token: ' . $this->access_token; 271 | } else { 272 | $header[] = 'X-Weebly-Client-ID: ' . $this->client_id; 273 | $header[] = 'X-Weebly-Client-Secret: ' . $this->client_secret; 274 | } 275 | $options[CURLOPT_HTTPHEADER] = $header; 276 | 277 | $options[CURLOPT_URL] = $url; 278 | curl_setopt_array($curl_handler, $this->default_curl_options + $options); 279 | $result = curl_exec($curl_handler); 280 | return json_decode($result); 281 | } 282 | 283 | /** 284 | * Retrieves the running instance of a curl handler if one exists; otherwise creates one. 285 | * 286 | * @return resource $this->curl_handler 287 | */ 288 | private function getCurlHandler() 289 | { 290 | if (isset($this->curl_handler) === false) { 291 | $this->curl_handler = curl_init(); 292 | } 293 | 294 | return $this->curl_handler; 295 | } 296 | } -------------------------------------------------------------------------------- /lib/Weebly/readme.md: -------------------------------------------------------------------------------- 1 | # Weebly Client 2 | 3 | Soon to be a client for using the weebly platform API -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": "1", 3 | "client_id": "{{YOUR_CLIENT_ID}}", 4 | "version": "1.0.0", 5 | "callback_url": "{{YOUR_SECURE_APP_ROOT_URL}}/oauth/phase-one", 6 | "oauth_final_destination": "home", 7 | "webhooks": { 8 | "callback_url": "{{YOUR_SECURE_APP_ROOT_URL}}/webhooks/callback", 9 | "events": [ 10 | "user.update","site.publish", "site.delete", "app.uninstall" 11 | ] 12 | }, 13 | "scopes": [ 14 | "read:user", 15 | "read:site" 16 | ], 17 | "locale": { 18 | "default": "en-us", 19 | "supported": ["en-us"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /messages/messages.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Weebly/php-webhook-client/1d4179597c569f7b8da0e5de2faae775fb88074f/messages/messages.txt --------------------------------------------------------------------------------