diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..74c22bf --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +MAUTICAPIUSER='' +MAUTICAPIPW='' +MAUTICAPIURL='' \ No newline at end of file diff --git a/.gcloudignore b/.gcloudignore deleted file mode 100644 index c44fa30..0000000 --- a/.gcloudignore +++ /dev/null @@ -1,19 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -node_modules -#!include:.gitignore -docs -README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5657f6e..feba77f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -vendor \ No newline at end of file +vendor +.env +.DS_Store +log.txt +webhooks/*.json \ No newline at end of file diff --git a/README.md b/README.md index 43ccea9..6e8007a 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,51 @@ # What it does 🤔 -Coach Freem receives your webhooks from Freemius, dusts them off, gives them a pep talk, and then sends them off to Mautic (he doesn't know that he's an app). +Coach Freem receives your webhooks from Freemius, dusts them off, gives them a pep talk, and then sends them off to Mautic. -# Why 🤷 - -Because automation. - -# How to use it 🛠️ +In other words, Coach Freem receives webhooks from Freemius, saves them, processes them, and creates/updates the respect Mautic contact based on the Webhook received by Freemius. -Coach Freem was intended to be run as a Google Cloud Function that comes up every time the various webhooks are sent by Freemius. Google Cloud functions has a nice free tier so there's that. +When a webhook is received from Freemius, the contents are saved to the `webhooks` folder. This is so that the connection from Freemius can be disconnected so as to not timeout at 10 seconds, which is the maximum amount of time Freemius allows it's webhooks to run. -Coach isn't limited to Google Cloud Functions, he can easily be moved to your very own server with a few tweaks. All you'd need to do is create a route on your server/website that accepts POST requests, and pass that request body to Coach. -That pretty much happens on the first few lines of the `init` function. You can use a framework such as [SlimPHP](https://www.slimframework.com/) if you'd like to go that route. +A separate cron job then comes in to process the saved webhooks and creates/updates the mautic contact. -Coach makes use of Mautic's Basic Auth, so all you need to do is create an Admin user on your Mautic install and change the details in `setClient` method, inside `Client.php` in the `includes` folder. You also need to enable the REST API on your Mautic install. +# Why 🤷 -By default the method expects environment variables for these values, but you can set the details manually for testing or actually set them inside your $PATH. +Because automation. -In production you'd set the set environment variables inside the Google Functions config. +# How to use it 🛠️ -## Composer +Coach makes use of Mautic's Basic Auth, so all you need to do is: -On your localhost run `composer install` inside the root folder of Coach Freem. -## Google Functions Setup +- Create a custom field inside your Mautic install called `freemius_id`, make sure that the **Is Unique Identifier** switch for the field is set to **Yes**. Also make sure the alias is `freemius_id` -You may manually upload the source to the Google Cloud Functions dashboard but ideally you can make use of the Google Cloud SDK to deploy and redeploy new versions. +- Enable the Mautic RestAPI in its configuration settings -Install the Google Cloud sdk. If you're using MacOS then you can do so using [Homebrew](https://formulae.brew.sh/cask/google-cloud-sdk). +- Create an Admin *user* in Mautic -You can deploy the new version by running `composer deploy`. This will start the deploy steps and you may be asked for Google Cloud Credentials if you've never connected via command line before. +- Edit the `.env.example` file in the `coach-freem` folder then replace the placeholders with the user's credentials and the Mautic API URL (typically something like `https:///api/` -## Local Testing +- Rename `.env.example` to `.env` -You can start a local PHP server for testing your changes by running `composer start`, you will then be able to access Coach locally by going to `http://localhost:8080`. You can then `POST` Freemius webhooks to this URL using a tool like [Insomnia](https://insomnia.rest/) or [Postman](https://www.postman.com/). +## Functions to edit before going live -An example Freemius request webhook can be found in `docs/sample-freemius-webhook.json`, simply copy the contents of this file and paste it as a JSON POST request inside Insomnia/Postman to `http://localhost:8080`. +The following functions inside `index.php` should be edited to match your plugin and mautic details: -You can change the `type` key in the JSON request to the webhook you want to test. +- productIDs() +- customContactDataMappings() +- contactSegments() +- contactTags() +- excludedTLDs() +- isExcludedTLD() -For your own plugin/theme, you can use the Freemius webhook area to send all webhooks to a URL provided by https://webhook.site/ to examine the webhook content testing. +Comments have been added to help explain how these functions should be edited. If comments can be made better then feel free to submit a pull request! ## Freemius dashboard setup -Once you've deployed Coach Freem to Google Cloud Functions, you should get a URL where it can be accessed: +Once you've dropped Coach Freem to your server. You should add the following URL to your Freemius Custom Webhooks area: `https://example.com/coach-freem/?save=1`. Replace `example.com` with your domain name. -![Google Cloud Function URL](./docs/url.png) +The example URL assumes you have the `coach-freem` folder placed in the root directory of your domain. -Copy this URL and head to `Freemius Dashboard->Integrations->Custom Webhooks`, create a new webhook with the URL you copied before but for the event types **ONLY SELECT THE CURRENT EVENTS THAT ARE BEING CONSUMED BY COACH FREEM**. Selecting "all event types" would send every single Freemius event type to your Cloud Function causing unnecessary charges. +Copy this URL and head to `Freemius Dashboard->Integrations->Custom Webhooks`, create a new webhook with the URL you copied before but for the event types **ONLY SELECT THE CURRENT EVENTS THAT ARE BEING CONSUMED BY COACH FREEM**. At the time of writing this line the only events being used by Coach Freem are: @@ -58,32 +57,29 @@ At the time of writing this line the only events being used by Coach Freem are: - install.deactivated - install.uninstalled -You can confirm this by checking the `init()` function in `index.php`. +You can confirm this by checking the `process_webhook()` function in `index.php`. -Your webhook should look something like this: +## Cron Job Setup -![Example webhook configured](./docs/webhooks.png) +Add a cronjob that runs every 5 minutes to your server, the cronjob should hit the URL: `https://example.com/coach-freem/?process=1`. Replace `example.com` with your domain name. -In the image above you can see that we're sending all events to https://webhook.site, this is simply for testing purposes as mentioned above. You can turn off the webhook.site webhook once you're done testing. -## Functions to edit before going live +The example URL assumes you have the `coach-freem` folder placed in the root directory of your domain. -The following functions inside `index.php` should be edited to match your plugin and mautic details: +# Local Testing -- productIDs() -- customContactDataMappings() -- contactSegments() -- contactTags() -- excludedTLDs() -- isExcludedTLD() +You can start a local PHP server for testing your changes by running `php -S localhost:8080` in the root directory of Coach Freem. You will then be able to access Coach locally by going to `http://localhost:8080`. You can then `POST` Freemius webhooks to this URL using a tool like [Insomnia](https://insomnia.rest/) or [Postman](https://www.postman.com/). -Comments have been added to help explain how these functions should be edited. If comments can be made better then feel free to submit a pull request! +An example Freemius request webhook can be found in `docs/sample-freemius-webhook.json`, simply copy the contents of this file and paste it as a JSON POST request inside Insomnia/Postman to `http://localhost:8080`. + +You can change the `type` key in the JSON request to the webhook you want to test. + +For your own plugin/theme, you can use the Freemius webhook area to send all webhooks to a URL provided by https://webhook.site/ to examine the webhook content testing. # What's missing 👨🏾‍💻 -Well...I put this together to *just* get some automation set up for Mautic to further automate my email marketing (the whole reason I'm using it). So pull requests are welcome. +Well...I put this together to *just* get some automation set up for Mautic to further automate my email marketing (the whole reason I'm using it). Missing: -- Handle when a user opts out of marketing. -- Ignore Local sites (create TLD blacklist like .local, .dev etc) \ No newline at end of file +- Handle when a user opts out of marketing. \ No newline at end of file diff --git a/composer.json b/composer.json index b8e78fd..e0e74f7 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,8 @@ "description": "Middleware between Freemius webhooks and Mautic API.", "type": "library", "require": { - "google/cloud-functions-framework": "^1.1", - "mautic/api-library": "^3.1" + "mautic/api-library": "^3.1", + "vlucas/phpdotenv": "^5.5" }, "license": "GPLv2", "authors": [ @@ -18,11 +18,6 @@ } }, "scripts": { - "start": [ - "Composer\\Config::disableProcessTimeout", - "FUNCTION_TARGET=init php -S localhost:${PORT:-8080} vendor/google/cloud-functions-framework/router.php" - ], - "deploy": "gcloud functions deploy coach-freem --gen2 --project=freemius-related" }, "require-dev": { "spatie/ray": "^1.37" diff --git a/composer.lock b/composer.lock index 62114f8..aaacb87 100644 --- a/composer.lock +++ b/composer.lock @@ -4,158 +4,33 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0121629e9619b7c18dac4176e41b6c63", + "content-hash": "e5d86c599d43fe51cd94736e4515a463", "packages": [ { - "name": "cloudevents/sdk-php", - "version": "v1.0.2", + "name": "graham-campbell/result-type", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/cloudevents/sdk-php.git", - "reference": "f2e22c624bd1380e50b6a5c1a9526e29bcce6317" + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cloudevents/sdk-php/zipball/f2e22c624bd1380e50b6a5c1a9526e29bcce6317", - "reference": "f2e22c624bd1380e50b6a5c1a9526e29bcce6317", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-pcre": "*", - "php": "^7.4.15 || ^8.0.2", - "symfony/polyfill-php80": "^1.26" - }, - "require-dev": { - "guzzlehttp/psr7": "^2.4.3", - "php-http/discovery": "^1.15.2", - "phpunit/phpunit": "^9.6.3 || ^10.0.12", - "psalm/phar": "5.7.6", - "psr/http-factory": "^1.0.1", - "psr/http-message": "^1.0.1", - "squizlabs/php_codesniffer": "3.7.2" - }, - "suggest": { - "php-http/discovery": "Required for automatic discovery of HTTP message factories in the HTTP Marshaller.", - "psr/http-factory": "Required for use of the HTTP Marshaller.", - "psr/http-factory-implementation": "Required for use of the HTTP Marshaller.", - "psr/http-message": "Required for use of the HTTP Marshaller and Unmarshaller.", - "psr/http-message-implementation": "Required for use of the HTTP Marshaller and Unmarshaller." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "CloudEvents\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "CloudEvents SDK", - "support": { - "issues": "https://github.com/cloudevents/sdk-php/issues", - "source": "https://github.com/cloudevents/sdk-php" - }, - "time": "2023-02-25T17:31:59+00:00" - }, - { - "name": "google/cloud-functions-framework", - "version": "v1.1.0", - "source": { - "type": "git", - "url": "https://github.com/GoogleCloudPlatform/functions-framework-php.git", - "reference": "7e84ad415d7bc4bf7cc060ee22ba76eefd685bf3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/GoogleCloudPlatform/functions-framework-php/zipball/7e84ad415d7bc4bf7cc060ee22ba76eefd685bf3", - "reference": "7e84ad415d7bc4bf7cc060ee22ba76eefd685bf3", - "shasum": "" - }, - "require": { - "cloudevents/sdk-php": "^1.0", - "guzzlehttp/psr7": "^1.7|^2.0", - "php": ">=7.4", - "psr/http-message": "^1.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^7.2", - "phpunit/phpunit": "^7.5|^8.0" - }, - "suggest": { - "google/cloud-storage": "Google Cloud Storage client library for storing and persisting objects. When included, the functions framework will register the gs:// stream wrapper." - }, - "bin": [ - "router.php" - ], - "type": "library", - "autoload": { - "psr-4": { - "Google\\CloudFunctions\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "Google Cloud Functions Framework for PHP", - "support": { - "issues": "https://github.com/GoogleCloudPlatform/functions-framework-php/issues", - "source": "https://github.com/GoogleCloudPlatform/functions-framework-php/tree/v1.1.0" - }, - "time": "2021-11-09T21:23:24+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.4.4", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", - "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" + "phpoption/phpoption": "^1.9.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - }, - "branch-alias": { - "dev-master": "2.4-dev" - } - }, "autoload": { "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" + "GrahamCampbell\\ResultType\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -167,52 +42,19 @@ "name": "Graham Campbell", "email": "hello@gjcampbell.co.uk", "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" } ], - "description": "PSR-7 message implementation that also provides common utility methods", + "description": "An Implementation Of The Result Type", "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" ], "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.4" + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" }, "funding": [ { @@ -220,15 +62,11 @@ "type": "github" }, { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", "type": "tidelift" } ], - "time": "2023-03-09T13:19:02+00:00" + "time": "2023-02-25T20:23:15+00:00" }, { "name": "mautic/api-library", @@ -274,72 +112,92 @@ "time": "2022-05-04T13:59:18+00:00" }, { - "name": "psr/http-factory", - "version": "1.0.1", + "name": "phpoption/phpoption", + "version": "1.9.1", "source": { "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", "shasum": "" }, "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.9-dev" } }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "PhpOption\\": "src/PhpOption/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "Option Type for PHP", "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" + "language", + "option", + "php", + "type" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" }, - "time": "2019-04-30T12:38:16+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2023-02-25T19:38:58+00:00" }, { - "name": "psr/http-message", - "version": "1.0.1", + "name": "psr/log", + "version": "1.1.4", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -348,12 +206,12 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -363,50 +221,60 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ - "http", - "http-message", + "log", "psr", - "psr-7", - "request", - "response" + "psr-3" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { - "name": "psr/log", - "version": "1.1.4", + "name": "symfony/polyfill-ctype", + "version": "v1.27.0", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -415,48 +283,81 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", "keywords": [ - "log", - "psr", - "psr-3" + "compatibility", + "ctype", + "polyfill", + "portable" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" }, - "time": "2021-05-03T11:20:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" }, { - "name": "ralouphie/getallheaders", - "version": "3.0.3", + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", "source": { "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.1" }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { "files": [ - "src/getallheaders.php" - ] + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -464,16 +365,41 @@ ], "authors": [ { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A polyfill for getallheaders.", + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" }, - "time": "2019-03-08T08:55:37+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php80", @@ -557,6 +483,90 @@ } ], "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.2", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.8", + "symfony/polyfill-ctype": "^1.23", + "symfony/polyfill-mbstring": "^1.23.1", + "symfony/polyfill-php80": "^1.23.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2022-10-16T01:01:54+00:00" } ], "packages-dev": [ @@ -1111,171 +1121,6 @@ ], "time": "2022-01-02T09:53:40+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-11-03T14:55:06+00:00" - }, { "name": "symfony/polyfill-php81", "version": "v1.27.0", diff --git a/docs/sample-freemius-webhook.json b/docs/sample-freemius-webhook.json index 72dde2d..018da91 100644 --- a/docs/sample-freemius-webhook.json +++ b/docs/sample-freemius-webhook.json @@ -1,6 +1,6 @@ { "type": "install.installed", - "plugin_id": "8507", + "plugin_id": "", "user_id": "4952303", "install_id": "11729544", "data": "1.7.3", diff --git a/docs/url.png b/docs/url.png deleted file mode 100644 index 4c169cf..0000000 Binary files a/docs/url.png and /dev/null differ diff --git a/docs/webhooks.png b/docs/webhooks.png deleted file mode 100644 index a031a85..0000000 Binary files a/docs/webhooks.png and /dev/null differ diff --git a/includes/Client.php b/includes/Client.php index c2621af..2580b6c 100644 --- a/includes/Client.php +++ b/includes/Client.php @@ -1,4 +1,5 @@ load(); $settings = [ - 'userName' => getenv('MAUTICAPIUSER'), - 'password' => getenv('MAUTICAPIPW') + 'userName' => $_ENV['MAUTICAPIUSER'], + 'password' => $_ENV['MAUTICAPIPW'] ]; - $endpoint = getenv('MAUTICAPIURL'); + $endpoint = $_ENV['MAUTICAPIURL']; try { $initAuth = new ApiAuth(); $auth = $initAuth->newAuth($settings, 'BasicAuth'); - + $api = new MauticApi(); return $api->newApi($context, $auth, $endpoint); } catch (\Throwable $th) { - //throw $th; + Logger::log('There was an issue setting up the Mautic API client. Error: ' . $th->getMessage()); } return null; @@ -77,5 +81,4 @@ public function getClient(): Api { return $this->setClient($this->context); } - -} \ No newline at end of file +} diff --git a/includes/Contacts/Base.php b/includes/Contacts/Base.php index 47a0b3f..fd777d1 100644 --- a/includes/Contacts/Base.php +++ b/includes/Contacts/Base.php @@ -14,6 +14,8 @@ namespace CoachFreem\Contacts; use CoachFreem\Client; +use CoachFreem\Logger; +use Exception; /** * Base contacts class. @@ -39,6 +41,14 @@ class Base */ protected \Mautic\Api\Contacts $client; + /** + * Logging class. + * + * @var Logger + * @since 1.2.0 + */ + protected Logger $logger; + /** * Contructor. * @@ -48,18 +58,32 @@ class Base public function __construct() { $this->client = (new Client(self::CONTEXT))->getClient(); + $this->logger = new Logger; } /** * Get a contact details using email address. * - * @param array $user_data + * @param int $id The freemius ID. * @return array * @since 1.1.0 + * @throws Exception */ - protected function findContactDetailsByEmail(string $email): array + protected function findContactDetailsByFreemiusID(int $freemius_id): array { - $response = $this->client->getList($email); + try { + $response = $this->client->getList( + "freemius_id:{$freemius_id}", + 0, + 1 + ); + } catch (\Throwable $th) { + Logger::log("Error checking existing contact. Freemius ID: $freemius_id, Error Msg: " . $th->getMessage()); + return array(); + } + if (!is_array($response) && empty($response)) { + return array(); + } $id = array_key_first($response['contacts']); return $response['contacts'][$id] ?? array(); } diff --git a/includes/Contacts/Create.php b/includes/Contacts/Create.php index c664a42..c31c440 100644 --- a/includes/Contacts/Create.php +++ b/includes/Contacts/Create.php @@ -79,25 +79,13 @@ public function __construct(int $plugin_id) */ private function mapWebHookData(array $user_data): array { - $mautic_field_list = $this->client->getFieldList(); + $plugin_custom_mappings = $this->custom_mappings[$this->plugin_id]; $hold = array(); - foreach ($mautic_field_list as $index => $data) { - - $field_name = $data['alias'] ?? ''; - - if (empty($field_name)) { - continue; - } - - $hold[$field_name] = $user_data[$field_name] ?? ''; - - $mapping = array_search($field_name, $plugin_custom_mappings, true); - if (!empty($mapping)) { - $hold[$field_name] = $user_data[$mapping]; - } + foreach ($plugin_custom_mappings as $key => $mapping) { + $hold[$mapping] = $user_data[$key]; } return $hold; @@ -115,9 +103,23 @@ private function addContactToMautic($user_data): ?int $contact_data = $this->mapWebHookData($user_data); $contact_data['tags'] = $this->prepareContactTags($user_data); + // If contact exists already then update values. + $existing_contact = $this->findContactDetailsByFreemiusID((int) $user_data['id']); + if ($existing_contact) { + $contact_details = $this->client->edit($existing_contact['id'], $contact_data); + return $contact_details['contact']['id'] ?? null; + } + + $contact_data['email'] = $user_data['email']; $response = $this->client->create($contact_data); - return $response[$this->client->itemName()]['id'] ?? null; + $mautic_id = $response[$this->client->itemName()]['id'] ?? null; + + if (empty($mautic_id)) { + $this->logger::log("Mautic response does not contain user ID. Response received: \n\n" . json_encode($response, JSON_PRETTY_PRINT), $user_data); + } + + return $mautic_id; } /** @@ -206,17 +208,13 @@ public function setCustomMappings(array $mapping): object public function add(array $user_data): ?int { - if (empty($user_data['is_marketing_allowed'])) { - return null; // Only opted in contacts please... - } - $id = $this->addContactToMautic($user_data); if (empty($id)) { + $this->logger::log("Empty contact ID received from Mautic API when trying to add contact. User Data: \n\n" . print_r($user_data, true)); return null; } $this->addContactToSegment($id); - return $id; } } diff --git a/includes/Contacts/Update.php b/includes/Contacts/Update.php index 9c73d4f..431f856 100644 --- a/includes/Contacts/Update.php +++ b/includes/Contacts/Update.php @@ -55,9 +55,10 @@ public function __construct(array $user_data) */ public function updateContactTags(array $add_tags = array(), array $remove_tags = array()): int { - $email = $this->user_data['email']; - $contact_details = $this->findContactDetailsByEmail($email); + $id = (int) $this->user_data['id']; + $contact_details = $this->findContactDetailsByFreemiusID($id); if (empty($contact_details)) { + $this->logger::log("Issue updating contact tags. No contact details returned for user id: $id"); return 0; } $contact_id = $contact_details['id']; diff --git a/includes/Logger.php b/includes/Logger.php new file mode 100644 index 0000000..19cd428 --- /dev/null +++ b/includes/Logger.php @@ -0,0 +1,54 @@ +getBody()->getContents(); $body = json_decode($body, true); + $webhook_id = $body['id'] ?? ''; $user_data = $body['objects']['user'] ?? ''; if (empty($user_data)) { - return 'no user data'; + Logger::log("No user data recieved in the webhook #$webhook_id"); + exit('no user data'); + } + + /** + * Only opted in contacts please... + */ + if (empty($user_data['is_marketing_allowed'])) { + return ('user didn\'t opt into marketing'); } $install = $body['objects']['install'] ?? array(); @@ -46,7 +155,8 @@ function init(ServerRequestInterface $request): string $plugin_id = $body['plugin_id'] ?? ''; if (empty($plugin_id)) { - return 'plugin id empty'; + Logger::log('Plugin ID value in webhook is empty.', $body); + exit('plugin id empty'); } $event_type = $body['type'] ?? ''; @@ -56,7 +166,8 @@ function init(ServerRequestInterface $request): string */ $user_email = $user_data['email'] ?? ''; if (in_array($user_email, excludedEmails()) || empty($user_email)) { - return 'excluded email'; + Logger::log("This email address is excluded. User email: $user_email"); + return ('excluded email'); // Return a value so that the webhook file will be deleted. } /** @@ -64,7 +175,8 @@ function init(ServerRequestInterface $request): string */ $domain = $install['url'] ?? ''; if (isExcludedTLD($domain)) { - return "development domain"; + Logger::log("This is a development domain. Domain: $domain"); + return ('development domain'); // Return a value so that the webhook file will be deleted. } $contactCreate = new CreateContact($plugin_id); @@ -75,7 +187,8 @@ function init(ServerRequestInterface $request): string * Bail if the plugin ID received is not in the array provided in productIDs() */ if (empty($product)) { - return "product id not found in array"; + Logger::log('This Product ID was not found in array of available IDs in productIDs() function.', $body); + exit('product id not found in array'); } $installed_tag = $product . '-installed'; @@ -130,14 +243,22 @@ function init(ServerRequestInterface $request): string break; } + http_response_code(200); + + $time_end = microtime(true); + $duration = $time_end - $time_start; + Logger::log("Webhook #$webhook_id execution duration: $duration"); + /** - * This will return null if: + * This will be 0/null if: * + * Event type didn't fall within switch statement. * The username and password set for the Client is wrong. * The URL you set for the Mautic API is wrong. * The contact email is in the excluded list. + * Trying to update a user ID that does not exist. */ - return json_encode($id); + return $id; } // ------ @@ -178,25 +299,21 @@ function productIDs(): array */ function customContactDataMappings(): array { - $product_id = productIDs(); + $product_ids = productIDs(); + + $hold = array(); /** - * Edit this array with your current plugin ids. + * Edit the $hold array with your custom mappings. */ - return array( - $product_id['kikote'] => array( - 'id' => 'freemius_id', - 'gross' => 'kikote_gross', - ), - $product_id['dps'] => array( + foreach ($product_ids as $product_name => $product_id) { + $hold[$product_id] = array( 'id' => 'freemius_id', - 'gross' => 'dps_gross', - ), - $product_id['printus'] => array( - 'id' => 'freemius_id', - 'gross' => 'printus_gross', - ), - ); + 'gross' => $product_name . '_gross', + ); + } + + return $hold; } /** @@ -237,40 +354,26 @@ function contactSegments(): array */ function contactTags(): array { - $product_id = productIDs(); + $product_ids = productIDs(); + + $hold = array(); /** * Edit this array with your current plugin ids. */ - return array( - $product_id['kikote'] => array( // Edit this ID with your plugin ID - 'free-users-tags' => array( - 'kikote-free-user', // Edit these tags with the tag that should be set for Free users. You can add more tags to this sub array. - ), - 'premium-users-tags' => array( - 'kikote-pro-user' // Edit these tags with the tag that should be set for Premium users. You can add more tags to this sub array. - ), - 'kikote-user', // You can set additional tags that you want attached to a contact other than the free/pro ones. - ), - $product_id['dps'] => array( + foreach ($product_ids as $product_name => $product_id) { + $hold[$product_id] = array( 'free-users-tags' => array( - 'dps-free-user' + $product_name . '-free-user', // Edit these tags with the tag that should be set for Free users. You can add more tags to this sub array. ), 'premium-users-tags' => array( - 'dps-pro-user' + $product_name . '-pro-user', // Edit these tags with the tag that should be set for Premium users. You can add more tags to this sub array. ), - 'dps-user' - ), - $product_id['printus'] => array( - 'free-users-tags' => array( - 'printus-free-user' - ), - 'premium-users-tags' => array( - 'printus-pro-user' - ), - 'printus-user' - ), - ); + $product_name . '-user' // You can set additional tags that you want attached to a contact other than the free/pro ones. + ); + } + + return $hold; } /** @@ -304,6 +407,7 @@ function excludedTLDs(): array 'dev', 'instawp.xyz', 'instawp.co', + 'instawp.com', 'instawp.link', 'dev.cc', 'test', diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webhooks/.gitkeep b/webhooks/.gitkeep new file mode 100644 index 0000000..e69de29